fix: harden profiles migration helper
This commit is contained in:
+88
-28
@@ -20,6 +20,43 @@ if PROJECT_ROOT not in sys.path:
|
||||
import gitea_config
|
||||
|
||||
|
||||
AUTHOR_DEFAULT_ALLOWED = ["read", "branch", "commit", "push", "open_pr", "comment"]
|
||||
AUTHOR_DEFAULT_FORBIDDEN = ["approve", "request_changes", "merge"]
|
||||
REVIEWER_DEFAULT_ALLOWED = [
|
||||
"read", "review", "comment", "approve", "request_changes", "merge"
|
||||
]
|
||||
REVIEWER_DEFAULT_FORBIDDEN = ["branch", "commit", "push", "open_pr"]
|
||||
|
||||
|
||||
def infer_role(name, execution_profile):
|
||||
"""Return the unambiguous role for a legacy profile name, or None."""
|
||||
haystack = f"{name} {execution_profile or ''}".lower()
|
||||
has_author = "author" in haystack
|
||||
has_reviewer = "reviewer" in haystack
|
||||
if has_author == has_reviewer:
|
||||
return None
|
||||
return "reviewer" if has_reviewer else "author"
|
||||
|
||||
|
||||
def migration_summary(v2_data):
|
||||
"""Return a redacted summary of the migrated config."""
|
||||
environments = v2_data.get("environments", {})
|
||||
service_count = 0
|
||||
identity_count = 0
|
||||
for env in environments.values():
|
||||
services = env.get("services", {})
|
||||
service_count += len(services)
|
||||
for service in services.values():
|
||||
identity_count += len(service.get("identities", {}))
|
||||
return {
|
||||
"version": v2_data.get("version"),
|
||||
"environments": len(environments),
|
||||
"services": service_count,
|
||||
"identities": identity_count,
|
||||
"aliases": len(v2_data.get("aliases", {})),
|
||||
}
|
||||
|
||||
|
||||
def migrate_v1_to_v2(v1_data):
|
||||
"""Convert version 1 profiles.json format to version 2 environments format."""
|
||||
environments = {}
|
||||
@@ -42,43 +79,62 @@ def migrate_v1_to_v2(v1_data):
|
||||
env_name = name
|
||||
ident_name = "author"
|
||||
|
||||
# Determine role and identity based on name / execution_profile
|
||||
role = "author"
|
||||
# Determine role and identity based on name / execution_profile.
|
||||
# Ambiguous profiles may still migrate only when they carry explicit
|
||||
# permissions; otherwise role-based defaults could widen permissions.
|
||||
exec_prof = prof.get("execution_profile") or ""
|
||||
if "reviewer" in name or "reviewer" in exec_prof:
|
||||
role = "reviewer"
|
||||
role = infer_role(name, exec_prof)
|
||||
if role == "reviewer":
|
||||
ident_name = "reviewer"
|
||||
elif "author" in name or "author" in exec_prof:
|
||||
role = "author"
|
||||
elif role == "author":
|
||||
ident_name = "author"
|
||||
else:
|
||||
role = prof.get("role")
|
||||
if role not in (None, "author", "reviewer"):
|
||||
raise ValueError(
|
||||
f"Profile '{name}' has unsupported role {role!r}"
|
||||
)
|
||||
|
||||
# Construct identity block
|
||||
identity_data = {
|
||||
"role": role,
|
||||
"username": prof.get("username"),
|
||||
"auth": prof.get("auth"),
|
||||
}
|
||||
if role:
|
||||
identity_data["role"] = role
|
||||
if prof.get("execution_profile"):
|
||||
identity_data["execution_profile"] = prof["execution_profile"]
|
||||
|
||||
|
||||
# Set audit label (default to old name to preserve context)
|
||||
identity_data["audit_label"] = prof.get("audit_label") or name
|
||||
|
||||
# Populate capabilities based on role
|
||||
if role == "author":
|
||||
identity_data["allowed_operations"] = [
|
||||
"read", "branch", "commit", "push", "open_pr", "comment"
|
||||
]
|
||||
identity_data["forbidden_operations"] = [
|
||||
"approve", "request_changes", "merge"
|
||||
]
|
||||
has_allowed = "allowed_operations" in prof
|
||||
has_forbidden = "forbidden_operations" in prof
|
||||
if has_allowed != has_forbidden:
|
||||
raise ValueError(
|
||||
f"Profile '{name}' must define both allowed_operations and "
|
||||
"forbidden_operations, or neither (fail closed)"
|
||||
)
|
||||
if has_allowed:
|
||||
allowed = prof.get("allowed_operations")
|
||||
forbidden = prof.get("forbidden_operations")
|
||||
if not isinstance(allowed, list) or not isinstance(forbidden, list):
|
||||
raise ValueError(
|
||||
f"Profile '{name}' operation fields must be lists"
|
||||
)
|
||||
identity_data["allowed_operations"] = list(allowed)
|
||||
identity_data["forbidden_operations"] = list(forbidden)
|
||||
elif role == "author":
|
||||
identity_data["allowed_operations"] = list(AUTHOR_DEFAULT_ALLOWED)
|
||||
identity_data["forbidden_operations"] = list(AUTHOR_DEFAULT_FORBIDDEN)
|
||||
elif role == "reviewer":
|
||||
identity_data["allowed_operations"] = list(REVIEWER_DEFAULT_ALLOWED)
|
||||
identity_data["forbidden_operations"] = list(REVIEWER_DEFAULT_FORBIDDEN)
|
||||
else:
|
||||
identity_data["allowed_operations"] = [
|
||||
"read", "review", "comment", "approve", "request_changes", "merge"
|
||||
]
|
||||
identity_data["forbidden_operations"] = [
|
||||
"branch", "commit", "push", "open_pr"
|
||||
]
|
||||
raise ValueError(
|
||||
f"Profile '{name}' has no explicit operation lists and no "
|
||||
"unambiguous author/reviewer role marker (fail closed)"
|
||||
)
|
||||
|
||||
# Nest inside environments/services structure
|
||||
env = environments.setdefault(env_name, {})
|
||||
@@ -100,7 +156,7 @@ def migrate_v1_to_v2(v1_data):
|
||||
alias_target = f"{env_name}.gitea.{ident_name}"
|
||||
if name != alias_target:
|
||||
aliases[name] = alias_target
|
||||
|
||||
|
||||
# Extra convenience alias for standard old-profile compatibility (e.g. prgs-author)
|
||||
convenience_alias = f"{env_name}-{ident_name}"
|
||||
if convenience_alias != alias_target and convenience_alias not in aliases:
|
||||
@@ -191,11 +247,15 @@ def main():
|
||||
|
||||
if not args.write:
|
||||
print("=== DRY-RUN MODE (No files modified) ===")
|
||||
print(f"Would read from: {input_path}")
|
||||
print(f"Would create backup at: {backup_path}")
|
||||
print(f"Would write v2 config to: {output_path}")
|
||||
print("\nGenerated v2 config:")
|
||||
print(json.dumps(v2_data, indent=2))
|
||||
print("Generated v2 config validated successfully.")
|
||||
print("Only aggregate counts are shown.")
|
||||
summary = migration_summary(v2_data)
|
||||
print("Summary:")
|
||||
print(f" version: {summary['version']}")
|
||||
print(f" environments: {summary['environments']}")
|
||||
print(f" services: {summary['services']}")
|
||||
print(f" identities: {summary['identities']}")
|
||||
print(f" aliases: {summary['aliases']}")
|
||||
sys.exit(0)
|
||||
|
||||
# Write Mode: Create Backup first
|
||||
|
||||
Reference in New Issue
Block a user