scopes.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """Upstream credential scope refuse helpers (§HGD.6.2).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | # Conceptual write-class scopes — refuse when advertised by host introspection. |
| 6 | REJECTED_SCOPE_TOKENS = frozenset( |
| 7 | { |
| 8 | "contents:write", |
| 9 | "administration", |
| 10 | "admin:org", |
| 11 | "admin:repo_hook", |
| 12 | "admin:org_hook", |
| 13 | "workflows", |
| 14 | "delete_repo", |
| 15 | "workflow", |
| 16 | "repo:status", # not write; keep focused on true write classes below |
| 17 | } |
| 18 | ) |
| 19 | |
| 20 | # Explicit write-class tokens used by tests and startup checks. |
| 21 | WRITE_SCOPE_TOKENS = frozenset( |
| 22 | { |
| 23 | "contents:write", |
| 24 | "administration", |
| 25 | "admin:org", |
| 26 | "admin:repo_hook", |
| 27 | "admin:org_hook", |
| 28 | "workflows", |
| 29 | "workflow", |
| 30 | "delete_repo", |
| 31 | "repo", # classic full-access PAT — refuse when introspected |
| 32 | } |
| 33 | ) |
| 34 | |
| 35 | # Narrow read-only conceptual scopes (informational). |
| 36 | READ_SCOPE_TOKENS = frozenset( |
| 37 | { |
| 38 | "contents:read", |
| 39 | "metadata:read", |
| 40 | "public_repo", # classic public read — handled carefully; prefer contents:read |
| 41 | } |
| 42 | ) |
| 43 | |
| 44 | |
| 45 | def scopes_contain_write_class(scopes: frozenset[str] | set[str] | list[str] | None) -> bool: |
| 46 | """Return True when any advertised scope is a rejected write class. |
| 47 | |
| 48 | When scopes cannot be introspected (``None``), returns False so the operator |
| 49 | runbook + tests must prove write HTTP verbs are unreachable. |
| 50 | """ |
| 51 | if scopes is None: |
| 52 | return False |
| 53 | normalized = {s.strip().lower() for s in scopes if isinstance(s, str) and s.strip()} |
| 54 | write_normalized = {s.lower() for s in WRITE_SCOPE_TOKENS} |
| 55 | # ``repo`` is classic full access — refuse when present among introspected scopes |
| 56 | # unless the only intersection is empty. |
| 57 | return bool(normalized & write_normalized) |
| 58 | |
| 59 | |
| 60 | def refuse_write_scopes(scopes: frozenset[str] | set[str] | list[str] | None) -> str | None: |
| 61 | """Return error token ``write_scope_refused`` when write scopes are present.""" |
| 62 | if scopes_contain_write_class(scopes): |
| 63 | return "write_scope_refused" |
| 64 | return None |