feat: harden control panel authentication
This commit is contained in:
@@ -204,29 +204,36 @@ def call_account_model(provider: str, prompt: str):
|
||||
def audit(job: dict, status: str) -> str:
|
||||
path = os.path.join(STATE_ROOT, "logs", "audit", "goal-orchestrator.jsonl")
|
||||
head_path = os.path.join(STATE_ROOT, "logs", "audit", "goal-orchestrator-head.txt")
|
||||
lock_path = os.path.join(STATE_ROOT, "logs", "audit", "goal-orchestrator.lock")
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
previous = "0" * 64
|
||||
try:
|
||||
with open(head_path, encoding="utf-8") as handle:
|
||||
previous = handle.read().strip() or previous
|
||||
except OSError:
|
||||
pass
|
||||
core = {
|
||||
"timestamp": now(),
|
||||
"harness": "H5-governance",
|
||||
"goal_id": job["id"],
|
||||
"tenant": job.get("tenant", "default"),
|
||||
"actor": job.get("actor", "unknown"),
|
||||
"goal_hash": sha(str(job.get("goal", ""))),
|
||||
"status": status,
|
||||
"local_provider": job.get("local_provider", ""),
|
||||
"cloud_provider": job.get("cloud_provider", ""),
|
||||
"prev_hash": previous,
|
||||
}
|
||||
record_hash = sha(json.dumps(core, sort_keys=True, ensure_ascii=False))
|
||||
append_jsonl(path, {**core, "record_hash": record_hash})
|
||||
with open(head_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(record_hash + "\n")
|
||||
with open(lock_path, "a", encoding="utf-8") as lock:
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
||||
previous = "0" * 64
|
||||
try:
|
||||
with open(head_path, encoding="utf-8") as handle:
|
||||
previous = handle.read().strip() or previous
|
||||
except OSError:
|
||||
pass
|
||||
core = {
|
||||
"timestamp": now(),
|
||||
"harness": "H5-governance",
|
||||
"goal_id": job["id"],
|
||||
"tenant": job.get("tenant", "default"),
|
||||
"actor": job.get("actor", "unknown"),
|
||||
"goal_hash": sha(str(job.get("goal", ""))),
|
||||
"status": status,
|
||||
"local_provider": job.get("local_provider", ""),
|
||||
"cloud_provider": job.get("cloud_provider", ""),
|
||||
"prev_hash": previous,
|
||||
}
|
||||
record_hash = sha(json.dumps(core, sort_keys=True, ensure_ascii=False))
|
||||
append_jsonl(path, {**core, "record_hash": record_hash})
|
||||
with open(head_path, "w", encoding="utf-8") as handle:
|
||||
handle.write(record_hash + "\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.chmod(head_path, 0o600)
|
||||
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
|
||||
return record_hash
|
||||
|
||||
|
||||
@@ -351,10 +358,43 @@ def run(job_path: str) -> int:
|
||||
return 2
|
||||
|
||||
|
||||
def verify_audit() -> int:
|
||||
path = os.path.join(STATE_ROOT, "logs", "audit", "goal-orchestrator.jsonl")
|
||||
previous = "0" * 64
|
||||
records = 0
|
||||
try:
|
||||
handle = open(path, encoding="utf-8")
|
||||
except OSError:
|
||||
print(json.dumps({"ok": True, "records": 0, "head": previous}))
|
||||
return 0
|
||||
with handle:
|
||||
for line in handle:
|
||||
if not line.strip():
|
||||
continue
|
||||
records += 1
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except ValueError:
|
||||
print(json.dumps({"ok": False, "records": records, "reason": "invalid_json"}))
|
||||
return 3
|
||||
record_hash = str(record.pop("record_hash", ""))
|
||||
expected = sha(json.dumps(record, sort_keys=True, ensure_ascii=False))
|
||||
if record.get("prev_hash") != previous or record_hash != expected:
|
||||
print(json.dumps({"ok": False, "records": records, "reason": "chain_break"}))
|
||||
return 3
|
||||
previous = record_hash
|
||||
print(json.dumps({"ok": True, "records": records, "head": previous}))
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--job-file", required=True)
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("--job-file")
|
||||
group.add_argument("--verify-audit", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.verify_audit:
|
||||
return verify_audit()
|
||||
path = os.path.abspath(args.job_file)
|
||||
state = os.path.abspath(STATE_ROOT) + os.sep
|
||||
if not path.startswith(state):
|
||||
|
||||
@@ -18,6 +18,7 @@ AUTH_BRIDGE_DIR="$ROOT/tmp/control-panel-local/auth-bridge"
|
||||
AUTH_BRIDGE_TOKEN_FILE="$AUTH_BRIDGE_DIR/token"
|
||||
AUTH_BRIDGE_PID_FILE="$AUTH_BRIDGE_DIR/bridge.pid"
|
||||
AUTH_BRIDGE_LOG="$AUTH_BRIDGE_DIR/bridge.log"
|
||||
AUTH_BRIDGE_AUDIT="$AUTH_BRIDGE_DIR/model-audit.jsonl"
|
||||
AUTH_BRIDGE="$ROOT/packages/casan-control-panel/scripts/provider-auth-bridge.py"
|
||||
CMD="${1:-status}"
|
||||
|
||||
@@ -43,9 +44,9 @@ start_auth_bridge() {
|
||||
return 0
|
||||
fi
|
||||
[[ -f "$AUTH_BRIDGE" ]] || { echo "CASAN_AUTH_BRIDGE_MISSING" >&2; return 1; }
|
||||
nohup python3 "$AUTH_BRIDGE" --bind 0.0.0.0 --port 20130 --token-file "$AUTH_BRIDGE_TOKEN_FILE" > "$AUTH_BRIDGE_LOG" 2>&1 &
|
||||
nohup python3 "$AUTH_BRIDGE" --bind 0.0.0.0 --port 20130 --token-file "$AUTH_BRIDGE_TOKEN_FILE" --audit-log "$AUTH_BRIDGE_AUDIT" > "$AUTH_BRIDGE_LOG" 2>&1 &
|
||||
echo "$!" > "$AUTH_BRIDGE_PID_FILE"
|
||||
chmod 600 "$AUTH_BRIDGE_PID_FILE" "$AUTH_BRIDGE_LOG" 2>/dev/null || true
|
||||
chmod 600 "$AUTH_BRIDGE_PID_FILE" "$AUTH_BRIDGE_LOG" "$AUTH_BRIDGE_AUDIT" 2>/dev/null || true
|
||||
wait_url "http://127.0.0.1:20130/healthz"
|
||||
}
|
||||
|
||||
@@ -56,6 +57,7 @@ stop_auth_bridge() {
|
||||
kill "$pid" 2>/dev/null || true
|
||||
rm -f "$AUTH_BRIDGE_PID_FILE"
|
||||
fi
|
||||
rm -f "$AUTH_BRIDGE_TOKEN_FILE"
|
||||
}
|
||||
|
||||
need_docker() {
|
||||
|
||||
Reference in New Issue
Block a user