Skip to content

feat: implement authorization with Gate hierarchy and repo decorators#56

Merged
rorybyrne merged 3 commits intomainfrom
029-feat-implement-authorization
Feb 7, 2026
Merged

feat: implement authorization with Gate hierarchy and repo decorators#56
rorybyrne merged 3 commits intomainfrom
029-feat-implement-authorization

Conversation

@rorybyrne
Copy link
Copy Markdown
Contributor

Summary

  • Implement role-based authorization with hierarchical Gate system (public(), at_least(Role)) enforced via CommandHandler/QueryHandler metaclasses
  • Add resource-level @reads/@writes repo decorators with composable checks (owner(), has_role(), AnyOf)
  • Add role management (assign/revoke) commands restricted to SUPERADMIN
  • Resolve Identity per-request from JWT + DB role lookup (Principal for authenticated, Anonymous for unauthenticated)
  • Map missing_token → 401, access_denied → 403 in error handler
  • 260 unit tests covering all authorization layers

Test plan

  • All 260 unit tests pass (just test unit)
  • Lint clean (just lint — ruff + ty)
  • Pre-commit hooks pass
  • QueryHandler gate tests mirror CommandHandler coverage
  • Error codes pinned for 401/403 HTTP mapping
  • AuthProvider identity resolution tested (valid/expired/invalid JWT, missing header)
  • Concrete handler configs verified (CreateDeposition, AssignRole, InitiateLogin)
  • Repo decorator edge cases covered (anonymous, role hierarchy, error codes)
  • Multi-role principal behavior verified

Closes #29
Closes #55

Replace the over-engineered PolicySet/Guarded/Action system with two
clean layers: handler-level Gate checks (__auth__ = public() or
at_least(Role.X)) and resource-level repo decorators (@reads/@writes).

- Add Gate base class with Public and AtLeast subclasses
- Add Identity hierarchy (Anonymous, System, Principal)
- Add @reads/@writes decorators for resource-level checks on repos
- Rename OAuth Identity entity to LinkedAccount (avoid name conflict)
- Workers inject System() identity via DI context (bypasses all checks)
- Delete Action enum, PolicySet, Guarded[T], Policy composables
Add 34 tests covering QueryHandler gates, error code pinning (401 vs 403),
AuthProvider identity resolution, concrete handler auth configs, repo
decorator edge cases, and multi-role principal behavior.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 7, 2026

Code Coverage

Package Line Rate Complexity Health
. 73% 0
application 0% 0
application.api 100% 0
application.api.rest 0% 0
application.api.v1 82% 0
application.api.v1.routes 0% 0
application.event 100% 0
cli 40% 0
cli.commands 18% 0
cli.util 53% 0
domain 100% 0
domain.auth 100% 0
domain.auth.command 97% 0
domain.auth.event 100% 0
domain.auth.model 91% 0
domain.auth.port 99% 0
domain.auth.query 93% 0
domain.auth.service 89% 0
domain.auth.util 100% 0
domain.auth.util.di 83% 0
domain.curation 100% 0
domain.curation.adapter 100% 0
domain.curation.command 100% 0
domain.curation.event 0% 0
domain.curation.handler 0% 0
domain.curation.model 100% 0
domain.curation.port 100% 0
domain.curation.query 100% 0
domain.curation.service 100% 0
domain.deposition 100% 0
domain.deposition.adapter 100% 0
domain.deposition.command 19% 0
domain.deposition.event 100% 0
domain.deposition.model 0% 0
domain.deposition.port 0% 0
domain.deposition.query 100% 0
domain.deposition.service 100% 0
domain.export 100% 0
domain.export.adapter 100% 0
domain.export.command 100% 0
domain.export.event 100% 0
domain.export.model 100% 0
domain.export.port 100% 0
domain.export.query 100% 0
domain.export.service 100% 0
domain.index 100% 0
domain.index.event 100% 0
domain.index.handler 76% 0
domain.index.model 84% 0
domain.index.service 100% 0
domain.record 100% 0
domain.record.adapter 100% 0
domain.record.command 100% 0
domain.record.event 100% 0
domain.record.handler 0% 0
domain.record.model 100% 0
domain.record.port 100% 0
domain.record.query 100% 0
domain.record.service 100% 0
domain.schema 100% 0
domain.schema.adapter 100% 0
domain.schema.command 100% 0
domain.schema.event 100% 0
domain.schema.model 100% 0
domain.schema.port 100% 0
domain.schema.query 100% 0
domain.schema.service 100% 0
domain.search 100% 0
domain.search.adapter 100% 0
domain.search.command 100% 0
domain.search.event 100% 0
domain.search.model 100% 0
domain.search.port 100% 0
domain.search.query 100% 0
domain.search.service 100% 0
domain.shared 90% 0
domain.shared.authorization 87% 0
domain.shared.model 90% 0
domain.shared.port 100% 0
domain.source 100% 0
domain.source.event 100% 0
domain.source.handler 0% 0
domain.source.model 76% 0
domain.source.schedule 0% 0
domain.source.service 92% 0
domain.validation 100% 0
domain.validation.adapter 100% 0
domain.validation.command 0% 0
domain.validation.event 0% 0
domain.validation.handler 0% 0
domain.validation.model 0% 0
domain.validation.port 0% 0
domain.validation.query 100% 0
domain.validation.service 0% 0
infrastructure 100% 0
infrastructure.auth 0% 0
infrastructure.event 66% 0
infrastructure.index 0% 0
infrastructure.index.vector 76% 0
infrastructure.messaging 100% 0
infrastructure.oci 0% 0
infrastructure.persistence 0% 0
infrastructure.persistence.adapter 0% 0
infrastructure.source 0% 0
sdk 100% 0
sdk.index 100% 0
sdk.source 100% 0
util 100% 0
util.di 25% 0
Summary 49% (2117 / 4303) 0

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 7, 2026

Greptile Overview

Greptile Summary

Comprehensive role-based authorization system with two-layer enforcement: handler-level gates (public(), at_least(Role)) enforced via metaclass wrapping, and resource-level decorators (@reads, @writes) with composable checks (owner(), has_role(), AnyOf). Identity resolution occurs per-request from JWT with DB role lookup, creating Principal with roles or Anonymous for unauthenticated requests. System identity allows workers to bypass checks. Error codes (missing_token, access_denied) map correctly to HTTP 401/403. Role management restricted to SUPERADMIN via AssignRoleHandler and RevokeRoleHandler. Startup validation ensures all handlers declare __auth__. Database migration adds role_assignments table and owner_id column to depositions.

Key implementation details:

  • Role hierarchy uses IntEnum with gaps for future expansion (PUBLIC=0, DEPOSITOR=10, CURATOR=20, ADMIN=30, SUPERADMIN=40)
  • Principal.has_role() implements hierarchy comparison (admin has curator privileges)
  • Metaclass wraps CommandHandler/QueryHandler run() methods at class creation time
  • Repository decorators execute before (writes) or after (reads) method execution
  • Workers use System identity injected via DI context
  • Test coverage: 260 unit tests across all authorization layers

Confidence Score: 5/5

  • Safe to merge - well-architected authorization system with comprehensive test coverage
  • Implementation demonstrates strong software engineering: clean separation of concerns (handler gates vs resource checks), fail-fast validation at startup, proper error code mapping, comprehensive test coverage (260 tests), and no security vulnerabilities identified
  • No files require special attention

Important Files Changed

Filename Overview
server/osa/domain/shared/authorization/gate.py Clean gate hierarchy implementation with public() and at_least(Role) factory functions
server/osa/domain/shared/authorization/resource.py Well-designed resource checks with composable owner(), has_role(), and AnyOf support
server/osa/domain/shared/authorization/decorators.py Clean @reads and @writes decorators with proper async handling and error propagation
server/osa/domain/shared/command.py Metaclass-based authorization wrapping for CommandHandler with gate validation
server/osa/domain/shared/query.py QueryHandler metaclass mirrors CommandHandler pattern for consistent authorization
server/osa/domain/auth/util/di/provider.py Identity resolution from JWT with DB role lookup per request
server/osa/application/api/v1/errors.py Error mapping with correct 401/403 distinction based on error codes
server/migrations/versions/add_authorization.py Migration adds role_assignments table and owner_id column with proper constraints

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as FastAPI Route
    participant Handler as CommandHandler/QueryHandler
    participant Metaclass as Handler Metaclass
    participant Gate as Gate Evaluator
    participant AuthProvider as AuthProvider (DI)
    participant RoleRepo as RoleAssignmentRepository
    participant Service as Domain Service
    participant Repo as Repository
    participant Decorator as @reads/@writes

    Client->>API: HTTP Request + JWT
    API->>AuthProvider: Extract Identity from JWT
    AuthProvider->>AuthProvider: Validate JWT token
    AuthProvider->>RoleRepo: get_by_user_id(user_id)
    RoleRepo-->>AuthProvider: [RoleAssignment...]
    AuthProvider-->>API: Identity (Anonymous/Principal/System)
    
    API->>Handler: Instantiate with Identity
    Handler->>Metaclass: run(command)
    Note over Metaclass: Metaclass wraps run() with auth
    
    Metaclass->>Gate: Evaluate __auth__ gate
    alt Public Gate
        Gate-->>Metaclass: Allow
    else AtLeast Gate
        Gate->>Gate: Check Principal.has_role(required_role)
        alt Has Required Role
            Gate-->>Metaclass: Allow
        else Insufficient Role
            Gate-->>Metaclass: AuthorizationError(access_denied, 403)
        end
    end
    
    Metaclass->>Handler: Execute run() logic
    Handler->>Service: Call domain service
    Service->>Repo: Repository operation
    
    Repo->>Decorator: @reads/@writes decorator
    Decorator->>Decorator: Evaluate ResourceCheck
    alt System Identity
        Decorator-->>Decorator: Bypass check
    else Anonymous
        Decorator-->>Decorator: AuthorizationError(missing_token, 401)
    else Principal
        Decorator->>Decorator: Check owner() or has_role()
        alt Authorized
            Decorator-->>Decorator: Allow
        else Not Authorized
            Decorator-->>Decorator: AuthorizationError(access_denied, 403)
        end
    end
    
    Decorator->>Repo: Execute repository method
    Repo-->>Service: Result
    Service-->>Handler: Result
    Handler-->>API: Result
    
    API->>API: Map OSAError to HTTPException
    Note over API: missing_token → 401<br/>access_denied → 403
    API-->>Client: HTTP Response
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@rorybyrne rorybyrne merged commit f076683 into main Feb 7, 2026
6 checks passed
@rorybyrne rorybyrne deleted the 029-feat-implement-authorization branch February 7, 2026 17:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: improve authorization test coverage feat: implement authorization with RBAC and relationship-based access

1 participant