P1: identity, roles, RBAC - #1
Merged
Merged
Conversation
Adds the auth layer the later phases build on. Models & migration - users (email, full_name, password_hash, role, team, is_active) and refresh_tokens (jti, expires_at, revoked_at); Alembic migration bd693f8a6bb0 with a PG-appropriate enum create/drop and a tested downgrade. Security - app/security.py: Argon2 password hashing (+ a dummy hash so unknown-email login timing matches), HS256 JWT encode/decode with a checked `type` claim and required exp/sub/type claims. - app/config.py: refuses to boot with the placeholder JWT secret (or one under 32 chars) unless ENVIRONMENT is dev/test/local. Auth flows (app/services/auth.py, app/routers/auth.py) - POST /auth/login, GET /auth/me - POST /auth/refresh: rotating refresh tokens with row locking; replay of a rotated token is detected and burns the whole family, committed in its own unit of work so it survives the 401. - POST /auth/logout: authenticated; only revokes a token the caller owns. - Credential/refresh failures all return one generic 401. RBAC - Role/Team enums; require_roles(...) dependency in app/deps.py (re-exports Role). get_current_user guards a malformed `sub` and checks is_active. - app/errors.py: AppError hierarchy -> JSON, wired in create_app(). Tests - tests/test_auth.py (login, /me, rotation + reuse, logout, per-role probe matrix via test-only /_probe/* routes), tests/test_config.py, conftest fixtures (make_user factory, per-role users + auth_* headers). 35 tests, ruff + mypy --strict + pytest green (SQLite locally / CI Postgres). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rkflow - README: mermaid architecture diagram, backend layering + domain-rule highlights, RBAC matrix, a phase-by-phase roadmap progress table, and a section on the guard-railed Claude Code workflow (hooks / subagents / skills / loop) with the real P1 security-reviewer catch as evidence. - STATUS / ai-workflow: record the P1 security-review findings and fixes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
P1 — Identity, roles, RBAC
The auth layer every later phase builds on. Closes the P1 checklist in
docs/ROADMAP.md.Models & migration
users(email,full_name,password_hash,role,team,is_active) andrefresh_tokens(jti,expires_at,revoked_at).bd693f8a6bb0— creates/drops theroleandteamPostgres enums explicitly, testeddowngrade(). Round-trips clean on SQLite; CI runs it on Postgres.Security
app/security.py— Argon2 password hashing (with a constant dummy hash so an unknown-email login takes the same time as a real one), HS256 JWT encode/decode. Tokens carry atypeclaim (access/refresh) that is checked, plus requiredexp/sub/type.app/config.py— the API refuses to boot with the placeholderJWT_SECRET(or anything under 32 chars) unlessENVIRONMENTisdev/test/local.Auth flows
POST /auth/login,GET /auth/me.POST /auth/refresh— rotating refresh tokens. The row is locked for the transaction; replay of an already-rotated token is detected and burns the whole token family, committed in its own unit of work so the revocation survives the 401 that follows.POST /auth/logout— authenticated; only revokes a refresh token the caller owns.401(no account-existence leak).RBAC
Role/Teamenums;require_roles(...)dependency inapp/deps.py(declared on the route, never in handler bodies).get_current_userguards a malformedsuband rejects inactive users.app/errors.py—AppErrorhierarchy rendered as{"detail": ...}JSON, wired increate_app().Tests — 35,
ruff+mypy --strict+pytestgreentests/test_auth.py— login ok/bad/inactive, expired + wrong-type tokens, rotation, reuse-family-burn (incl. a variant using real per-request sessions to prove the burn is committed), logout ownership, and a per-role probe matrix against test-only/_probe/*routes.tests/test_config.py— the strong-secret boot check.conftest.py—make_userfactory, per-role user +auth_*header fixtures.Notes
docker compose upverification — Docker is blocked on the dev machine) are marked as such indocs/ROADMAP.md; backend tests run on SQLite locally and the CI Postgres service.🤖 Generated with Claude Code