The independent review reproduced a real hole: the production first-bind path never pinned repository/org, so the drift checks it feeds were skipped. Root cause ---------- gitea_whoami, gitea_get_runtime_context, gitea_resolve_task_capability and gitea_activate_profile all seeded the session context without repository or org. First-bind-wins then made the mutation gate's later seed a no-op, and assess_session_context only compares repository/org when the bound fields are non-null. Result, reproduced in production order with the real REMOTES: after whoami: repository=None org=None same-host repo=Other-Tools -> NOT BLOCKED same-host org=Other-Org -> NOT BLOCKED cross-host -> blocked (this part always worked) Binding REMOTES defaults would not fix it: REMOTES['prgs'].repo is 'Timesheet' (a default *target*, not an authorization scope, see #530), so pinning it fails closed on every legitimate Gitea-Tools mutation. There was no trusted source for repository scope, so this adds one. Design ------ - New optional profile field `allowed_repositories`: canonical owner/repository slugs. An authorization boundary, never the binding itself. Config-only — no env var can widen or forge it. - The session repository is derived from the verified, workspace-aligned git remote, never from caller-supplied org/repo, and validated against that allowlist. The session binds immutably to exactly one canonical slug; org is derived from it. A profile authorizing several repositories still binds to the single verified one. - Every first-bind entry point now pins the same complete context. Activation rejects an unauthorized/unverifiable workspace. Mutations fail closed when repository/org is unverified, and a tool-level org/repo override that disagrees with the binding is rejected before the write. - A mutation request can no longer establish, complete, or replace the binding (it previously seeded from `org or REMOTES[...]`, i.e. caller-controlled). - Enforcement is opt-in per profile: a profile without the field keeps prior behaviour, so existing static-profile namespaces stay functional. First-bind-wins, immutability, the RLock, profile isolation, host/identity validation and the private pytest-only reset are unchanged. gitea_auth.get_profile() built an explicit whitelist dict and silently dropped `allowed_repositories`; the new integration tests caught that the scope was never enforced through the real path. Tests ----- New tests/test_issue_714_production_first_bind.py drives the real entry points in production order rather than constructing _SessionContext directly. Covers complete first bind via each entry point, same-host repo/org drift, Timesheet and other-repo/other-org rejection, unverified and unauthorized workspaces, caller values unable to establish the binding, concurrent init selecting one repository, and the PR #715 author path staying functional. Mutation-verified: disabling trusted derivation, override validation, the scope check, or the get_profile passthrough each fails these tests (9/8/4/5 failures). No security assertion was weakened, removed, skipped, or rewritten. Focused: 26 passed. Issue #714 total: 46 passed. Prior failing modules: 240 passed. Config/profile/remote modules: 130 passed. Full suite: 2739 passed, 6 skipped, 1 pre-existing warning, 161 subtests in 25.80s. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
94 lines
3.3 KiB
JSON
94 lines
3.3 KiB
JSON
{
|
|
"version": 2,
|
|
"contexts": {
|
|
"example-context": {
|
|
"enabled": true,
|
|
"label": "Example environment",
|
|
"description": "One deployment environment: its Gitea plus non-Gitea services.",
|
|
"default_owner": "Example-Org",
|
|
"gitea": {
|
|
"enabled": true,
|
|
"kind": "gitea",
|
|
"base_url": "https://gitea.example.invalid"
|
|
},
|
|
"services": {
|
|
"jenkins": {
|
|
"enabled": true,
|
|
"kind": "jenkins",
|
|
"label": "Example Jenkins",
|
|
"base_url": "https://jenkins.example.invalid",
|
|
"auth": { "type": "keychain", "id": "example-jenkins-token" },
|
|
"capabilities": ["read"]
|
|
},
|
|
"glitchtip": {
|
|
"enabled": false,
|
|
"kind": "glitchtip",
|
|
"label": "Example GlitchTip (disabled: defined but unavailable)",
|
|
"base_url": "",
|
|
"auth": { "type": "keychain", "id": "example-glitchtip-token" },
|
|
"capabilities": ["read"],
|
|
"allow_raw_events": false
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"profiles": {
|
|
"example-author": {
|
|
"enabled": true,
|
|
"context": "example-context",
|
|
"role": "author",
|
|
"username": "author-user",
|
|
"execution_profile": "example-author",
|
|
"audit_label": "example-author",
|
|
"auth": { "type": "keychain", "id": "example-gitea-author-token" },
|
|
"allowed_repositories": ["Example-Org/Example-Repo"],
|
|
"allowed_operations": ["read", "branch", "commit", "push", "open_pr", "comment", "issue.comment"],
|
|
"forbidden_operations": ["approve", "request_changes", "merge"]
|
|
},
|
|
"example-reviewer": {
|
|
"enabled": true,
|
|
"context": "example-context",
|
|
"role": "reviewer",
|
|
"username": "reviewer-user",
|
|
"execution_profile": "example-reviewer",
|
|
"audit_label": "example-reviewer",
|
|
"auth": { "type": "keychain", "id": "example-gitea-reviewer-token" },
|
|
"allowed_operations": ["read", "review", "comment", "issue.comment", "approve", "request_changes"],
|
|
"forbidden_operations": ["branch", "commit", "push", "open_pr", "merge"]
|
|
},
|
|
"example-merger": {
|
|
"enabled": true,
|
|
"context": "example-context",
|
|
"role": "merger",
|
|
"username": "reviewer-user",
|
|
"execution_profile": "example-merger",
|
|
"audit_label": "example-merger",
|
|
"auth": { "type": "keychain", "id": "example-gitea-reviewer-token" },
|
|
"allowed_operations": ["read", "comment", "issue.comment", "merge"],
|
|
"forbidden_operations": ["branch", "commit", "push", "open_pr", "approve", "review", "request_changes"]
|
|
}
|
|
},
|
|
"projects": {
|
|
"/absolute/path/to/local/repo": {
|
|
"enabled": true,
|
|
"context": "example-context",
|
|
"default_owner": "Example-Org",
|
|
"default_repo": "Example-Repo",
|
|
"default_author_profile": "example-author",
|
|
"default_reviewer_profile": "example-reviewer",
|
|
"default_merger_profile": "example-merger"
|
|
}
|
|
},
|
|
"rules": {
|
|
"disabled_behavior": "Defined but unavailable for action. MCP tools may report disabled entries during audits, but must not use them automatically.",
|
|
"no_silent_fallback": true,
|
|
"tokens_in_json": false,
|
|
"token_storage": "keychain",
|
|
"identity_must_match_task": true,
|
|
"same_username_cannot_review_own_pr": true,
|
|
"hide_service_urls_from_llm": true,
|
|
"hide_keychain_ids_from_llm": true,
|
|
"mcp_resolves_endpoints": true
|
|
}
|
|
}
|