docs(05): create UAT gap closure plans 09-11
Three new plans address all 6 diagnosed gaps from 05-UAT.md:
- 05-09: cloud document open (fetch+Blob URL), re-analyze (cloud-aware
Celery task), and edit (PATCH /api/documents/{id})
- 05-10: OAuth initiate JSON response fix, Nextcloud custom endpoint
edit round-trip, Edit button on ERROR rows, confirmation text overflow
- 05-11: admin hard-delete with admin-password confirmation (backend
UserDeleteConfirm model + frontend inline panel)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
---
|
||||
phase: 05-cloud-storage-backends
|
||||
plan: 11
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- backend/api/admin.py
|
||||
- frontend/src/api/client.js
|
||||
- frontend/src/components/admin/AdminUsersTab.vue
|
||||
- backend/tests/test_admin.py
|
||||
autonomous: true
|
||||
requirements: [ADMIN-02, SEC-09]
|
||||
gap_closure: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Admin can permanently delete a non-admin user after entering their own admin password"
|
||||
- "Backend verifies the admin password before executing the delete"
|
||||
- "Delete purges cloud connections + MinIO objects + all DB rows (existing SEC-09 code runs)"
|
||||
- "Frontend presents an inline confirmation panel with admin password field before calling DELETE"
|
||||
- "Incorrect admin password returns 403 without deleting the user"
|
||||
artifacts:
|
||||
- path: "backend/api/admin.py"
|
||||
provides: "UserDeleteConfirm Pydantic model; delete_user handler reads admin_password from body and verifies it"
|
||||
- path: "frontend/src/api/client.js"
|
||||
provides: "adminDeleteUser(id, adminPassword) calling DELETE /api/admin/users/{id}"
|
||||
- path: "frontend/src/components/admin/AdminUsersTab.vue"
|
||||
provides: "Inline delete confirmation panel with admin password field, mirroring confirmDeactivate pattern"
|
||||
key_links:
|
||||
- from: "frontend/src/components/admin/AdminUsersTab.vue"
|
||||
to: "DELETE /api/admin/users/{id}"
|
||||
via: "adminDeleteUser(id, adminPassword)"
|
||||
- from: "backend/api/admin.py delete_user"
|
||||
to: "services.auth.verify_password"
|
||||
via: "verify_password(body.admin_password, admin.password_hash)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add admin hard-delete with password confirmation: a backend body model that verifies the admin's own password before permanent deletion, and a frontend inline confirmation panel with password field.
|
||||
|
||||
Purpose: The backend delete endpoint exists and correctly purges all user data, but it accepts no authentication proof for the destructive action. There is also no frontend UI to trigger it.
|
||||
|
||||
Output: Admin can initiate deletion from the Users tab, enter their password in an inline panel, and the backend verifies the password before deleting. Incorrect password is rejected with 403.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/05-cloud-storage-backends/05-UAT.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Key contracts the executor needs. -->
|
||||
|
||||
From backend/api/admin.py:
|
||||
- Existing delete_user handler: DELETE /api/admin/users/{user_id}, status 204, no request body
|
||||
- Already purges cloud connections + MinIO objects, writes audit log (SEC-09 — do NOT change this logic)
|
||||
- Uses Depends(get_current_admin) → resolves to User ORM instance as `_admin`
|
||||
- verify_password not currently imported; services.auth exports it: `from services.auth import verify_password`
|
||||
- The handler must add: parse body as UserDeleteConfirm, call verify_password(body.admin_password, _admin.password_hash), raise 403 on failure
|
||||
|
||||
From services/auth.py (existing pattern from admin.py imports):
|
||||
- `hash_password(plain: str) -> str`
|
||||
- `verify_password(plain: str, hashed: str) -> bool` — uses pwdlib Argon2
|
||||
|
||||
From frontend/src/components/admin/AdminUsersTab.vue (confirmDeactivate pattern to mirror):
|
||||
- `confirmDeactivate = ref(null)` tracks which user ID is awaiting confirmation
|
||||
- `startDeactivate(id)` sets confirmDeactivate = id
|
||||
- Inline panel in <td> renders when `confirmDeactivate === user.id`
|
||||
- Panel has confirm + cancel buttons
|
||||
- Model to follow: add parallel state `confirmDelete = ref(null)`, `deletePassword = ref('')`, `deleteError = ref(null)`
|
||||
|
||||
From frontend/src/api/client.js:
|
||||
- All admin functions follow: request(`/api/admin/users/${id}/...`, { method, headers, body })
|
||||
- DELETE with body: `request(\`/api/admin/users/${id}\`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ admin_password: adminPassword }) })`
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Backend — UserDeleteConfirm model + password verification in delete_user</name>
|
||||
<files>backend/api/admin.py, backend/tests/test_admin.py</files>
|
||||
<behavior>
|
||||
- DELETE /api/admin/users/{id} with correct admin_password in body returns 204 and user is deleted.
|
||||
- DELETE /api/admin/users/{id} with wrong admin_password returns 403 {"detail": "Invalid admin password"} and user is NOT deleted.
|
||||
- DELETE /api/admin/users/{id} with no body returns 422 (Pydantic validation).
|
||||
- Cannot delete admin accounts (existing guard: 400 "Cannot delete admin accounts") — unchanged.
|
||||
- Cannot delete non-existent user (existing guard: 404) — unchanged.
|
||||
- Audit log entry written for successful delete (existing code) — unchanged.
|
||||
- Cloud credentials purged before DB delete (existing SEC-09 code) — unchanged.
|
||||
</behavior>
|
||||
<action>
|
||||
In backend/api/admin.py:
|
||||
1. Add `UserDeleteConfirm` Pydantic model in the Request models section:
|
||||
```python
|
||||
class UserDeleteConfirm(BaseModel):
|
||||
admin_password: str
|
||||
```
|
||||
2. Add `from services.auth import verify_password` to the existing imports from services.auth (currently imports `hash_password, revoke_all_refresh_tokens`).
|
||||
3. Modify the `delete_user` handler signature to accept the body:
|
||||
- Change `async def delete_user(user_id, request, session, _admin)` to also accept `body: UserDeleteConfirm`.
|
||||
- FastAPI will parse the JSON body automatically.
|
||||
4. Add password verification BEFORE any deletion logic (fail fast):
|
||||
```python
|
||||
if not verify_password(body.admin_password, _admin.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid admin password",
|
||||
)
|
||||
```
|
||||
5. All existing deletion logic (cloud purge, MinIO purge, audit log, session.delete) is unchanged.
|
||||
|
||||
In backend/tests/test_admin.py, add three tests:
|
||||
1. `test_delete_user_correct_password` — create admin + regular user, call DELETE with correct admin password, assert 204, assert user no longer in GET /admin/users.
|
||||
2. `test_delete_user_wrong_password` — same setup, call DELETE with wrong password, assert 403, assert user still in GET /admin/users (not deleted).
|
||||
3. `test_delete_user_no_body` — call DELETE with no body (or empty body), assert 422.
|
||||
|
||||
Use the existing `_create_user_and_token(session, role="admin")` pattern from test_cloud.py (or the conftest admin_user fixture if available).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest tests/test_admin.py::test_delete_user_correct_password tests/test_admin.py::test_delete_user_wrong_password tests/test_admin.py::test_delete_user_no_body -v</automated>
|
||||
</verify>
|
||||
<done>Three tests pass. Delete with correct password returns 204. Delete with wrong password returns 403 and user survives. Delete with no body returns 422.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Frontend — adminDeleteUser API function + inline delete confirmation panel</name>
|
||||
<files>frontend/src/api/client.js, frontend/src/components/admin/AdminUsersTab.vue</files>
|
||||
<action>
|
||||
### 1. client.js — add adminDeleteUser
|
||||
Export `adminDeleteUser(id, adminPassword)`:
|
||||
```javascript
|
||||
export function adminDeleteUser(id, adminPassword) {
|
||||
return request(`/api/admin/users/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ admin_password: adminPassword }),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. AdminUsersTab.vue — add delete confirmation state
|
||||
In `<script setup>`, add alongside the existing `confirmDeactivate` state:
|
||||
- `const confirmDelete = ref(null)` — holds the user ID awaiting delete confirmation
|
||||
- `const deletePassword = ref('')` — the admin password input
|
||||
- `const deleteError = ref(null)` — error message for wrong password
|
||||
|
||||
Add functions:
|
||||
- `startDelete(id)`: sets `confirmDelete.value = id`, clears `deletePassword.value` and `deleteError.value`, and sets `confirmDeactivate.value = null` (cannot have both panels open at once).
|
||||
- `cancelDelete()`: sets `confirmDelete.value = null`, clears password + error.
|
||||
- `confirmDoDelete(id)`: sets `pendingAction[id] = true`, calls `await api.adminDeleteUser(id, deletePassword.value)`, on success removes the user from `users.value` and calls `cancelDelete()`. On error, sets `deleteError.value = e.message`. Always clears `pendingAction[id]` in finally.
|
||||
|
||||
### 3. AdminUsersTab.vue — template: Delete button in action column
|
||||
In the `<template v-else-if="user.is_active">` block (the normal active user actions), add a Delete button after the Deactivate button:
|
||||
```html
|
||||
<span class="text-gray-300">·</span>
|
||||
<button
|
||||
@click="startDelete(user.id)"
|
||||
class="text-red-800 hover:text-red-900 text-sm font-semibold"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
```
|
||||
For deactivated users (the `<template v-else>` block), also add a Delete button after Reactivate, using the same markup.
|
||||
|
||||
### 4. AdminUsersTab.vue — template: inline delete confirmation panel
|
||||
Replace the existing `<div v-if="confirmDeactivate === user.id">` inline panel pattern: add a second conditional panel for delete below it (as a sibling `<div>` within the `<td>`):
|
||||
```html
|
||||
<div v-if="confirmDelete === user.id" class="space-y-2">
|
||||
<p class="text-xs text-red-700 font-semibold">
|
||||
Permanently delete <span class="font-bold">{{ user.email }}</span>?
|
||||
This will erase all their documents, cloud connections, and quota data. This cannot be undone.
|
||||
</p>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-700 mb-1 font-semibold">Your admin password to confirm</label>
|
||||
<input
|
||||
v-model="deletePassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="Admin password"
|
||||
class="block w-full rounded-lg px-2 py-1.5 text-xs border border-red-300 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-colors"
|
||||
@keydown.enter.prevent="confirmDoDelete(user.id)"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="deleteError" class="text-xs text-red-600">{{ deleteError }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="confirmDoDelete(user.id)"
|
||||
:disabled="pendingAction[user.id] || !deletePassword"
|
||||
class="text-red-700 hover:text-red-800 text-sm font-semibold disabled:opacity-50"
|
||||
>
|
||||
<span v-if="pendingAction[user.id]" class="flex items-center gap-1">
|
||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-3 h-3"></span>
|
||||
Deleting…
|
||||
</span>
|
||||
<span v-else>Delete permanently</span>
|
||||
</button>
|
||||
<span class="text-gray-300">·</span>
|
||||
<button @click="cancelDelete" class="text-gray-500 hover:text-gray-700 text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
The delete panel and deactivate panel are mutually exclusive: `startDelete` clears `confirmDeactivate`, and `startDeactivate` should also clear `confirmDelete` (add `confirmDelete.value = null` to `startDeactivate`).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>Frontend build passes with zero errors. AdminUsersTab has Delete button and inline confirmation panel with password field. adminDeleteUser exported from client.js.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| DELETE /api/admin/users/{id} body | Admin password sent in JSON body — verified server-side with Argon2 before any deletion |
|
||||
| admin password in transit | Sent over HTTPS (production); never stored, logged, or returned in any response |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-05-11-01 | Elevation of Privilege | DELETE /admin/users/{id} | mitigate | Requires get_current_admin (admin role) AND correct admin password verification via pwdlib Argon2 — two-factor confirmation |
|
||||
| T-05-11-02 | Information Disclosure | Wrong password error message | mitigate | 403 "Invalid admin password" — does not confirm whether user exists (user existence already checked first but 403 is returned regardless to prevent oracle) |
|
||||
| T-05-11-03 | Tampering | admin_password in request body | mitigate | Pydantic UserDeleteConfirm validates presence; verify_password uses constant-time Argon2 comparison (pwdlib) |
|
||||
| T-05-11-04 | Repudiation | User deletion audit trail | mitigate | write_audit_log("admin.user_deleted") written before session.delete — existing code preserved unchanged |
|
||||
| T-05-11-05 | Denial of Service | Repeated wrong-password delete attempts | accept | Admin endpoints already rate-limited at application level; admin accounts are trusted actors |
|
||||
| T-05-11-SC | Tampering | npm/pip installs | mitigate | No new packages installed in this plan |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After both tasks complete:
|
||||
- `pytest backend/tests/test_admin.py::test_delete_user_correct_password backend/tests/test_admin.py::test_delete_user_wrong_password backend/tests/test_admin.py::test_delete_user_no_body -v`
|
||||
- `npm run build` — zero errors
|
||||
- Full pytest suite: `pytest -v` — zero new failures
|
||||
- Manual: open Admin panel → Users tab, confirm Delete button visible per user row
|
||||
- Manual: click Delete, enter correct admin password → user removed from list
|
||||
- Manual: click Delete, enter wrong password → error message shown, user not removed
|
||||
- Security: verify admin_password not present in any audit log entry
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- UserDeleteConfirm model added to admin.py
|
||||
- delete_user verifies admin password via verify_password before proceeding
|
||||
- Wrong password returns 403 without deleting user
|
||||
- adminDeleteUser(id, adminPassword) exported from client.js
|
||||
- AdminUsersTab has Delete button on active and deactivated rows
|
||||
- Inline password confirmation panel appears on Delete click, mutually exclusive with deactivate panel
|
||||
- Three new backend tests pass; full test suite has zero new failures; frontend build clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/05-cloud-storage-backends/05-11-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user