feat: operation-name normalization table with fail-closed enforcement (#106)
Promote the #103 minimal alias map to the documented public table GITEA_OPERATION_ALIASES and add the #106 enforcement layer: - normalize_operation(op, service): canonical namespaced names; legacy spellings accepted only via the explicit table; unknown, ambiguous, and cross-service names fail closed. - check_operation(op, allowed, forbidden, service): normalizes BOTH the requested operation and the profile lists before any membership check; forbidden always overrides allowed; unnormalizable allowed entries grant nothing and unnormalizable forbidden entries deny the request, so normalization can never silently widen permissions; empty/missing allowed list denies everything. - gitea_check_pr_eligibility now routes its capability check through check_operation, fixing the mismatch where canonical namespaced profile ops (gitea.pr.merge) never matched the raw action (merge) and namespaced forbidden entries were never enforced. - Document the normalization table and enforcement rules in docs/gitea-execution-profiles.md, replacing the stale 'enforcement out of scope' caveat. - tests/test_op_normalization.py: full #106 matrix (27 tests) — qualified/legacy allowed and forbidden, unknown, ambiguous, service mismatch, forbidden-overrides-allowed, empty/missing allowed, duplicates after normalization, no permission widening, and eligibility integration proving normalization happens before enforcement. Existing v1/env unqualified behaviour stays compatible. Closes #106 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,8 +134,50 @@ Rules:
|
||||
appears in both, it is forbidden.
|
||||
- An operation not present in `allowed_operations` is treated as **not
|
||||
allowed** (deny by default).
|
||||
- These categories are descriptive for this issue. Their runtime enforcement is
|
||||
out of scope here (see roadmap links).
|
||||
|
||||
## Operation-name normalization (#106)
|
||||
|
||||
Canonical operation names are namespaced: `{service}.{area}.{verb}` (e.g.
|
||||
`gitea.pr.merge`, `jenkins.build.read`). Legacy unqualified spellings are
|
||||
accepted **only** through the explicit alias table below (the code of record
|
||||
is `GITEA_OPERATION_ALIASES` in `gitea_config.py`; the enforcement matrix is
|
||||
`tests/test_op_normalization.py`).
|
||||
|
||||
| Legacy spelling | Canonical operation |
|
||||
|-------------------|----------------------------|
|
||||
| `read` | `gitea.read` |
|
||||
| `review` | `gitea.pr.review` |
|
||||
| `comment` | `gitea.pr.comment` |
|
||||
| `approve` | `gitea.pr.approve` |
|
||||
| `request_changes` | `gitea.pr.request_changes` |
|
||||
| `merge` | `gitea.pr.merge` |
|
||||
| `pr.create` | `gitea.pr.create` |
|
||||
| `branch.push` | `gitea.branch.push` |
|
||||
| `branch` | `gitea.branch.create` |
|
||||
| `commit` | `gitea.repo.commit` |
|
||||
| `push` | `gitea.branch.push` |
|
||||
| `open_pr` | `gitea.pr.create` |
|
||||
|
||||
For non-Gitea services, a single unqualified word namespaces to the checked
|
||||
service (`read` → `jenkins.read` when checking Jenkins); names already
|
||||
prefixed with that service pass through unchanged.
|
||||
|
||||
Enforcement rules (`gitea_config.check_operation`, run **before** any
|
||||
allowed/forbidden membership check):
|
||||
|
||||
- Unknown operation names fail closed (denied).
|
||||
- Ambiguous names — dotted names that are neither service-prefixed nor in the
|
||||
alias table — fail closed.
|
||||
- Cross-service names are never accepted by the wrong service
|
||||
(`jenkins.read` never matches a Gitea check, and a Gitea alias is never
|
||||
applied to another service).
|
||||
- `forbidden_operations` overrides `allowed_operations` after both sides are
|
||||
normalized, so a legacy spelling can never bypass a canonical forbidden
|
||||
entry (or vice versa).
|
||||
- An allowed entry that cannot be normalized grants nothing; a forbidden
|
||||
entry that cannot be normalized denies the request. Normalization can
|
||||
therefore never silently widen permissions.
|
||||
- An empty or missing `allowed_operations` list denies everything.
|
||||
|
||||
## Identity and fail-closed rules
|
||||
|
||||
|
||||
Reference in New Issue
Block a user