feat: implement authorization with Gate hierarchy and repo decorators#56
feat: implement authorization with Gate hierarchy and repo decorators#56
Conversation
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.
|
Greptile OverviewGreptile SummaryComprehensive role-based authorization system with two-layer enforcement: handler-level gates ( Key implementation details:
Confidence Score: 5/5
|
| 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
Summary
public(),at_least(Role)) enforced via CommandHandler/QueryHandler metaclasses@reads/@writesrepo decorators with composable checks (owner(),has_role(),AnyOf)missing_token→ 401,access_denied→ 403 in error handlerTest plan
just test unit)just lint— ruff + ty)Closes #29
Closes #55