Phase 06: Backend Authentication - 3 plan(s) in 2 wave(s) - Wave 1: 06-01 (login/logout foundation) - Wave 2: 06-02 (password reset), 06-03 (RBAC + 2FA) - parallel - Ready for execution
335 lines
13 KiB
Markdown
335 lines
13 KiB
Markdown
---
|
|
phase: 06-backend-authentication
|
|
plan: 03
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["06-01"]
|
|
files_modified:
|
|
- summercms/src/auth/Permission.scala
|
|
- summercms/src/auth/PermissionRegistry.scala
|
|
- summercms/src/auth/PermissionChecker.scala
|
|
- summercms/src/auth/TotpService.scala
|
|
- summercms/src/auth/middleware/PermissionMiddleware.scala
|
|
- summercms/src/auth/routes/TotpRoutes.scala
|
|
- summercms/src/auth/routes/RoleRoutes.scala
|
|
- summercms/src/auth/repository/RoleRepository.scala
|
|
- summercms/src/auth/LoginService.scala
|
|
- summercms/src/api/Routes.scala
|
|
- build.mill
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Roles can be created with specific permissions attached"
|
|
- "Controllers and views check permissions before rendering protected content"
|
|
- "Wildcard permissions work (e.g., 'blog.*' matches 'blog.posts.create')"
|
|
- "Super Admin role has '*' permission that matches everything"
|
|
- "2FA can be enabled optionally by admin users"
|
|
artifacts:
|
|
- path: "summercms/src/auth/PermissionRegistry.scala"
|
|
provides: "Plugin permission registration"
|
|
exports: ["PermissionRegistry", "register", "getAllPermissions"]
|
|
- path: "summercms/src/auth/PermissionChecker.scala"
|
|
provides: "Permission checking with wildcard support"
|
|
exports: ["PermissionChecker", "hasPermission", "hasAnyPermission"]
|
|
- path: "summercms/src/auth/middleware/PermissionMiddleware.scala"
|
|
provides: "Route-level permission enforcement"
|
|
exports: ["PermissionMiddleware", "require"]
|
|
- path: "summercms/src/auth/TotpService.scala"
|
|
provides: "TOTP 2FA setup and verification"
|
|
exports: ["TotpService", "generateSecret", "verifyCode"]
|
|
key_links:
|
|
- from: "PermissionMiddleware"
|
|
to: "PermissionChecker"
|
|
via: "permission validation"
|
|
pattern: "permissionChecker\\.hasPermission"
|
|
- from: "PermissionChecker"
|
|
to: "RoleRepository"
|
|
via: "role lookup"
|
|
pattern: "roleRepository\\.findById"
|
|
- from: "LoginService"
|
|
to: "TotpService"
|
|
via: "2FA verification"
|
|
pattern: "totpService\\.verifyCode"
|
|
---
|
|
|
|
<objective>
|
|
Implement role-based access control (RBAC) with plugin-registered permissions and optional TOTP 2FA.
|
|
|
|
Purpose: Enable fine-grained access control where plugins can register their own permissions, roles aggregate permissions, and middleware enforces access rules. Optional 2FA adds an extra security layer for admin accounts.
|
|
|
|
Output:
|
|
- Permission registry for plugin-registered permissions
|
|
- Permission checker with wildcard matching
|
|
- Permission middleware for route protection
|
|
- Role CRUD endpoints
|
|
- TOTP 2FA setup and verification
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/06-backend-authentication/06-CONTEXT.md
|
|
@.planning/phases/06-backend-authentication/06-RESEARCH.md
|
|
@.planning/phases/06-backend-authentication/06-01-SUMMARY.md
|
|
@.planning/phases/02-plugin-system/02-RESEARCH.md
|
|
|
|
# Dependencies from 06-01
|
|
@summercms/src/auth/Role.scala
|
|
@summercms/src/auth/AdminUser.scala
|
|
@summercms/src/auth/LoginService.scala
|
|
@summercms/src/auth/middleware/AuthMiddleware.scala
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create permission system</name>
|
|
<files>
|
|
summercms/src/auth/Permission.scala
|
|
summercms/src/auth/PermissionRegistry.scala
|
|
summercms/src/auth/PermissionChecker.scala
|
|
summercms/src/auth/repository/RoleRepository.scala
|
|
</files>
|
|
<action>
|
|
1. Create Permission.scala:
|
|
- case class PermissionDef(
|
|
code: String, // e.g., "golem15.blog.access_posts"
|
|
label: String, // Human-readable: "Access blog posts"
|
|
tab: String, // Grouping in UI: "Blog"
|
|
order: Int = 0
|
|
)
|
|
- companion object with helper methods
|
|
|
|
2. Create PermissionRegistry (ZIO service with Ref):
|
|
- trait PermissionRegistry:
|
|
def register(pluginId: String, permissions: List[PermissionDef]): UIO[Unit]
|
|
def getAllPermissions: UIO[List[PermissionDef]]
|
|
def getPermissionsForPlugin(pluginId: String): UIO[List[PermissionDef]]
|
|
def getPermissionsByTab: UIO[Map[String, List[PermissionDef]]]
|
|
- Implementation uses ZIO Ref[Map[String, List[PermissionDef]]]
|
|
- Register some core permissions on initialization:
|
|
- "backend.access" - "Access admin backend"
|
|
- "backend.manage_users" - "Manage admin users"
|
|
- "backend.manage_roles" - "Manage roles"
|
|
|
|
3. Create RoleRepository (following existing patterns):
|
|
- findById(id: Long): IO[RepositoryError, Option[Role]]
|
|
- findByCode(code: String): IO[RepositoryError, Option[Role]]
|
|
- findAll: IO[RepositoryError, List[Role]]
|
|
- create(role: Role): IO[RepositoryError, Role]
|
|
- update(role: Role): IO[RepositoryError, Role]
|
|
- delete(id: Long): IO[RepositoryError, Unit]
|
|
Use Quill with querySchema mapping.
|
|
|
|
4. Create PermissionChecker (ZIO service):
|
|
- trait PermissionChecker:
|
|
def hasPermission(userId: Long, permission: String): Task[Boolean]
|
|
def hasAnyPermission(userId: Long, permissions: List[String]): Task[Boolean]
|
|
def hasAllPermissions(userId: Long, permissions: List[String]): Task[Boolean]
|
|
|
|
- Implementation:
|
|
- Get user's role from AdminUserRepository
|
|
- Get role's permissions
|
|
- Match using wildcard logic:
|
|
- "*" matches everything (Super Admin)
|
|
- "blog.*" matches "blog.posts.create", "blog.categories.edit", etc.
|
|
- Exact match: "blog.posts.create" matches only "blog.posts.create"
|
|
|
|
- Wildcard matching algorithm:
|
|
```scala
|
|
def matchesPermission(granted: String, required: String): Boolean =
|
|
if granted == "*" then true
|
|
else if granted.endsWith(".*") then
|
|
required.startsWith(granted.dropRight(1)) // "blog.*" -> "blog."
|
|
else
|
|
granted == required
|
|
```
|
|
</action>
|
|
<verify>
|
|
- ./mill summercms.compile succeeds
|
|
- PermissionChecker.hasPermission returns true for user with matching permission
|
|
- Wildcard "*" grants all permissions
|
|
- Wildcard "blog.*" grants "blog.posts.create" but not "users.list"
|
|
</verify>
|
|
<done>
|
|
Permission registry stores plugin permissions, permission checker validates with wildcard support, role repository provides CRUD
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create permission middleware and role management routes</name>
|
|
<files>
|
|
summercms/src/auth/middleware/PermissionMiddleware.scala
|
|
summercms/src/auth/routes/RoleRoutes.scala
|
|
summercms/src/api/Routes.scala
|
|
</files>
|
|
<action>
|
|
1. Create PermissionMiddleware.scala:
|
|
- object PermissionMiddleware:
|
|
def require(permission: String): Middleware[...] - requires exactly this permission
|
|
def requireAny(permissions: String*): Middleware[...] - requires any of these
|
|
def requireAll(permissions: String*): Middleware[...] - requires all of these
|
|
|
|
- Implementation pattern (composing with AuthMiddleware):
|
|
- Get SessionClaims from context (set by AuthMiddleware)
|
|
- Call PermissionChecker.hasPermission(claims.userId, permission)
|
|
- If false, return 403 Forbidden with JSON: { error: "Insufficient permissions" }
|
|
- If true, continue to handler
|
|
|
|
- Example usage in routes:
|
|
```scala
|
|
val protectedRoute = AuthMiddleware.apply @@
|
|
PermissionMiddleware.require("blog.posts.create") @@
|
|
handler { ... }
|
|
```
|
|
|
|
2. Create RoleRoutes.scala (protected, requires "backend.manage_roles"):
|
|
- GET /admin/roles
|
|
- List all roles
|
|
- Return: [{ id, code, name, permissions: [...], isSystem }]
|
|
|
|
- GET /admin/roles/:id
|
|
- Get single role
|
|
- Return: { id, code, name, permissions: [...], isSystem }
|
|
|
|
- POST /admin/roles (requires "backend.manage_roles")
|
|
- Body: { code, name, permissions: [...] }
|
|
- Validate code is unique
|
|
- Return created role
|
|
|
|
- PUT /admin/roles/:id (requires "backend.manage_roles")
|
|
- Body: { name, permissions: [...] }
|
|
- Cannot modify code or isSystem roles
|
|
- Return updated role
|
|
|
|
- DELETE /admin/roles/:id (requires "backend.manage_roles")
|
|
- Cannot delete system roles (is_system = true)
|
|
- Return 204 No Content
|
|
|
|
- GET /admin/permissions
|
|
- List all registered permissions (for role editor UI)
|
|
- Return: { tabs: [{ name, permissions: [{code, label}] }] }
|
|
|
|
3. Update Routes.scala to include RoleRoutes with proper middleware chain
|
|
</action>
|
|
<verify>
|
|
- ./mill summercms.compile succeeds
|
|
- GET /admin/roles returns role list (with Super Admin having "*")
|
|
- POST /admin/roles creates new role
|
|
- User without permission gets 403 Forbidden
|
|
- User with permission gets 200 OK
|
|
</verify>
|
|
<done>
|
|
PermissionMiddleware enforces route-level permissions, role CRUD endpoints available, permission listing available for UI
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Add optional TOTP 2FA</name>
|
|
<files>
|
|
summercms/src/auth/TotpService.scala
|
|
summercms/src/auth/routes/TotpRoutes.scala
|
|
summercms/src/auth/LoginService.scala
|
|
summercms/src/api/Routes.scala
|
|
summercms/src/Main.scala
|
|
build.mill
|
|
</files>
|
|
<action>
|
|
1. Add java-totp dependency to build.mill:
|
|
- mvn"dev.samstevens.totp:totp:1.7.1"
|
|
|
|
2. Create TotpService.scala (ZIO service):
|
|
- trait TotpService:
|
|
def generateSecret: Task[String]
|
|
def generateQrCodeDataUri(secret: String, email: String): Task[String]
|
|
def verifyCode(secret: String, code: String): Task[Boolean]
|
|
def generateRecoveryCodes: Task[List[String]]
|
|
|
|
- Implementation using dev.samstevens.totp:
|
|
- DefaultSecretGenerator for secret generation
|
|
- QrData.Builder with issuer "SummerCMS Admin"
|
|
- ZxingPngQrGenerator for QR code
|
|
- DefaultCodeVerifier for verification
|
|
- RecoveryCodeGenerator for backup codes (8 codes)
|
|
|
|
- Recovery codes:
|
|
- Generate 8 random codes
|
|
- Hash each with SHA-256 before storing (same pattern as password reset tokens)
|
|
- When verifying, hash input and compare to stored hashes
|
|
- Each recovery code is single-use
|
|
|
|
3. Create TotpRoutes.scala (all require auth):
|
|
- POST /admin/auth/2fa/setup
|
|
- Generate new secret
|
|
- Return: { secret, qrCodeDataUri }
|
|
- Does NOT enable 2FA yet (user must verify first)
|
|
|
|
- POST /admin/auth/2fa/verify-setup
|
|
- Body: { secret, code }
|
|
- Verify the code matches the secret
|
|
- If valid: update user with totp_secret, totp_enabled=true, generate and store recovery codes
|
|
- Return: { enabled: true, recoveryCodes: [...] } (only time codes are shown!)
|
|
|
|
- POST /admin/auth/2fa/disable
|
|
- Body: { code } (require current TOTP code to disable)
|
|
- Clear totp_secret, set totp_enabled=false, clear recovery_codes
|
|
- Return: { enabled: false }
|
|
|
|
- GET /admin/auth/2fa/status
|
|
- Return: { enabled: boolean }
|
|
|
|
4. Update LoginService.authenticate:
|
|
- After password verification, check if user has totp_enabled=true
|
|
- If yes and totpCode is None: return AuthError.TotpRequired
|
|
- If yes and totpCode provided:
|
|
- First try TotpService.verifyCode
|
|
- If fails, try matching against recovery codes (consume if match)
|
|
- If no 2FA, proceed normally
|
|
|
|
5. Update Main.scala to provide TotpService.live
|
|
</action>
|
|
<verify>
|
|
- ./mill summercms.compile succeeds
|
|
- POST /admin/auth/2fa/setup returns secret and QR code data URI
|
|
- After enabling 2FA, login requires TOTP code
|
|
- Recovery code can be used once in place of TOTP
|
|
- POST /admin/auth/2fa/disable removes 2FA requirement
|
|
</verify>
|
|
<done>
|
|
2FA can be enabled/disabled by users, login enforces 2FA when enabled, recovery codes work as backup
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After all tasks complete:
|
|
1. Run ./mill summercms.compile - should succeed
|
|
2. Test role creation: POST /admin/roles with new role
|
|
3. Test permission check: Access route without permission (should get 403)
|
|
4. Test wildcard: Give role "blog.*", verify "blog.posts.create" works
|
|
5. Test 2FA setup: POST /admin/auth/2fa/setup, scan QR, verify setup
|
|
6. Test 2FA login: Login without code (should fail), login with code (should succeed)
|
|
7. Test recovery code: Use recovery code instead of TOTP (should work once)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Roles can be created, updated, deleted (except system roles)
|
|
- Permission middleware returns 403 for unauthorized access
|
|
- Wildcard permissions work correctly ("*" and "prefix.*")
|
|
- 2FA can be optionally enabled by users
|
|
- 2FA login requires valid TOTP code when enabled
|
|
- Recovery codes work as 2FA backup (single-use)
|
|
- Super Admin role with "*" can access everything
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/06-backend-authentication/06-03-SUMMARY.md`
|
|
</output>
|