Runnable REST API exercising every micro-lib tier in a containerless setup: N-layer architecture, SQLite persistence, header-based auth simulating Firebase output, and bit-mask RBAC enforcement. What's included: - cmd/todo-api: minimal main delegating to application.Run - internal/application: full object graph wiring — launcher, sqlite, httpserver, httpmw stack, routes in BeforeStart - internal/domain: User entity, ResourceTodos constant, PermReadTodo/PermWriteTodo bit positions - internal/repository: TodoRepository, UserRepository, DBPermissionProvider (SQLite via modernc) - internal/service: TodoService, UserService with interface-based dependencies - internal/handler: TodoHandler, UserHandler using httputil adapters and valid for input validation - internal/middleware: Auth (X-User-ID → rbac.Identity) and Require (bit-mask permission gate) - logAdapter: bridges logz.Logger.With return type to httpmw.Logger interface - SQLite schema: users, user_role (bitmask), todos; migrations run in BeforeStart - Routes: POST /users (open), GET+POST /todos (RBAC), GET /users (RBAC) Tested-via: todo-api POC integration Reviewed-against: docs/adr/
2.8 KiB
ADR-002: Header-Based Auth for POC
Status: Accepted Date: 2026-03-18
Context
The todo-api needs authentication so it can demonstrate the RBAC permission system. In production this role would be played by Firebase Auth: the client presents a Firebase ID token, the httpauth-firebase middleware (from httpmw) verifies the JWT, and an rbac.Identity is placed in the request context.
For this POC, introducing Firebase Auth would require:
- A Firebase project or emulator
- Token issuance in tests and curl examples
- JWT verification dependencies in the application
This overhead is not necessary to demonstrate the RBAC flow. The goal is to show the contract between auth middleware and the rest of the stack, not to validate the auth mechanism itself.
Decision
Authentication is performed by reading the X-User-ID request header. The middleware.Auth middleware:
- Reads
r.Header.Get("X-User-ID"). - Returns 401 if the header is absent.
- Calls
userRepo.FindByID(ctx, uid)to verify the user exists in the database. - Returns 401 if the user is not found (the two failure cases are intentionally indistinguishable to callers — no enumeration of which check failed).
- Constructs an
rbac.Identityviarbac.NewIdentity(user.ID, user.Name, user.Email)and stores it in the context withrbac.SetInContext.
The output contract — an rbac.Identity in the request context — is identical to what httpauth-firebase would produce. The downstream middleware.Require RBAC guard consumes rbac.Identity from context and has no knowledge of how the identity was established.
The POST /users route is deliberately unguarded. It allows bootstrapping: a new deployment can create its first users (with permission bits set) without prior credentials.
Consequences
- Replacing header auth with Firebase auth in a production version requires only swapping the
Authmiddleware. All downstream code (Require, handlers, services, repositories) is unchanged. - The header-based approach is intentionally insecure and must not be used in production. This is a POC.
- The 401 response does not distinguish "missing header" from "user not found" — this is intentional to avoid user enumeration.
POST /usersis an open endpoint. In a production system this would be protected by Firebase auth or an admin credential. Here it serves as the bootstrap mechanism to populate theuserstable and assign permission bits.- The
logAdapterinapplicationbridgeslogz.Logger.With(...)(which returnslogz.Logger) tohttpmw.Logger.With(...)(which must returnhttpmw.Logger). This mismatch arises becausehttpmw.Loggeris a duck-typed interface with aWithmethod that must returnhttpmw.Logger, not a concrete type. The adapter is defined locally inapplication— no extra package needed.