Protect private bank configuration from drift reconciliation (Refs #152)
This commit is contained in:
@@ -35,7 +35,7 @@ import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
VERSION = "1.9.2"
|
||||
VERSION = "1.9.3"
|
||||
LABEL = "io.electric-cherry"
|
||||
MAX_FILE = 32 * 1024 * 1024
|
||||
MANIFEST_MAX_ENTRIES = 10000
|
||||
@@ -400,7 +400,17 @@ def managed_open(root, value):
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def private_bank_config(value):
|
||||
return isinstance(value, str) and PurePosixPath(value).as_posix() == "configs/electric_cherry_bank.cfg"
|
||||
|
||||
|
||||
def require_public_file(value):
|
||||
if private_bank_config(value):
|
||||
raise Rejected("Private bank configuration cannot be read or changed by file operations")
|
||||
|
||||
|
||||
def managed_read(root, value):
|
||||
require_public_file(value)
|
||||
try:
|
||||
with managed_open(root, value) as stream:
|
||||
if os.fstat(stream.fileno()).st_size > MAX_FILE:
|
||||
@@ -446,6 +456,8 @@ def managed_manifest(root, payload, stopping):
|
||||
|
||||
def validate_paths():
|
||||
for value in paths:
|
||||
if private_bank_config(value):
|
||||
continue
|
||||
check_budget()
|
||||
try:
|
||||
with managed_open(root, value):
|
||||
@@ -480,7 +492,7 @@ def managed_manifest(root, payload, stopping):
|
||||
entries += 1
|
||||
if entries > MANIFEST_MAX_ENTRIES:
|
||||
raise AgentError("Manifest scan exceeded its entry limit")
|
||||
if child.name in ignored:
|
||||
if child.name in ignored or private_bank_config(relative + "/" + child.name):
|
||||
continue
|
||||
info = os.stat(child.name, dir_fd=directory, follow_symlinks=False)
|
||||
if stat_module.S_ISLNK(info.st_mode):
|
||||
@@ -1949,6 +1961,8 @@ class Agent:
|
||||
files = payload.get("files", [])
|
||||
if not isinstance(files, list):
|
||||
raise Rejected("Invalid initial file list")
|
||||
for item in files:
|
||||
require_public_file(item["path"])
|
||||
bank = payload.get("bank")
|
||||
if bank is None:
|
||||
return files
|
||||
@@ -2219,6 +2233,7 @@ class Agent:
|
||||
paths = set()
|
||||
total_size = 0
|
||||
for item in files:
|
||||
require_public_file(item["path"])
|
||||
scoped(directory / "app", item["path"])
|
||||
if item["path"] in paths:
|
||||
raise AgentError("Duplicate staged file path")
|
||||
@@ -2267,6 +2282,8 @@ class Agent:
|
||||
stage = directory / "stages" / identifier(stage_id)
|
||||
marker = stage / "manifest.json"
|
||||
saved = json.loads(marker.read_text())
|
||||
for item in saved["files"]:
|
||||
require_public_file(item["path"])
|
||||
if saved["state"] == "activated":
|
||||
return saved["before"]
|
||||
if saved["state"] != "staged":
|
||||
@@ -2297,6 +2314,8 @@ class Agent:
|
||||
return before
|
||||
|
||||
def restore_files(self, app, before):
|
||||
for item in before:
|
||||
require_public_file(item["path"])
|
||||
for item in before:
|
||||
managed_write(app, item["path"], None if item["contentBase64"] is None else self.decode_file(item), int(self.config.get("runtimeUid", 1000)), int(self.config.get("runtimeGid", 1000)))
|
||||
|
||||
@@ -2351,6 +2370,7 @@ class Agent:
|
||||
def read_log(self, instance_id, generation, payload):
|
||||
app = self.instance_dir(instance_id, generation) / "app"
|
||||
value = payload["path"]
|
||||
require_public_file(value)
|
||||
count = min(2000, max(1, int(payload.get("lines", 200))))
|
||||
# Validate the original relative spelling independently of symlinks.
|
||||
scoped(self.root / "agent" / "log-validation", value)
|
||||
@@ -2367,6 +2387,7 @@ class Agent:
|
||||
raise AgentError("Log symlink resolves outside the managed instance") from None
|
||||
# Reopen the resolved relative target through no-follow directory FDs;
|
||||
# swapping either the original link or a target component cannot escape.
|
||||
require_public_file(relative)
|
||||
with managed_open(app, relative) as stream:
|
||||
stat = os.fstat(stream.fileno())
|
||||
if payload.get("mode") == "incremental":
|
||||
|
||||
Reference in New Issue
Block a user