Skip to content

feat(auth): enforce purpose-bound route authorization - #34

Draft
seonghobae wants to merge 55 commits into
developfrom
feat/purpose-bound-authorization
Draft

feat(auth): enforce purpose-bound route authorization#34
seonghobae wants to merge 55 commits into
developfrom
feat/purpose-bound-authorization

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Current exact-head state — 2026-09-03

  • live protected integration branch: develop@239008c4edc7d305c97704c5102b593c6622b36f;
  • recorded PR base remains predecessor develop@81a2920b0ae9b054fcdcc95902341e3a5bc809c5 and therefore requires a later non-force reconciliation/restack after the dependency root integrates;
  • exact branch head: 9cdafb76c0a727943692a1217872618d9251314b;
  • open / Draft / mergeable / not merge-ready: reconciliation dependency root fix: allow conserved multi-match reconciliation approvals #29 must integrate first, then this branch must be reconciled with the canonical integrated reconciliation command vocabulary and documentation;
  • predecessor exact-head CI/security/SAST evidence remains non-transferable after the required base reconciliation;
  • resolved review threads are not approval.

Purpose-bound authorization boundary

This branch adds a host-neutral AuthenticatedPrincipal, request-scoped trusted-principal resolution, versioned route-to-operation authorization, and tenant-scoped append-only authorization-decision evidence. The trusted host identity adapter remains deployment-owned and must validate signature, issuer, audience, expiry and token binding before AIS receives opaque principal claims. Tenant identity remains distinct from operation authority.

Authorization policy accounting-authorization-v3 reserves separate high-impact operations complete_reconciliation / accounting.complete_reconciliation and resolve_reconciliation_exception / accounting.resolve_reconciliation_exception. Completion, exception resolution, posting, read, soft/hard-close, tax, outbox, bank-ingest and bank-account-management permissions are non-equivalent; agent/model contexts remain denied high-impact authority by default. This branch reserves policy only and does not consume mutable #47 implementation bytes.

Caller-derived identity vocabulary remains bounded before durable audit storage. Correlation evidence intentionally follows the database's 512-character contract rather than an unrelated UTF-8 byte heuristic; the retained Korean multibyte regression proves an identity over 512 bytes but within 512 characters remains valid.

ADR numbering was reconciled so the purpose-bound authorization decision is ADR 0064. RED 4ef1f4226ea3d432048215fa2fe11c976cda4b05 requires the CWL purpose-bound authorization contract traceability row to cite ADR 0064 rather than retired ADR 0055; successor 9cdafb76c0a727943692a1217872618d9251314b repairs that documentation contract without changing runtime authorization semantics.

Merge boundary

Keep Draft while #29 remains an unmet prerequisite. First integrate #47 -> #43 -> #29 through ordinary protection, then non-destructively restack/reconcile authorization against the exact protected base and canonical docs, and require one unchanged exact head to pass repository/organization CI, real PostgreSQL authorization behavior, exact 100% owned production statement/branch/docstring/edge-case gates, SAST/security/dependency checks, current-head reviews, stack correctness and every applicable protection rule.

Do not merge, auto-merge, bypass protection, force-push, destructively rebase, version, tag or release from this branch while any prerequisite or gate is non-passing.

github-code-quality[bot]

This comment was marked as resolved.

@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

HTTP 회계 경로가 요청마다 검증된 주체를 확인하도록 변경되었습니다. complete_reconciliation 권한과 정책 버전이 추가되었습니다. 인가 결정은 테넌트 격리 append-only PostgreSQL 증거로 저장됩니다. 마이그레이션, 테스트, 운영 문서가 갱신되었습니다.

Changes

목적 기반 인가

Layer / File(s) Summary
인가 계약과 결정 판정
src/accounting_information_platform/authorization.py, src/accounting_information_platform/__init__.py, tests/test_reconciliation_completion_authorization_contract.py
정책 버전을 accounting-authorization-v2로 변경합니다. complete_reconciliationaccounting.complete_reconciliation에 매핑합니다. agent 주체의 고영향 작업 수행을 거부합니다. 인가 관련 계약을 패키지에서 공개합니다.
인가 증거 저장소
database/migrations/0015_authorization_decision_evidence.sql, src/accounting_information_platform/persistence.py, scripts/validate_repository.py, tests/test_database_migration_contracts.py, tests/test_foundation_install_manifest_contract.py
authorization_decision_record 테이블과 인덱스를 추가합니다. forced RLS, 테넌트 격리, append-only 트리거, PUBLIC 권한 철회를 적용합니다. migration 0015를 설치 순서와 필수 파일 검증에 연결합니다.
HTTP 경로 인가 흐름
src/accounting_information_platform/http_api.py
고정 principal 대신 request_principal_resolver를 사용합니다. 각 요청의 주체를 확인한 뒤 경로와 본문을 operation code로 매핑하고 인가합니다. resolver 예외는 503으로 처리합니다. 인가 실패는 403으로 처리합니다.
인가 및 설치 계약 검증
tests/test_authorization.py, tests/test_request_scoped_authorization_context.py, tests/test_postgres_posting.py
주체 유형, 권한 매핑, 요청별 주체 해석, fail-closed 응답, provenance, 테넌트 참조, malformed period-close 처리, 상관 참조 길이, PostgreSQL 증거 저장을 검증합니다.
인가 운영 문서와 추적성
CHANGELOG.md, README.md, docs/ARCHITECTURE.md, docs/OPERABILITY.md, docs/SECURITY.md, docs/adr/0055-purpose-bound-authorization.md, docs/doctoring/*, docs/product-technical-gap-baseline.md
요청별 identity adapter, principal_kind, migration 0015, 인가 증거, reconciliation 완료 권한, 운영 조건, 연구 근거와 통합 상태를 문서화합니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 5a985

The change adds purpose-bound authorization and durable authorization decisions, but it is not currently merge-ready because required checks are not passing, dependency integration and branch reconciliation remain outstanding, and a localized resolver type mismatch still needs correction.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant JournalProposalServer
  participant request_principal_resolver
  participant authorize
  participant record_authorization_decision
  Client->>JournalProposalServer: 회계 HTTP 요청
  JournalProposalServer->>request_principal_resolver: 현재 요청의 검증된 주체 조회
  request_principal_resolver-->>JournalProposalServer: AuthenticatedPrincipal 반환
  JournalProposalServer->>authorize: 경로 operation과 테넌트로 인가
  authorize-->>JournalProposalServer: AuthorizationDecision 반환
  JournalProposalServer->>record_authorization_decision: 결정과 correlation_reference 기록
  JournalProposalServer-->>Client: 200, 403 또는 503 응답
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 91.53% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 59 functions across 10 files. (9 skipped: 8…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 회계 HTTP 라우트에 목적 기반 인가를 적용하는 PR의 핵심 변경을 정확하고 간결하게 설명합니다.
Full details: Docstring Coverage

Explanation

Docstring coverage is 91.53% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 59 functions across 10 files. (9 skipped: 8 unsupported, 1 too large.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/purpose-bound-authorization

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

@OpenCode review

@seonghobae

Copy link
Copy Markdown
Contributor Author

@opencode-agent

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Current-head validation: a778e3b6972e0b9e90933e1d0fd1a83f2c9b7f4b. TDD regression rejects authorization evidence when requested_tenant_reference differs from the persistence tenant scope; the full PostgreSQL 18 suite passed (481 tests), repository contracts, compileall, changed-file Ruff, and diff-check passed. Current hosted checks are still pending; merge state is blocked, no qualifying approval is present, and no bypass will be used.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Devin informational note confirmed on exact head a778e3b: the persistence guard compares requested tenant to storage scope while retaining principal tenant as separate evidence. A denied cross-tenant request remains recorded under the requested tenant without granting authority; no additional source change is needed.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Current-head authorization repair on c656ab9: malformed or non-object /period-closes bodies no longer classify as hard_close_period, so no allowed hard-close decision is recorded before structural request rejection. Added RED/green coverage and documented the boundary in ADR 0055 and CHANGELOG. Local successor validation: 481 tests passed; 5,118/5,118 statements and 1,840/1,840 branches; repository contracts, compileall, changed-file Ruff, and diff-check passed. Hosted checks are now expected to rerun on this exact head; predecessor evidence is not transferred.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Current-head validation for 4868dbae5a1772fab84074416df3cf38b5f4da18:

  • Added RED/GREEN coverage for caller-constructed AuthorizationDecision(allowed=True) and rejected it at record_authorization_decision; only decisions issued by authorize reach durable evidence.
  • Updated CHANGELOG, ADR 0055, SECURITY, and standards traceability.
  • Local PostgreSQL 18.6: 482 tests passed; 5,126/5,126 statements and 1,842/1,842 branches; repository validator, compileall, changed-file Ruff, and diff-check passed.
  • This push invalidates predecessor evidence. Current PR state is open, non-draft, mergeable but blocked; no qualifying approval is present. Current-head hosted checks are still running, and the dependency-review check is already failing closed on its external GitHub dependency-graph path. No bypass will be used.

devin-ai-integration[bot]

This comment was marked as resolved.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Current-head validation for 010c896363c528e790052b2463b4c937d29e0019:

  • Addressed current Devin findings: evaluator decisions now retain provenance across copying, and post-issuance mutation is detected by a value fingerprint before persistence.
  • Local PostgreSQL 18.6: 484 tests passed; 5,136/5,136 statements and 1,842/1,842 branches; repository validator, compileall, changed-file Ruff, and diff-check passed.
  • Current PR remains open, non-draft, mergeable but blocked. No qualifying independent approval exists. All predecessor evidence is stale after this push; hosted checks for this exact SHA are pending. No bypass will be used.

@seonghobae

Copy link
Copy Markdown
Contributor Author

@OpenCode review

devin-ai-integration[bot]

This comment was marked as resolved.

@opencode-agent

opencode-agent Bot commented Sep 1, 2026

Copy link
Copy Markdown

Scheduled review-feedback autofix for this PR head.

  • Head SHA: dc1cb7b9dd1d03f07233f78f07cdf1beb11fceaf

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Authorization policy-owner follow-up on the current branch: RED f0dc0288b51870ffe5d59737bd971eb384b7dc46 reserves an independent reviewed-exception operation/permission (resolve_reconciliation_exceptionaccounting.resolve_reconciliation_exception) and proves completion/posting/close/read grants are non-equivalent plus agent-origin denial. GREEN 764e5d0c60c6ea518a25d33a4605d1273d051231 registers that operation as high-impact and advances immutable audit policy identity to accounting-authorization-v3; c7f558e1c2f7503a88794534085c2a93883bca42 advances the existing completion contract to the same policy vocabulary. ADR 0055 was updated at b9fbab41a90e4a6b1961c5cc920eb13156c35a94, and doctoring 2026-09-02-reconciliation-exception-resolution-authorization-policy.md was added at exact head 71db01962f60503a19a1856d0a7aa50c5205e179. This reserves policy only: it creates no transport, consumes no #47 branch bytes, and grants no database/journal/close authority. GitHub currently reports no workflow run attached to this new exact head, so predecessor checks are non-transferable and this Ready sibling remains not merge-ready behind the reconciliation dependency root/canonical docs restack.

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 new potential issues.

Devin Review

Comment on lines +118 to +128
principal_reference: str
tenant_reference: str
requested_tenant_reference: str
authentication_context_reference: str
credential_evidence_reference: str
operation_code: str
permission_code: str
purpose_code: str
policy_version: str
decision_code: str
allowed: bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Audit records omit caller classification

Every authorization decision drops principal_kind before persistence. Audit evidence cannot distinguish human, service, and agent callers or verify agent restrictions.

Prompt for agents
Add principal_kind to AuthorizationDecision and its fingerprinted values, populate it for authenticated and unauthenticated decisions, persist it in database/migrations/0015_authorization_decision_evidence.sql with the same closed vocabulary, and extend unit and PostgreSQL tests to verify it survives into immutable audit evidence.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +98 to +105
for value, label in (
(self.principal_reference, "principal reference"),
(self.tenant_reference, "tenant reference"),
(self.authentication_context_reference, "authentication context reference"),
(self.credential_evidence_reference, "credential evidence reference"),
):
_require_reference(value, label)
_require_code(self.purpose_code, "purpose code")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Long identity references disable authorized routes

A valid CWL reference above 255 bytes passes AuthenticatedPrincipal but fails evidence storage. Every affected request returns 503 instead of its route response.

Prompt for agents
Enforce the migration's 255-octet CWL-reference profile in AuthenticatedPrincipal before authorization. Apply the same bound to principal_reference, tenant_reference, authentication_context_reference, and credential_evidence_reference, and validate the configured/requested tenant reference before evidence persistence. Add boundary tests for ASCII and multibyte references so application acceptance exactly matches migration 0015.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread CHANGELOG.md
authorization evaluator; caller-constructed or post-issuance-mutated `allowed` decisions
cannot be promoted into durable audit evidence, while copied evaluator decisions retain
their provenance.
- Malformed or non-object period-close request bodies no longer create an allowed hard-close authorization record before the request is rejected; authorization evidence is reserved for a structurally valid close command.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Malformed-close documentation contradicts runtime

The changelog says malformed close bodies never create allowed evidence. _post_authorization_operation classifies them as hard closes, records an allow, then returns 400.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

USING (tenant_account_id = accounting_core.current_tenant_account_id())
WITH CHECK (tenant_account_id = accounting_core.current_tenant_account_id());

REVOKE ALL ON accounting_integration.authorization_decision_record FROM PUBLIC;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Runtime insert grant needs deployment verification

The migration revokes public access without granting INSERT. Production authorization returns 503 for every routed request unless deployment provisions the runtime role explicitly.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +176 to +183
tenant_matches = principal.tenant_reference == requested_tenant_reference
agent_restricted = principal.principal_kind == "agent" and operation_code in _HIGH_IMPACT_OPERATIONS
allowed = (
bool(permission_code)
and tenant_matches
and not agent_restricted
and permission_code in principal.granted_permission_codes
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Purpose claims do not restrict access

authorize records purpose_code without evaluating it. A permission issued for one purpose authorizes the same sensitive operation under any other purpose.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@cwl-noema-review cwl-noema-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noema LLM review

The PR implements purpose-bound route authorization, replacing server-wide principal inheritance with a request-scoped resolver and establishing an immutable, tenant-isolated audit trail. It successfully resolves multiple critical security findings from prior threads: (1) closing the authorization bypass for malformed period-close requests, (2) enforcing explicit principal kinds to prevent privilege escalation, (3) preventing 503 errors for multibyte correlation keys by using character-length bounds in PostgreSQL, and (4) ensuring audit provenance by rejecting mutated authorization decisions. The database schema is correctly bounded to prevent storage inflation and uses RLS for tenant isolation. Documentation across ADR 0064, SECURITY.md, and OPERABILITY.md is comprehensively updated to reflect these new security boundaries.

Reviewed changed lines

  • database/migrations/0015_authorization_decision_evidence.sql:13 (RIGHT): Correctly implements octet-length bounds (255) and URN regex for principal references to prevent unbounded storage and ensure normalized identity format.
  • database/migrations/0015_authorization_decision_evidence.sql:56 (RIGHT): Correctly uses char_length(correlation_reference) <= 512, aligning with the HTTP boundary contract and preventing 503 errors for multibyte UTF-8 keys.
  • database/migrations/0015_authorization_decision_evidence.sql:75 (RIGHT): Implements an append-only trigger to ensure authorization evidence is immutable, satisfying audit requirements.
  • docs/adr/0064-purpose-bound-authorization.md:35 (RIGHT): Explicitly defines the fail-closed behavior for malformed /period-closes bodies, ensuring they are classified as hard_close_period for authorization purposes.
  • docs/SECURITY.md:31 (RIGHT): Documents the requirement for a request-scoped resolver and the rejection of omitted principal_kind, closing the gap where a server-wide principal could be inherited.

Adversarial validation

  • database/migrations/0015_authorization_decision_evidence.sql:56 (RIGHT) falsified: A multibyte UTF-8 correlation key that is < 512 characters but > 512 bytes will cause a 503 error during evidence persistence. — The migration uses char_length(correlation_reference) <= 512 rather than octet_length, which counts characters regardless of byte size.
  • docs/adr/0064-purpose-bound-authorization.md:35 (RIGHT) falsified: A malformed JSON body in a POST /period-closes request can bypass the authorization check and reach the handler. — ADR 0064 and the associated doctoring records confirm that malformed bodies are conservatively classified as hard_close_period for the authorization step, ensuring a 403 is returned if the user lacks that permission.
  • Residual risk: Low. The system relies on the 'trusted host identity adapter' to provide the AuthenticatedPrincipal; if the adapter itself is compromised, the application-level authorization is bypassed. This is an external boundary defined in the architecture.

Findings

  • No blocking findings.
  • Result: APPROVE
  • Head SHA: 9cdafb76c0a727943692a1217872618d9251314b
  • Reviewer credential: noema-review-github-app-refresh
  • Actor: cwl-noema-review[bot]

@seonghobae
seonghobae marked this pull request as draft September 3, 2026 04:15
@opencode-agent

opencode-agent Bot commented Sep 3, 2026

Copy link
Copy Markdown

Scheduled review-feedback autofix for this PR head.

  • Head SHA: 9cdafb76c0a727943692a1217872618d9251314b

@opencode-agent

opencode-agent Bot commented Sep 4, 2026

Copy link
Copy Markdown

Scheduled review-feedback autofix for this PR head.

  • Head SHA: 9cdafb76c0a727943692a1217872618d9251314b

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant