@mriekki/project-forge 4.26.0 → 4.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/authentication/distribution.json +90 -0
- package/assets/components.json +18 -4
- package/assets/core/distribution.json +12 -0
- package/assets/deployment/distribution.json +292 -68
- package/assets/generated/ASSEMBLY.json +152 -6
- package/assets/generated/authentication/docs/AUTH_RECOVERY_RETENTION.md +101 -0
- package/assets/generated/authentication/docs/HOUSEHOLD_INFRASTRUCTURE_ADOPTION.md +55 -0
- package/assets/generated/authentication/docs/HOUSEHOLD_REGISTRY.md +246 -0
- package/assets/generated/authentication/docs/PROJECT_AUTH_REFERENCE.md +878 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/Program.cs +40 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/ProjectForgeAuth.Api.csproj +13 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/auth.js +65 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/index.html +44 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/AdmissionPolicy.cs +167 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/AuthenticationDiagnostics.cs +159 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/ProjectForge.Auth.csproj +9 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/PwaExistingProjectCompatibility.cs +64 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectAuthStore.cs +471 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForge.Auth.Runtime.csproj +15 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForgeAuthRuntimeExtensions.cs +472 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ResendProjectInvitationDelivery.cs +105 -0
- package/assets/generated/authentication/src/ProjectForge.Transport/ProjectForge.Transport.csproj +11 -0
- package/assets/generated/authentication/src/ProjectForge.Transport/StrictPublicOriginTransport.cs +95 -0
- package/assets/generated/authentication/tools/household_connect.py +145 -0
- package/assets/generated/authentication/tools/project_auth_recovery.py +716 -0
- package/assets/generated/core/core/process/ENGINEERING_BACKLOG.md +150 -0
- package/assets/generated/core/core/skills/manage-backlog/SKILL.md +115 -0
- package/assets/generated/core/core/skills/review-user-feedback/SKILL.md +50 -0
- package/assets/generated/core/tools/setup_maintenance/maintenance.py +162 -6
- package/assets/generated/deployment/docs/RECOVERY_READINESS.md +103 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_capture.py +164 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_guard.py +225 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_lifecycle.py +455 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_retention.py +220 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_schedule.py +489 -0
- package/package.json +1 -1
- package/src/cli.mjs +32 -3
- package/src/engine.mjs +87 -5
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Exact provenance + stopped-Docker adapter for isolated Auth Kit capture.
|
|
2
|
+
|
|
3
|
+
Internal library only. Never stops/restarts a service, configures a provider,
|
|
4
|
+
logs Docker environment or offers an operator-ready Production backup CLI.
|
|
5
|
+
The caller independently manages quiescence and must validate service
|
|
6
|
+
resumption/health, scheduling, alert receipt and application restore.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import re
|
|
15
|
+
import stat
|
|
16
|
+
import sys
|
|
17
|
+
import types
|
|
18
|
+
|
|
19
|
+
from .auth_recovery_guard import docker_auth_quiescence_guard
|
|
20
|
+
from .errors import DeploymentKitError
|
|
21
|
+
|
|
22
|
+
SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
23
|
+
REVISION = re.compile(r"^[0-9a-f]{40}$")
|
|
24
|
+
HELPER_PATH = "tools/project-forge-auth-kit/project_auth_recovery.py"
|
|
25
|
+
BASELINE_NAME = "AUTHENTICATION_BASELINE.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _safe_regular(path: Path) -> os.stat_result:
|
|
29
|
+
for part in (path, *path.parents):
|
|
30
|
+
if part.is_symlink():
|
|
31
|
+
raise DeploymentKitError("Managed recovery source contains a symlink.")
|
|
32
|
+
try:
|
|
33
|
+
st = path.lstat()
|
|
34
|
+
except OSError as exc:
|
|
35
|
+
raise DeploymentKitError("Managed recovery source is missing.") from exc
|
|
36
|
+
if not stat.S_ISREG(st.st_mode) or st.st_nlink != 1:
|
|
37
|
+
raise DeploymentKitError("Managed recovery source is not a regular owned file.")
|
|
38
|
+
return st
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_pinned_helper(
|
|
42
|
+
project_root: Path, reviewed_revision: str, reviewed_sha256: str
|
|
43
|
+
):
|
|
44
|
+
"""Load exactly reviewed source bytes, not an attacker-modified pathname."""
|
|
45
|
+
if (type(reviewed_revision) is not str or
|
|
46
|
+
REVISION.fullmatch(reviewed_revision) is None or
|
|
47
|
+
type(reviewed_sha256) is not str or
|
|
48
|
+
SHA256.fullmatch(reviewed_sha256) is None):
|
|
49
|
+
raise DeploymentKitError("Exact reviewed Auth component provenance required.")
|
|
50
|
+
root = Path(os.path.abspath(project_root))
|
|
51
|
+
for part in (root, *root.parents):
|
|
52
|
+
if part.is_symlink():
|
|
53
|
+
raise DeploymentKitError("Project root may not contain a symlink.")
|
|
54
|
+
if not root.is_dir():
|
|
55
|
+
raise DeploymentKitError("Generated Project root does not exist.")
|
|
56
|
+
baseline_path = root / BASELINE_NAME
|
|
57
|
+
helper_path = root / HELPER_PATH
|
|
58
|
+
_safe_regular(baseline_path)
|
|
59
|
+
_safe_regular(helper_path)
|
|
60
|
+
try:
|
|
61
|
+
if baseline_path.stat().st_size > 512 * 1024:
|
|
62
|
+
raise DeploymentKitError("Auth baseline is too large.")
|
|
63
|
+
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
|
|
64
|
+
except (ValueError, UnicodeError, OSError) as exc:
|
|
65
|
+
raise DeploymentKitError("Auth baseline is unreadable.") from exc
|
|
66
|
+
if (type(baseline) is not dict or
|
|
67
|
+
baseline.get("schemaVersion") != 2 or
|
|
68
|
+
baseline.get("component") != "authentication" or
|
|
69
|
+
baseline.get("distribution") != "greenfield-project" or
|
|
70
|
+
baseline.get("revision") != reviewed_revision):
|
|
71
|
+
raise DeploymentKitError("Installed Authentication baseline identity differs from reviewed revision.")
|
|
72
|
+
entries = baseline.get("managedPaths")
|
|
73
|
+
if type(entries) is not list or not entries or len(entries) > 1000:
|
|
74
|
+
raise DeploymentKitError("Installed Authentication managed-file ledger is invalid.")
|
|
75
|
+
matching = []
|
|
76
|
+
seen = set()
|
|
77
|
+
for entry in entries:
|
|
78
|
+
if (type(entry) is not dict or
|
|
79
|
+
set(entry) != {"source", "path", "sha256"} or
|
|
80
|
+
type(entry["path"]) is not str or
|
|
81
|
+
type(entry["sha256"]) is not str or
|
|
82
|
+
SHA256.fullmatch(entry["sha256"]) is None or
|
|
83
|
+
entry["path"] in seen):
|
|
84
|
+
raise DeploymentKitError("Installed Authentication managed-file ledger is malformed.")
|
|
85
|
+
seen.add(entry["path"])
|
|
86
|
+
if entry["path"] == HELPER_PATH:
|
|
87
|
+
matching.append(entry)
|
|
88
|
+
if len(matching) != 1 or matching[0]["sha256"] != reviewed_sha256:
|
|
89
|
+
raise DeploymentKitError("Auth recovery helper is missing from reviewed managed baseline.")
|
|
90
|
+
|
|
91
|
+
# Hash and compile the same bytes; no check-then-import path race.
|
|
92
|
+
try:
|
|
93
|
+
with helper_path.open("rb") as source:
|
|
94
|
+
data = source.read(256 * 1024 + 1)
|
|
95
|
+
if (not data or len(data) > 256 * 1024 or
|
|
96
|
+
hashlib.sha256(data).hexdigest() != reviewed_sha256):
|
|
97
|
+
raise DeploymentKitError("Auth recovery helper bytes differ from reviewed release.")
|
|
98
|
+
module = types.ModuleType("_project_forge_verified_auth_recovery")
|
|
99
|
+
module.__file__ = str(helper_path)
|
|
100
|
+
exec(compile(data, str(helper_path), "exec"), module.__dict__)
|
|
101
|
+
except DeploymentKitError:
|
|
102
|
+
raise
|
|
103
|
+
except (OSError, SyntaxError, UnicodeError, ValueError, RuntimeError) as exc:
|
|
104
|
+
raise DeploymentKitError("Reviewed Auth recovery helper cannot be loaded.") from exc
|
|
105
|
+
if not callable(getattr(module, "capture_quiesced_set", None)):
|
|
106
|
+
raise DeploymentKitError("Reviewed Auth recovery helper is missing its capture contract.")
|
|
107
|
+
return module
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def capture_reviewed_stopped_auth_project(
|
|
111
|
+
*, project_root: Path, source_data_root: Path, destination: Path,
|
|
112
|
+
declaration: dict, reviewed_auth_revision: str,
|
|
113
|
+
reviewed_recovery_sha256: str, reviewed_docker_binding: dict
|
|
114
|
+
) -> dict:
|
|
115
|
+
"""Write only a new private capture when exact reviewed Docker is stopped.
|
|
116
|
+
|
|
117
|
+
The source/storage, expected image, labels, container name and binding must
|
|
118
|
+
come from separately reviewed Project deployment evidence. The helper's
|
|
119
|
+
accepted SHA and component revision must come from an independent immutable
|
|
120
|
+
release/provenance receipt, not merely the editable local baseline.
|
|
121
|
+
"""
|
|
122
|
+
if sys.platform != "linux":
|
|
123
|
+
raise DeploymentKitError("Stopped Auth Project capture is Linux-host only.")
|
|
124
|
+
source = Path(os.path.abspath(source_data_root))
|
|
125
|
+
if type(reviewed_docker_binding) is not dict or set(reviewed_docker_binding) != {
|
|
126
|
+
"container_name", "expected_owner", "expected_environment",
|
|
127
|
+
"expected_image_digest", "expected_data_source",
|
|
128
|
+
"expected_data_destination", "owner_label", "environment_label",
|
|
129
|
+
"required_auth_environment",
|
|
130
|
+
}:
|
|
131
|
+
raise DeploymentKitError("Exact reviewed Docker binding is required.")
|
|
132
|
+
bind_source = reviewed_docker_binding["expected_data_source"]
|
|
133
|
+
if not isinstance(bind_source, Path) or Path(os.path.abspath(bind_source)) != source:
|
|
134
|
+
raise DeploymentKitError("Reviewed Docker data source differs from Auth data root.")
|
|
135
|
+
required = reviewed_docker_binding["required_auth_environment"]
|
|
136
|
+
mount = reviewed_docker_binding["expected_data_destination"]
|
|
137
|
+
if (type(required) is not dict or type(mount) is not str or
|
|
138
|
+
required.get("ProjectForge__Authentication__DbPath") !=
|
|
139
|
+
mount.rstrip("/") + "/auth.sqlite" or
|
|
140
|
+
required.get("ProjectForge__Authentication__DataProtectionKeysPath") !=
|
|
141
|
+
mount.rstrip("/") + "/keys"):
|
|
142
|
+
raise DeploymentKitError("Exact Auth database and key-ring container bindings required.")
|
|
143
|
+
helper = _load_pinned_helper(
|
|
144
|
+
project_root, reviewed_auth_revision, reviewed_recovery_sha256)
|
|
145
|
+
try:
|
|
146
|
+
guard = docker_auth_quiescence_guard(**reviewed_docker_binding)
|
|
147
|
+
proof = guard()
|
|
148
|
+
if (type(proof) is not dict or proof.get("stopped") is not True):
|
|
149
|
+
raise DeploymentKitError("Exact stopped Auth service not proven.")
|
|
150
|
+
report = helper.capture_quiesced_set(
|
|
151
|
+
source, Path(destination), declaration, guard)
|
|
152
|
+
except DeploymentKitError:
|
|
153
|
+
raise
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
# Do not print Docker inspect, private host paths, credential env or
|
|
156
|
+
# key files on failure; never return apparent success.
|
|
157
|
+
raise DeploymentKitError("Reviewed Auth backup capture failed closed.") from exc
|
|
158
|
+
if (type(report) is not dict or
|
|
159
|
+
report.get("backupArtifactIntegrityVerified") is not True or
|
|
160
|
+
report.get("applicationRecoveryReady") is not False or
|
|
161
|
+
report.get("quiescenceGuardRuntimeId") != proof.get("runtimeId") or
|
|
162
|
+
report.get("quiescenceGuardImageDigest") != proof.get("imageDigest")):
|
|
163
|
+
raise DeploymentKitError("Auth recovery helper did not prove expected capture receipt.")
|
|
164
|
+
return report
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Read-only, exact Docker quiescence evidence for future Auth Kit recovery.
|
|
2
|
+
|
|
3
|
+
No container stop/restart, backup, key read, provider operation or Production
|
|
4
|
+
mutation is performed. Do not expose this primitive as a supported backup
|
|
5
|
+
workflow until Deployment Kit owns a reviewed quiescence orchestration driver.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import stat
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Any, Callable
|
|
18
|
+
|
|
19
|
+
from .errors import DeploymentKitError
|
|
20
|
+
|
|
21
|
+
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
|
|
22
|
+
_CONTAINER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _exact_dict(value: object, required: set[str], context: str) -> dict:
|
|
26
|
+
if not isinstance(value, dict) or not required.issubset(value):
|
|
27
|
+
raise DeploymentKitError(f"Invalid {context} runtime evidence.")
|
|
28
|
+
return value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _host_directory_is_real(path: Path) -> None:
|
|
32
|
+
# Local host only. A symlink anywhere in the owned mount path is
|
|
33
|
+
# forbidden even when its final resolution would be inside the root.
|
|
34
|
+
if not path.is_absolute():
|
|
35
|
+
raise DeploymentKitError("Auth recovery data mount must be absolute.")
|
|
36
|
+
for part in (path, *path.parents):
|
|
37
|
+
if part.is_symlink():
|
|
38
|
+
raise DeploymentKitError("Auth recovery mount may not use symlinks.")
|
|
39
|
+
try:
|
|
40
|
+
st = path.lstat()
|
|
41
|
+
except OSError as error:
|
|
42
|
+
raise DeploymentKitError("Auth recovery data directory is unavailable.") from error
|
|
43
|
+
if not stat.S_ISDIR(st.st_mode):
|
|
44
|
+
raise DeploymentKitError("Auth recovery data mount must be a directory.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def verify_stopped_auth_container(
|
|
48
|
+
observed: dict[str, Any],
|
|
49
|
+
*,
|
|
50
|
+
container_name: str,
|
|
51
|
+
expected_owner: str,
|
|
52
|
+
expected_environment: str,
|
|
53
|
+
expected_image_digest: str,
|
|
54
|
+
expected_data_source: Path,
|
|
55
|
+
expected_data_destination: str,
|
|
56
|
+
owner_label: str,
|
|
57
|
+
environment_label: str,
|
|
58
|
+
required_auth_environment: dict[str, str],
|
|
59
|
+
) -> dict[str, object]:
|
|
60
|
+
"""Return only non-secret proof for an exactly owned, stopped container.
|
|
61
|
+
|
|
62
|
+
The caller must provide expected identity, image, labels, mount and auth
|
|
63
|
+
settings from a reviewed Project deployment receipt. This does not stop
|
|
64
|
+
the application or establish a viable restored application.
|
|
65
|
+
"""
|
|
66
|
+
if not _CONTAINER.fullmatch(container_name):
|
|
67
|
+
raise DeploymentKitError("Invalid inspected container name.")
|
|
68
|
+
if not _HEX64.fullmatch(expected_image_digest.removeprefix("sha256:")) or (
|
|
69
|
+
not expected_image_digest.startswith("sha256:")
|
|
70
|
+
):
|
|
71
|
+
raise DeploymentKitError("An exact immutable Docker image digest is required.")
|
|
72
|
+
if (not isinstance(expected_owner, str) or not expected_owner or
|
|
73
|
+
not isinstance(expected_environment, str) or not expected_environment or
|
|
74
|
+
not isinstance(owner_label, str) or not owner_label or
|
|
75
|
+
not isinstance(environment_label, str) or not environment_label or
|
|
76
|
+
type(required_auth_environment) is not dict or
|
|
77
|
+
not required_auth_environment or
|
|
78
|
+
any(not isinstance(k, str) or not k or not isinstance(v, str)
|
|
79
|
+
for k, v in required_auth_environment.items())):
|
|
80
|
+
raise DeploymentKitError("Exact Auth recovery ownership declaration required.")
|
|
81
|
+
mount_root = Path(expected_data_source)
|
|
82
|
+
_host_directory_is_real(mount_root)
|
|
83
|
+
if (not isinstance(expected_data_destination, str) or
|
|
84
|
+
not expected_data_destination.startswith("/") or
|
|
85
|
+
".." in expected_data_destination.split("/")):
|
|
86
|
+
raise DeploymentKitError("Invalid reviewed container data destination.")
|
|
87
|
+
|
|
88
|
+
info = _exact_dict(observed, {
|
|
89
|
+
"Id", "Name", "Image", "State", "Config", "HostConfig", "Mounts"
|
|
90
|
+
}, "container")
|
|
91
|
+
runtime_id = info["Id"]
|
|
92
|
+
if (not isinstance(runtime_id, str) or not _HEX64.fullmatch(runtime_id) or
|
|
93
|
+
info["Name"] != "/" + container_name or
|
|
94
|
+
info["Image"] != expected_image_digest):
|
|
95
|
+
raise DeploymentKitError("Auth container identity or image drift detected.")
|
|
96
|
+
state = _exact_dict(info["State"], {"Running", "Restarting", "Status"}, "state")
|
|
97
|
+
policy = _exact_dict(info["HostConfig"], {"RestartPolicy"}, "host configuration")
|
|
98
|
+
restart = _exact_dict(policy["RestartPolicy"], {"Name"}, "restart policy")
|
|
99
|
+
if (state["Running"] is not False or state["Restarting"] is not False or
|
|
100
|
+
state["Status"] != "exited" or restart["Name"] != "no"):
|
|
101
|
+
raise DeploymentKitError(
|
|
102
|
+
"Auth recovery requires a stopped, restart-disabled exact container."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
config = _exact_dict(info["Config"], {"Labels", "Env"}, "configuration")
|
|
106
|
+
labels = _exact_dict(config["Labels"], {owner_label, environment_label}, "labels")
|
|
107
|
+
if (labels[owner_label] != expected_owner or
|
|
108
|
+
labels[environment_label] != expected_environment):
|
|
109
|
+
raise DeploymentKitError("Auth runtime ownership labels do not match.")
|
|
110
|
+
raw_env = config["Env"]
|
|
111
|
+
if not isinstance(raw_env, list):
|
|
112
|
+
raise DeploymentKitError("Malformed Auth runtime environment.")
|
|
113
|
+
env: dict[str, str] = {}
|
|
114
|
+
for item in raw_env:
|
|
115
|
+
if not isinstance(item, str) or "=" not in item:
|
|
116
|
+
raise DeploymentKitError("Malformed Auth runtime environment.")
|
|
117
|
+
name, value = item.split("=", 1)
|
|
118
|
+
if name in env or not name:
|
|
119
|
+
raise DeploymentKitError("Duplicate Auth runtime environment key.")
|
|
120
|
+
env[name] = value
|
|
121
|
+
for key, value in required_auth_environment.items():
|
|
122
|
+
if env.get(key) != value:
|
|
123
|
+
raise DeploymentKitError("Auth runtime binding differs from approved receipt.")
|
|
124
|
+
|
|
125
|
+
mounts = info["Mounts"]
|
|
126
|
+
if not isinstance(mounts, list):
|
|
127
|
+
raise DeploymentKitError("Malformed Auth container mount inventory.")
|
|
128
|
+
data = [m for m in mounts if isinstance(m, dict)
|
|
129
|
+
and m.get("Destination") == expected_data_destination]
|
|
130
|
+
if (len(data) != 1 or data[0].get("Type") != "bind" or
|
|
131
|
+
data[0].get("RW") is not True or
|
|
132
|
+
data[0].get("Source") != str(mount_root)):
|
|
133
|
+
raise DeploymentKitError("Auth owned data bind is not the reviewed mount.")
|
|
134
|
+
if any(not isinstance(m, dict) for m in mounts):
|
|
135
|
+
raise DeploymentKitError("Malformed Auth container mount inventory.")
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"stopped": True,
|
|
139
|
+
"runtimeId": runtime_id,
|
|
140
|
+
"imageDigest": expected_image_digest,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def verify_running_auth_container(
|
|
146
|
+
observed: dict[str, Any], **binding: Any
|
|
147
|
+
) -> dict[str, object]:
|
|
148
|
+
"""Verify the *same* strict Auth owner/image/mount, but in running state.
|
|
149
|
+
|
|
150
|
+
Reuse the stopped proof's complete configuration inspection after
|
|
151
|
+
independently checking the actual running state. The normalized copy
|
|
152
|
+
below is verification-only and never sent to Docker.
|
|
153
|
+
"""
|
|
154
|
+
if type(observed) is not dict:
|
|
155
|
+
raise DeploymentKitError("Malformed running Auth Docker inspection.")
|
|
156
|
+
state = observed.get("State")
|
|
157
|
+
if (type(state) is not dict or state.get("Running") is not True
|
|
158
|
+
or state.get("Restarting") is not False
|
|
159
|
+
or state.get("Status") != "running"):
|
|
160
|
+
raise DeploymentKitError("Exact Auth container is not running.")
|
|
161
|
+
projection = {
|
|
162
|
+
**observed,
|
|
163
|
+
"State": {**state, "Running": False, "Status": "exited"},
|
|
164
|
+
}
|
|
165
|
+
proof = verify_stopped_auth_container(projection, **binding)
|
|
166
|
+
return {
|
|
167
|
+
"running": True,
|
|
168
|
+
"runtimeId": proof["runtimeId"],
|
|
169
|
+
"imageDigest": proof["imageDigest"],
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def docker_auth_quiescence_guard(
|
|
174
|
+
*,
|
|
175
|
+
container_name: str,
|
|
176
|
+
expected_owner: str,
|
|
177
|
+
expected_environment: str,
|
|
178
|
+
expected_image_digest: str,
|
|
179
|
+
expected_data_source: Path,
|
|
180
|
+
expected_data_destination: str,
|
|
181
|
+
owner_label: str,
|
|
182
|
+
environment_label: str,
|
|
183
|
+
required_auth_environment: dict[str, str],
|
|
184
|
+
) -> Callable[[], dict[str, object]]:
|
|
185
|
+
"""Build a read-only local Linux Docker inspect guard, to call twice.
|
|
186
|
+
|
|
187
|
+
Raw Docker inspect includes sensitive environment material. Never log the
|
|
188
|
+
response, return it, or include stderr in user-facing failure messages.
|
|
189
|
+
"""
|
|
190
|
+
if sys.platform != "linux":
|
|
191
|
+
raise DeploymentKitError(
|
|
192
|
+
"Auth recovery Docker quiescence requires a reviewed Linux host driver."
|
|
193
|
+
)
|
|
194
|
+
if not _CONTAINER.fullmatch(container_name):
|
|
195
|
+
raise DeploymentKitError("Invalid container identity.")
|
|
196
|
+
docker = shutil.which("docker")
|
|
197
|
+
if docker is None:
|
|
198
|
+
raise DeploymentKitError("Docker runtime is unavailable.")
|
|
199
|
+
|
|
200
|
+
def inspect() -> dict[str, object]:
|
|
201
|
+
try:
|
|
202
|
+
cmd = subprocess.run(
|
|
203
|
+
[docker, "inspect", "--format", "{{json .}}", container_name],
|
|
204
|
+
capture_output=True, text=True, check=False,
|
|
205
|
+
timeout=15, encoding="utf-8", errors="replace",
|
|
206
|
+
)
|
|
207
|
+
if cmd.returncode != 0 or len(cmd.stdout) > 256 * 1024:
|
|
208
|
+
raise DeploymentKitError("Auth runtime inspect failed closed.")
|
|
209
|
+
observed = json.loads(cmd.stdout)
|
|
210
|
+
except (OSError, subprocess.TimeoutExpired, ValueError) as error:
|
|
211
|
+
raise DeploymentKitError(
|
|
212
|
+
"Auth runtime evidence is unavailable or invalid."
|
|
213
|
+
) from error
|
|
214
|
+
return verify_stopped_auth_container(
|
|
215
|
+
observed, container_name=container_name,
|
|
216
|
+
expected_owner=expected_owner,
|
|
217
|
+
expected_environment=expected_environment,
|
|
218
|
+
expected_image_digest=expected_image_digest,
|
|
219
|
+
expected_data_source=expected_data_source,
|
|
220
|
+
expected_data_destination=expected_data_destination,
|
|
221
|
+
owner_label=owner_label,
|
|
222
|
+
environment_label=environment_label,
|
|
223
|
+
required_auth_environment=required_auth_environment,
|
|
224
|
+
)
|
|
225
|
+
return inspect
|