docs(06): create phase plan
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
This commit is contained in:
334
.planning/phases/06-backend-authentication/06-03-PLAN.md
Normal file
334
.planning/phases/06-backend-authentication/06-03-PLAN.md
Normal file
@@ -0,0 +1,334 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user