99# Feature Specification: FEAT-012 - Authorization
1010
1111** Feature ID** : FEAT-012
12- ** Status** : Draft
12+ ** Status** : Active — V1 shipped; V2 scaffolded (not wired); V3 deferred
1313** Priority** : P1
1414** Owner** : Core Team
1515** Created** : 2026-04-05
16- ** Updated** : 2026-04-05
16+ ** Updated** : 2026-04-12
17+
18+ ## Implementation Status
19+
20+ | Phase | Capability | Status |
21+ | -------| -----------| --------|
22+ | V1 | Global RBAC (admin/write/read/none) + ` --no-auth ` + ` --guest-role ` | ** Shipped** |
23+ | V2 | ` MaskPolicy ` / ` WritePolicy ` structs and ` GrantRegistry ` defined | ** Scaffolded** — types built and tested in ` axon-core ` , not yet wired into handlers |
24+ | V3 | Per-entity attribute conditions, policy inheritance, policy UI | ** Deferred** |
25+
26+ Key V1 implementation choices that differ from this spec:
27+ - ** No OIDC support** — Tailscale is the only external provider; OIDC deferred to V2+
28+ - ** No ` tailscale-localapi ` crate** — identity is resolved via direct HTTP/1.1 over the
29+ Tailscale Unix socket using ` hyper ` + ` tokio::net::UnixStream `
30+ - ** ` --guest-role ` mode added** — unauthenticated requests receive a fixed role (not in
31+ original spec, added during implementation for edge deployments without Tailscale)
32+ - ** Actor = node name, not email** — ` Identity.actor ` is set to the Tailscale
33+ ` ComputedName ` (preferred) or ` Hostinfo.Hostname ` , not the user's email
1734
1835## Overview
1936
2037Axon requires an authentication and authorization layer to control who can
21- access data and what operations they can perform. The auth model is built
22- on ** OIDC (OpenID Connect) ** for identity, with ** Tailscale** as the
23- default (and first) identity provider via its LocalAPI whois mechanism .
38+ access data and what operations they can perform. The V1 auth model uses
39+ ** Tailscale LocalAPI whois ** for identity, with Tailscale ACL tags mapping
40+ to built-in RBAC roles .
2441
2542The design separates identity (who you are) from authorization (what you
26- can do), allowing other OIDC providers (Auth0, Okta, Google, etc.) to be
27- added without changing the authorization model.
43+ can do). Future phases may add OIDC providers, but V1 is Tailscale-only.
2844
2945## Problem Statement
3046
@@ -65,13 +81,21 @@ Audit log entries should carry real actor identities, not "anonymous".
6581 | ` read ` | Read entities, query, traverse, browse audit log |
6682 | ` none ` | No access (explicitly denied) |
6783
68- - ** Role assignment** : Roles are derived from the identity provider:
69- - ** Tailscale** : Mapped from ACL tags (` tag:axon-admin ` → admin,
70- ` tag:axon-write ` → write, ` tag:axon-read ` → read)
71- - ** Generic OIDC** : Mapped from JWT claims (configurable claim name,
72- e.g., ` axon_role ` in the ID token)
73- - ** Default role** : Configurable. Default is ` read ` for authenticated
74- users with no explicit role assignment
84+ - ** Role assignment** : Roles are derived from Tailscale ACL tags. When a
85+ node carries multiple role-granting tags the highest-privilege role wins:
86+
87+ | Tag | Role |
88+ | -----| ------|
89+ | ` tag:axon-admin ` / ` tag:admin ` | ` admin ` |
90+ | ` tag:axon-write ` / ` tag:axon-agent ` / ` tag:write ` | ` write ` |
91+ | ` tag:axon-read ` / ` tag:read ` | ` read ` |
92+ | * (no matching tag)* | ` --tailscale-default-role ` (default ` read ` ) |
93+
94+ ` tag:axon-agent ` is the conventional tag for automated agent workloads
95+ that need read/write but not admin access.
96+
97+ - ** Default role** : Configurable via ` --tailscale-default-role ` (default ` read ` )
98+ for authenticated nodes that carry no recognized ACL tag
7599
76100#### Attribute-Based Access Control (ABAC)
77101
@@ -161,11 +185,11 @@ attributes of the **user**, the **resource** (entity/collection), and the
161185
162186##### Implementation Phases
163187
164- | Phase | Capability |
165- | -------| -----------|
166- | V1 | Global RBAC roles (admin/write/read/none) + --no-auth mode |
167- | V2 | Per-collection policies, field masking, field immutability |
168- | V3 | Per-entity attribute conditions, policy inheritance, policy UI |
188+ | Phase | Capability | Status |
189+ | -------| -----------| -------- |
190+ | V1 | Global RBAC roles (admin/write/read/none) + ` --no-auth ` + ` --guest-role ` | ** Shipped ** |
191+ | V2 | Per-collection policies, field masking, field immutability | Scaffolded (structs + tests, not wired) |
192+ | V3 | Per-entity attribute conditions, policy inheritance, policy UI | Deferred |
169193
170194#### Network-Layer Security (Tailscale-Specific)
171195
@@ -195,10 +219,10 @@ attributes of the **user**, the **resource** (entity/collection), and the
195219** So that** my operations are attributed to me in the audit log
196220
197221** Acceptance Criteria:**
198- - [ ] Agent connects via Tailscale IP; Axon resolves its identity via whois
199- - [ ] Audit entries show the agent's Tailscale node name as actor
200- - [ ] Connections from outside the tailnet are rejected with 401
201- - [ ] Agent with no recognized Tailscale tags receives the configured ` default_role ` (default: ` read ` )
222+ - [x ] Agent connects via Tailscale IP; Axon resolves its identity via whois
223+ - [x ] Audit entries show the agent's Tailscale node name as actor
224+ - [x ] Connections from outside the tailnet are rejected with 401
225+ - [x ] Agent with no recognized Tailscale tags receives the configured ` default_role ` (default: ` read ` )
202226
203227### Story US-044: Role-Based Access Control [ FEAT-012]
204228
@@ -207,11 +231,11 @@ attributes of the **user**, the **resource** (entity/collection), and the
207231** So that** agents can't accidentally drop collections or modify schemas
208232
209233** Acceptance Criteria:**
210- - [ ] An agent with ` tag:axon-write ` can create/update/delete entities
211- - [ ] An agent with ` tag:axon-write ` cannot drop collections or change schemas
212- - [ ] An agent with ` tag:axon-read ` gets 403 on any write operation
213- - [ ] An admin with ` tag:axon-admin ` can perform all operations
214- - [ ] When a node has multiple role-granting tags, the highest-privilege role wins (admin > write > read)
234+ - [x ] An agent with ` tag:axon-write ` can create/update/delete entities
235+ - [x ] An agent with ` tag:axon-write ` cannot drop collections or change schemas
236+ - [x ] An agent with ` tag:axon-read ` gets 403 on any write operation
237+ - [x ] An admin with ` tag:axon-admin ` can perform all operations
238+ - [x ] When a node has multiple role-granting tags, the highest-privilege role wins (admin > write > read)
215239
216240### Story US-045: Development Without Auth [ FEAT-012]
217241
@@ -220,9 +244,9 @@ attributes of the **user**, the **resource** (entity/collection), and the
220244** So that** I don't need a Tailscale connection during development
221245
222246** Acceptance Criteria:**
223- - [ ] ` axon-server --no-auth ` starts without requiring tailscaled
224- - [ ] All requests succeed as admin in no-auth mode
225- - [ ] Audit entries show actor as ` "anonymous" ` in no-auth mode
247+ - [x ] ` axon-server --no-auth ` starts without requiring tailscaled
248+ - [x ] All requests succeed as admin in no-auth mode
249+ - [x ] Audit entries show actor as ` "anonymous" ` in no-auth mode
226250
227251## Technical Design
228252
@@ -233,75 +257,90 @@ Request arrives
233257 │
234258 ▼
235259┌─────────────────┐
236- │ --no-auth set? │──yes──▶ Identity = { actor: "anonymous", role: admin }
260+ │ --no-auth set? │──yes──▶ Identity = { actor: "anonymous", role: Admin }
237261└────────┬────────┘
238262 │ no
239263 ▼
240264┌─────────────────┐
241- │ Extract peer IP │
242- │ from connection │
265+ │ --guest-role? │──yes──▶ Identity = { actor: "guest", role: <configured> }
243266└────────┬────────┘
267+ │ no (Tailscale mode)
244268 ▼
245269┌─────────────────┐
246- │ Check cache │──hit──▶ Use cached identity
247- │ (IP → Identity) │
270+ │ Extract peer IP │
271+ │ from connection │
248272└────────┬────────┘
249- │ miss
250273 ▼
251274┌─────────────────┐
252- │ Call provider │
253- │ (Tailscale whois │
254- │ or OIDC verify) │
275+ │ Check IP cache │──hit──▶ Use cached identity (TTL: 60 s default)
276+ │ (RwLock map) │
255277└────────┬────────┘
278+ │ miss
256279 ▼
280+ ┌──────────────────────────────────┐
281+ │ GET /localapi/v0/whois?addr=peer │
282+ │ via Unix socket (tailscaled.sock) │
283+ └────────┬─────────────────────────┘
284+ │ 200 OK └─ 422 → 401 Unauthorized
285+ ▼ └─ socket error → 503 Unavailable
257286┌─────────────────┐
258- │ Map to Role │
259- │ (tags → role) │
287+ │ Map ACL tags │
288+ │ to Role │
260289└────────┬────────┘
261290 ▼
262291┌─────────────────┐
263292│ Cache + inject │
264- │ into request ctx │
293+ │ Identity into │
294+ │ request context │
265295└─────────────────┘
266296```
267297
268298### Middleware Architecture
269299
300+ HTTP auth runs as an axum middleware layer (` authenticate_http_request ` in
301+ ` crates/axon-server/src/gateway.rs ` ). gRPC uses ` AxonServiceImpl::authorize `
302+ in ` crates/axon-server/src/service.rs ` . Both delegate to ` AuthContext::resolve_peer ` .
303+
270304``` rust
271- // axum middleware (HTTP)
272- async fn auth_middleware (
273- ConnectInfo (addr ): ConnectInfo <SocketAddr >,
274- State (auth ): State <AuthProvider >,
305+ // Actual implementation (axon-server/src/gateway.rs)
306+ pub (crate ) async fn authenticate_http_request (
307+ State (auth ): State <AuthContext >,
275308 mut request : Request ,
276309 next : Next ,
277310) -> Response {
278- match auth . resolve_identity ( addr ). await {
311+ match auth . resolve_peer ( request_peer_address ( & request ) ). await {
279312 Ok (identity ) => {
280313 request . extensions_mut (). insert (identity );
281314 next . run (request ). await
282315 }
283- Err (AuthError :: Unauthorized ) => StatusCode :: UNAUTHORIZED . into_response (),
284- Err (AuthError :: Forbidden ) => StatusCode :: FORBIDDEN . into_response (),
285- Err (AuthError :: Unavailable ) => StatusCode :: SERVICE_UNAVAILABLE . into_response (),
316+ Err (error ) => auth_error_response (error ),
286317 }
287318}
319+ ```
320+
321+ Route handlers extract identity with ` Extension<Identity> ` and enforce
322+ role requirements:
288323
289- // Same pattern for tonic interceptor (gRPC)
324+ ``` rust
325+ async fn my_handler (Extension (identity ): Extension <Identity >, ... ) {
326+ identity . require_write ()? ; // or require_read() / require_admin()
327+ // ... handler logic
328+ }
290329```
291330
292- ### Provider Trait
331+ ### Auth Types
293332
294333``` rust
295- #[async_trait]
296- trait IdentityProvider : Send + Sync {
297- async fn resolve (& self , peer_addr : SocketAddr ) -> Result <Identity , AuthError >;
334+ // crates/axon-server/src/auth.rs
335+ pub struct Identity {
336+ pub actor : String , // Tailscale ComputedName, "anonymous", or "guest"
337+ pub role : Role , // Admin | Write | Read
298338}
299339
300- struct Identity {
301- actor : String , // email, node name, or "anonymous"
302- role : Role , // admin, write, read, none
303- provider : String , // "tailscale", "oidc", "none"
304- raw_claims : Value , // provider-specific metadata
340+ pub enum AuthMode {
341+ NoAuth ,
342+ Tailscale { default_role : Role },
343+ Guest { role : Role },
305344}
306345```
307346
@@ -351,8 +390,9 @@ role_claim = "axon_role"
351390
352391- ** FEAT-005** (API Surface): Auth middleware wraps HTTP and gRPC endpoints
353392- ** ADR-005** : Architecture decision for Tailscale LocalAPI whois
354- - ` tailscale-localapi ` crate (v0.5.0) for whois calls
355- - ` jsonwebtoken ` crate for generic OIDC JWT validation (Phase 2)
393+ - ` hyper ` + ` tokio::net::UnixStream ` — direct HTTP/1.1 over the Tailscale Unix socket
394+ (` tailscale-localapi ` crate was evaluated but not used; direct socket calls chosen)
395+ - ` jsonwebtoken ` — deferred (OIDC not implemented in V1)
356396
357397### Story US-046: Field-Level Masking [ FEAT-012]
358398
@@ -391,9 +431,14 @@ role_claim = "axon_role"
391431
392432### Related Artifacts
393433- ** Parent PRD Section** : Requirements Overview > P1 #6 (Authentication/authorization)
394- - ** User Stories** : US-043, US-044, US-045
434+ - ** User Stories** : US-043, US-044, US-045 (shipped); US-046, US-047 (scaffolded)
395435- ** Architecture** : ADR-005 (Tailscale LocalAPI whois)
396- - ** Implementation** : ` crates/axon-server/src/auth/ ` (planned)
436+ - ** Implementation** :
437+ - ` crates/axon-server/src/auth.rs ` — ` AuthMode ` , ` AuthContext ` , ` Identity ` , ` LocalApiWhoisProvider `
438+ - ` crates/axon-core/src/auth.rs ` — ` Role ` , ` CallerIdentity ` , ` MaskPolicy ` , ` WritePolicy ` , ` GrantRegistry `
439+ - ` crates/axon-server/src/gateway.rs ` — ` authenticate_http_request ` middleware, ` /auth/me ` endpoint
440+ - ` crates/axon-server/src/service.rs ` — gRPC ` authorize ` + ` auth_to_status `
441+ - ` crates/axon-server/src/serve.rs ` — ` auth_context_from_serve_args ` , CLI flags
397442
398443### Feature Dependencies
399444- ** Depends On** : FEAT-005
0 commit comments