Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:35:57 +02:00
commit 1dfbca08a1
528 files changed
+131054

No files matched your search

+372
View File
@@ -0,0 +1,372 @@
"""Strict, secret-free contracts for managed Unraid deployments.
This module intentionally models a small deployment vocabulary. It is not a
Compose parser, a Docker API wrapper, or a generic remote-command interface.
The controller derives filesystem paths and runtime arguments from this plan
after it has accepted the immutable repository revision.
"""
from __future__ import annotations
import posixpath
import re
from typing import Any, Literal
import yaml
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_validator,
model_validator,
)
_APP_ID = re.compile(r"^[a-z][a-z0-9-]{1,62}$")
_REPOSITORY = re.compile(r"^jens/[a-z0-9][a-z0-9_.-]{0,96}$")
_REVISION = re.compile(r"^[0-9a-f]{40}$")
_VERSION = re.compile(r"^v?(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*)){2}(?:-[0-9A-Za-z.-]+)?$")
_NAME = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,62}$")
_DOMAIN = re.compile(
r"^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$"
)
_SECRET_REFERENCE = re.compile(r"^[A-Z][A-Z0-9_]{2,120}$")
_SAFE_PATH = re.compile(
r"^(?!/)(?!.*(?:^|/)\.\.(?:/|$))(?:[A-Za-z0-9]|\.[A-Za-z0-9])[A-Za-z0-9_./-]{0,239}$"
)
TRUSTED_APPCONFIG_ROOT = "/srv/example/appdata"
class _StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True, str_strip_whitespace=True)
class DeploymentTopologyV1(_StrictModel):
"""The only runtime shape accepted by V1.
V1 is deliberately compose-only and always private behind the existing
proxy network. A public host binding, Docker socket or host networking is
outside this contract.
"""
kind: Literal["compose"] = "compose"
compose_file: str = ".itworx/deployment.yml"
compose_project: str = Field(min_length=2, max_length=63)
containers: tuple[str, ...] = Field(min_length=1, max_length=8)
proxy_network: str = Field(min_length=2, max_length=63)
internal_port: int = Field(ge=1, le=65535)
restart_policy: Literal["unless-stopped"] = "unless-stopped"
build: RepositoryBuildV1 | None = None
@field_validator("compose_file")
@classmethod
def safe_compose_file(cls, value: str) -> str:
if not _SAFE_PATH.fullmatch(value) or not value.endswith((".yml", ".yaml")):
raise ValueError("compose_file must be a safe repository-relative YAML path")
return value
@field_validator("compose_project", "proxy_network", "containers")
@classmethod
def safe_runtime_name(cls, value: str | tuple[str, ...]) -> str | tuple[str, ...]:
values = value if isinstance(value, tuple) else (value,)
if any(not _NAME.fullmatch(item) for item in values):
raise ValueError("runtime names must use the bounded deployment identifier format")
if len(set(values)) != len(values):
raise ValueError("container names must be unique")
return value
class RepositoryBuildV1(_StrictModel):
"""A controller-revalidated repository build, never a caller shell step."""
context: str = "."
dockerfile: str = "Dockerfile"
@field_validator("context")
@classmethod
def safe_context(cls, value: str) -> str:
if value != "." and not _SAFE_PATH.fullmatch(value):
raise ValueError("build context must be a safe repository-relative path")
return value
@field_validator("dockerfile")
@classmethod
def safe_dockerfile(cls, value: str) -> str:
if not _SAFE_PATH.fullmatch(value) or not value.endswith("Dockerfile"):
raise ValueError("dockerfile must be a safe repository-relative Dockerfile path")
return value
DeploymentTopologyV1.model_rebuild()
class ProxyHostV1(_StrictModel):
hostname: str
certificate_mode: Literal["existing", "managed"] = "managed"
force_ssl: bool = True
http2: bool = True
upload_limit_mb: int = Field(default=256, ge=1, le=2048)
read_timeout_seconds: int = Field(default=180, ge=15, le=900)
@field_validator("hostname")
@classmethod
def safe_hostname(cls, value: str) -> str:
normalized = value.lower().rstrip(".")
if not _DOMAIN.fullmatch(normalized) or normalized.startswith("-"):
raise ValueError("hostname must be a canonical DNS hostname")
return normalized
@model_validator(mode="after")
def require_https_redirect(self) -> ProxyHostV1:
if not self.force_ssl:
raise ValueError("managed proxy hosts must force HTTPS")
return self
class OidcRoleMappingV1(_StrictModel):
administrator_group: str = Field(min_length=1, max_length=120)
support_agent_group: str = Field(min_length=1, max_length=120)
read_only_group: str = Field(min_length=1, max_length=120)
@field_validator("administrator_group", "support_agent_group", "read_only_group")
@classmethod
def safe_group_name(cls, value: str) -> str:
if any(character in value for character in "\r\n\x00"):
raise ValueError("role group names may not contain control characters")
return value
class OidcProvisioningV1(_StrictModel):
provider: Literal["authentik"] = "authentik"
application_slug: str = Field(min_length=2, max_length=63)
redirect_path: str = "/signin-oidc"
role_claim: str = Field(default="groups", min_length=1, max_length=80)
client_secret_ref: str
roles: OidcRoleMappingV1
@field_validator("application_slug")
@classmethod
def safe_application_slug(cls, value: str) -> str:
if not _APP_ID.fullmatch(value):
raise ValueError("application_slug must be a bounded lower-case identifier")
return value
@field_validator("client_secret_ref")
@classmethod
def secret_reference_only(cls, value: str) -> str:
if not _SECRET_REFERENCE.fullmatch(value):
raise ValueError("client_secret_ref must reference a stored AppOps secret, never a value")
return value
@field_validator("redirect_path")
@classmethod
def safe_redirect_path(cls, value: str) -> str:
if not value.startswith("/") or "//" in value or any(character in value for character in "?#\\\r\n"):
raise ValueError("redirect_path must be a relative HTTPS callback path")
return value
class DeploymentPlanV1(_StrictModel):
"""A validated, immutable deployment intent for one managed application."""
api_version: Literal["itworx.deployment/v1"] = "itworx.deployment/v1"
application_id: str
repository: str
revision: str
release_version: str
image_repository: str
image_tag: str
topology: DeploymentTopologyV1
proxy: ProxyHostV1 | None = None
oidc: OidcProvisioningV1 | None = None
health_path: str = "/healthz"
health_timeout_seconds: int = Field(default=180, ge=15, le=900)
broker_secret_ref: str
rollback_enabled: bool = True
@field_validator("application_id")
@classmethod
def safe_app_id(cls, value: str) -> str:
if not _APP_ID.fullmatch(value):
raise ValueError("application_id must be a bounded lower-case identifier")
return value
@field_validator("repository")
@classmethod
def safe_repository(cls, value: str) -> str:
normalized = value.lower()
if not _REPOSITORY.fullmatch(normalized):
raise ValueError("repository must be a canonical Jens repository")
return normalized
@field_validator("revision")
@classmethod
def immutable_revision(cls, value: str) -> str:
normalized = value.lower()
if not _REVISION.fullmatch(normalized):
raise ValueError("revision must be a full lower-case 40-character Git SHA")
return normalized
@field_validator("release_version")
@classmethod
def semantic_version(cls, value: str) -> str:
if not _VERSION.fullmatch(value):
raise ValueError("release_version must be a semantic version")
return value.removeprefix("v")
@field_validator("image_repository")
@classmethod
def safe_image_repository(cls, value: str) -> str:
normalized = value.lower()
if not re.fullmatch(r"[a-z0-9][a-z0-9._/-]{1,180}", normalized):
raise ValueError("image_repository is not a safe image identifier")
return normalized
@field_validator("image_tag")
@classmethod
def immutable_image_tag(cls, value: str) -> str:
if value.lower() == "latest" or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}", value):
raise ValueError("image_tag must be an explicit non-latest tag")
return value
@field_validator("broker_secret_ref")
@classmethod
def broker_secret_reference_only(cls, value: str) -> str:
if not _SECRET_REFERENCE.fullmatch(value):
raise ValueError("broker_secret_ref must reference a stored AppOps secret, never a value")
return value
@field_validator("health_path")
@classmethod
def safe_health_path(cls, value: str) -> str:
if not value.startswith("/") or "//" in value or any(character in value for character in "?#\\\r\n"):
raise ValueError("health_path must be a relative path")
return value
@model_validator(mode="after")
def validate_cross_field_contract(self) -> DeploymentPlanV1:
if self.application_id not in self.topology.containers:
raise ValueError("application_id must be one of the managed container names")
if self.topology.compose_project != self.application_id:
raise ValueError("compose_project must equal application_id in DeploymentPlanV1")
if self.image_tag not in {self.release_version, self.revision[:12]}:
raise ValueError("image_tag must be the release version or immutable revision prefix")
if self.oidc and not self.proxy:
raise ValueError("OIDC provisioning requires a managed HTTPS proxy host")
return self
@property
def appdata_path(self) -> str:
return f"{TRUSTED_APPCONFIG_ROOT}/{self.application_id}"
@property
def public_url(self) -> str | None:
return f"https://{self.proxy.hostname}" if self.proxy else None
def validate_compose_runtime_contract(plan: DeploymentPlanV1, raw_yaml: str) -> dict[str, Any]:
"""Validate a repository compose artifact against the narrow V1 policy.
The result is suitable for evidence/audit storage and deliberately contains
no Compose environment values. It rejects mounts, privileged flags and
runtime capabilities that would turn a product deployment into a host
control interface.
"""
try:
document = yaml.safe_load(raw_yaml)
except yaml.YAMLError as exc:
raise ValueError("deployment compose YAML could not be parsed") from exc
if not isinstance(document, dict) or set(document) - {"services", "networks", "name"}:
raise ValueError("deployment compose only permits name, services and networks")
services = document.get("services")
if not isinstance(services, dict) or not services:
raise ValueError("deployment compose must define services")
expected = set(plan.topology.containers)
observed = set(services)
if observed != expected:
raise ValueError("deployment compose services must exactly match the typed plan containers")
for service_name, service in services.items():
if not isinstance(service, dict):
raise TypeError("deployment service must be an object")
forbidden = {
"privileged",
"devices",
"pid",
"ipc",
"cap_add",
"dockerfile",
}
present = forbidden.intersection(service)
if present:
raise ValueError(f"deployment service {service_name} uses forbidden runtime fields")
build = service.get("build")
if plan.topology.build is None:
if build:
raise ValueError("deployment service build is not declared by the typed plan")
else:
if isinstance(build, str):
observed_build = {"context": build, "dockerfile": "Dockerfile"}
elif isinstance(build, dict):
observed_build = {
"context": str(build.get("context") or "."),
"dockerfile": str(build.get("dockerfile") or "Dockerfile"),
}
if set(build) - {"context", "dockerfile"}:
raise ValueError("deployment service build contains unsupported fields")
else:
raise ValueError("deployment service build must be declared by the typed plan")
expected_build = plan.topology.build.model_dump()
if observed_build != expected_build:
raise ValueError("deployment service build does not match the typed plan")
if service.get("network_mode") in {"host", "service:host"}:
raise ValueError("host networking is forbidden")
if service.get("ports"):
raise ValueError("published host ports are forbidden for managed proxy deployments")
security_options = service.get("security_opt") or []
if set(security_options) - {"no-new-privileges:true"}:
raise ValueError("deployment service uses unsupported security options")
if service.get("read_only") is not True:
raise ValueError("deployment services must use a read-only root filesystem")
if service.get("cap_drop") != ["ALL"]:
raise ValueError("deployment services must drop every Linux capability")
if service.get("restart") != "unless-stopped":
raise ValueError("deployment services must use restart: unless-stopped")
image = str(service.get("image") or "")
if image != f"{plan.image_repository}:{plan.image_tag}":
raise ValueError("deployment service image must equal the immutable plan image")
owned_bind_mounts = 0
for mount in service.get("volumes", []) or []:
if isinstance(mount, dict) and mount.get("type") not in {None, "bind"}:
raise ValueError("deployment services permit only application-owned bind mounts")
source = mount.get("source") if isinstance(mount, dict) else str(mount).split(":", 1)[0]
source_text = str(source or "")
normalized_source = posixpath.normpath(source_text)
if "docker.sock" in source_text:
raise ValueError("docker socket and host-control mounts are forbidden")
if (
not source_text.startswith("/")
or normalized_source != source_text
or source_text in {"/", "/proc", "/sys"}
):
raise ValueError("only normalized application-owned bind mounts are permitted")
if source_text != plan.appdata_path and not source_text.startswith(plan.appdata_path + "/"):
raise ValueError("bind mounts must stay below the plan's owned appdata path")
owned_bind_mounts += 1
if not owned_bind_mounts:
raise ValueError("deployment services require an application-owned persistent bind mount")
networks = service.get("networks")
if networks != [plan.topology.proxy_network] and networks != {plan.topology.proxy_network: None}:
raise ValueError("deployment services must use only the typed proxy network")
networks = document.get("networks")
if networks != {plan.topology.proxy_network: {"external": True}}:
raise ValueError("deployment compose must reference exactly one existing proxy network")
return {
"services": sorted(observed),
"proxyNetwork": plan.topology.proxy_network,
"appdataPath": plan.appdata_path,
"image": f"{plan.image_repository}:{plan.image_tag}",
}