update doc and optimize

This commit is contained in:
thanhnv
2026-07-06 17:47:12 +09:00
parent ace442da0e
commit 4419cd9eae
95 changed files with 2951 additions and 1353 deletions
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""CASAN RBAC — role-based access control decision engine (Plan-14 core, harness-owned).
Reusable governance asset in the core harness (not in any generated app). The
Control Plane web app and any harness action call this to authorize a request.
Model: role × (resource:action) with scope. deny-by-default + fail-closed:
unknown role / action / cross-tenant / sensitive-without-org-admin ⇒ DENY.
Separation of Duties (SoD): a proposer cannot approve their own request.
Exit codes: 0 = ALLOW, 1 = DENY. Reason printed to stderr.
"""
import argparse
import sys
# scope: "org" (all projects) or "project" (must match the acted-on project)
PERMISSIONS = {
"org-admin": {"scope": "org", "allow": {"*"}},
"project-admin": {"scope": "project", "allow": {"settings:read", "settings:write", "monitoring:read", "audit:read"}},
"approver": {"scope": "project", "allow": {"settings:read", "monitoring:read", "approval:grant"}},
"operator": {"scope": "project", "allow": {"monitoring:read", "kill_switch:engage"}},
"viewer": {"scope": "project", "allow": {"settings:read", "monitoring:read"}},
"auditor": {"scope": "org", "allow": {"settings:read", "monitoring:read", "audit:read"}},
}
# Maps an IdP-issued claim (role name / group) to an RBAC role. The IdP (Plan-07
# C4) is the identity authority; RBAC only maps a *verified* claim to a role.
# deny-by-default: an unmapped claim yields no role.
CLAIM_ROLE_MAP = {
"casan-org-admin": "org-admin",
"casan-project-admin": "project-admin",
"casan-approver": "approver",
"casan-operator": "operator",
"casan-viewer": "viewer",
"casan-auditor": "auditor",
}
def decide(role, resource, action, role_project, target_project, sensitive):
perm = PERMISSIONS.get(role)
if perm is None:
return False, f"UNKNOWN_ROLE {role}"
action_key = f"{resource}:{action}"
# Sensitive settings writes are org-admin only, regardless of other grants.
if sensitive and not (resource == "settings" and action == "write"):
# sensitivity only meaningful for settings:write
pass
if sensitive and resource == "settings" and action == "write" and role != "org-admin":
return False, f"SENSITIVE_REQUIRES_ORG_ADMIN {action_key}"
if "*" in perm["allow"]:
return True, "ALLOW org-admin"
if action_key not in perm["allow"]:
return False, f"ACTION_NOT_ALLOWED {role} {action_key}"
if perm["scope"] == "project":
if not role_project or not target_project:
return False, "PROJECT_SCOPE_REQUIRED"
if role_project != target_project:
return False, f"CROSS_TENANT {role_project}!={target_project}"
return True, f"ALLOW {role} {action_key}"
def main() -> int:
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("check")
c.add_argument("--role", required=True)
c.add_argument("--resource", required=True)
c.add_argument("--action", required=True)
c.add_argument("--role-project", default="")
c.add_argument("--target-project", default="")
c.add_argument("--sensitive", action="store_true")
s = sub.add_parser("check-sod")
s.add_argument("--proposer", required=True)
s.add_argument("--approver", required=True)
p = sub.add_parser("list-roles")
m = sub.add_parser("map-claim")
m.add_argument("--claim", required=True, help="IdP-issued role/group claim")
args = ap.parse_args()
if args.cmd == "list-roles":
for role, perm in PERMISSIONS.items():
print(f"{role} scope={perm['scope']} allow={sorted(perm['allow'])}")
return 0
if args.cmd == "map-claim":
role = CLAIM_ROLE_MAP.get(args.claim)
if role is None:
print(f"RBAC_DENY UNKNOWN_CLAIM {args.claim}", file=sys.stderr)
return 1
print(role)
return 0
if args.cmd == "check-sod":
if args.proposer == args.approver:
print(f"RBAC_DENY SOD_SELF_APPROVAL actor={args.approver}", file=sys.stderr)
return 1
print(f"RBAC_ALLOW SOD_OK proposer={args.proposer} approver={args.approver}")
return 0
allowed, reason = decide(
args.role, args.resource, args.action, args.role_project, args.target_project, args.sensitive
)
if allowed:
print(f"RBAC_ALLOW {reason}")
return 0
print(f"RBAC_DENY {reason}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())