feat: operation-name normalization table with fail-closed enforcement (#106) #122

Open
jcwalker3 wants to merge 1 commits from feat/issue-106-op-normalization into master
Owner

Closes #106

What

Defines the canonical operation-name normalization table for profiles.json v2 and enforces it before every allowed/forbidden check.

Normalization (gitea_config.py)

  • GITEA_OPERATION_ALIASES — the #103 minimal map promoted to the documented public table (contents unchanged; no new permissions introduced).
  • normalize_operation(op, service="gitea") — canonical namespaced names (gitea.pr.merge, jenkins.read). Legacy unqualified spellings accepted only via the explicit table. Unknown, ambiguous (dotted, non-prefixed, not in table), and cross-service names raise ConfigError (fail closed). Gitea aliases are never applied to other services.
  • check_operation(op, allowed, forbidden, service) — normalizes both the requested operation and the profile lists before any membership check. forbidden always overrides allowed. An unnormalizable allowed entry grants nothing; an unnormalizable forbidden entry denies the request — normalization can never silently widen permissions. Empty/missing allowed list denies everything.
  • _normalize_op (v2 identity loading) now wraps the public function, adding identity context to errors; load-time behavior unchanged.

Enforcement fix (mcp_server.py)

gitea_check_pr_eligibility previously compared the raw action ("merge") against normalized profile lists (["gitea.pr.merge"]): canonical allowed ops never matched, and canonical forbidden entries were never enforced at this gate. The capability check now routes through check_operation; all existing reason strings preserved.

Docs

docs/gitea-execution-profiles.md gains the normalization table and enforcement rules, replacing the stale "runtime enforcement is out of scope" caveat.

Tests

452 passed, 6 skipped (27 new in tests/test_op_normalization.py). Full #106 matrix: fully-qualified allowed/forbidden, legacy unqualified allowed/forbidden, unknown op, ambiguous op, service mismatch, forbidden-overrides-allowed (across mixed spellings), empty allowed, missing allowed, duplicates after normalization, junk-entry no-widening, plus eligibility integration tests. TDD red verified: the two eligibility integration tests fail against the pre-change mcp_server.py and pass after. Existing v1/env unqualified behaviour stays compatible (regression test included).

Non-goals honored

No service tools implemented, no release/tag state changed, no permissions broadened (alias table contents identical to #103).

🤖 Generated with Claude Code

Closes #106 ## What Defines the canonical operation-name normalization table for `profiles.json` v2 and enforces it before every allowed/forbidden check. ### Normalization (`gitea_config.py`) - `GITEA_OPERATION_ALIASES` — the #103 minimal map promoted to the documented public table (contents unchanged; no new permissions introduced). - `normalize_operation(op, service="gitea")` — canonical namespaced names (`gitea.pr.merge`, `jenkins.read`). Legacy unqualified spellings accepted **only** via the explicit table. Unknown, ambiguous (dotted, non-prefixed, not in table), and cross-service names raise `ConfigError` (fail closed). Gitea aliases are never applied to other services. - `check_operation(op, allowed, forbidden, service)` — normalizes **both** the requested operation and the profile lists before any membership check. `forbidden` always overrides `allowed`. An unnormalizable allowed entry grants nothing; an unnormalizable forbidden entry denies the request — normalization can never silently widen permissions. Empty/missing allowed list denies everything. - `_normalize_op` (v2 identity loading) now wraps the public function, adding identity context to errors; load-time behavior unchanged. ### Enforcement fix (`mcp_server.py`) `gitea_check_pr_eligibility` previously compared the raw action (`"merge"`) against normalized profile lists (`["gitea.pr.merge"]`): canonical allowed ops never matched, and canonical **forbidden** entries were never enforced at this gate. The capability check now routes through `check_operation`; all existing reason strings preserved. ### Docs `docs/gitea-execution-profiles.md` gains the normalization table and enforcement rules, replacing the stale "runtime enforcement is out of scope" caveat. ## Tests 452 passed, 6 skipped (27 new in `tests/test_op_normalization.py`). Full #106 matrix: fully-qualified allowed/forbidden, legacy unqualified allowed/forbidden, unknown op, ambiguous op, service mismatch, forbidden-overrides-allowed (across mixed spellings), empty allowed, missing allowed, duplicates after normalization, junk-entry no-widening, plus eligibility integration tests. TDD red verified: the two eligibility integration tests fail against the pre-change `mcp_server.py` and pass after. Existing v1/env unqualified behaviour stays compatible (regression test included). ## Non-goals honored No service tools implemented, no release/tag state changed, no permissions broadened (alias table contents identical to #103). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jcwalker3 added 1 commit 2026-07-03 02:35:42 -05:00
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>
sysadmin reviewed 2026-07-03 03:14:51 -05:00
sysadmin left a comment
Owner

Approved. Reviewed operation-name normalization for #106, including canonical namespaced operations, fail-closed unknown/ambiguous/cross-service behavior, forbidden-overrides-allowed enforcement, junk-entry handling, empty-allowed denial, docs, and tests. Validation passed: eligibility gate from pinned PR head, pytest tests/ -q, git diff --check, py_compile, and secret/provenance sweep.

Approved. Reviewed operation-name normalization for #106, including canonical namespaced operations, fail-closed unknown/ambiguous/cross-service behavior, forbidden-overrides-allowed enforcement, junk-entry handling, empty-allowed denial, docs, and tests. Validation passed: eligibility gate from pinned PR head, pytest tests/ -q, git diff --check, py_compile, and secret/provenance sweep.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/issue-106-op-normalization:feat/issue-106-op-normalization
git checkout feat/issue-106-op-normalization
Sign in to join this conversation.