forked from retoor/devplacepy
fix: extend content delete authorization to allow administrators alongside owners across all endpoints
This commit is contained in:
@@ -170,12 +170,12 @@ Devii partially configures its OWN system message: every system prompt ends with
|
||||
- **Forbidden variable name patterns:** `_new`, `_old`, `_temp`, `_v2`, `better_`, `my_`, `the_`.
|
||||
- **Form validation is Pydantic-native.** Routers declare a typed body param `data: Annotated[SomeForm, Form()]` (models live in `models.py`); FastAPI validates it and the global `RequestValidationError` handler in `main.py` re-renders auth pages with messages (400) or redirects other routes. Read raw `await request.form()` only when also reading an uploaded file alongside a model - adding a `File()` param would embed the model under its parameter name.
|
||||
- **`RedirectResponse(url=..., status_code=302)`** - always pass `status_code` as an integer literal, never `status.HTTP_302_FOUND`.
|
||||
- **Ownership checks:** compare `resource["user_uid"] == user["uid"]` before edit/delete. Deletes must cascade: comments → votes → resource.
|
||||
- **Ownership checks:** compare `resource["user_uid"] == user["uid"]` before edit/delete (use `content.is_owner`). **Deletes additionally allow any admin:** every content delete guards `is_owner(...) or is_admin(user)` on the endpoint (posts/gists/projects in `content.delete_content_item`, `comments.delete_comment`, `project_files.project_file_delete`, `media.delete_media`, `uploads.delete_attachment_route`; news is admin-only), so an administrator (including via Devii, which only calls the API) may delete any member's content while members are limited to their own. Edits stay owner-only. Deletes are **soft** (`deleted_at`/`deleted_by`) and cascade comments → votes → engagement → resource under one shared stamp.
|
||||
- **All dates shown to users are DD/MM/YYYY** (European). Use `format_date()` from `utils.py` (template global), or `time_ago()` which auto-switches to DD/MM/YYYY past 30 days.
|
||||
- **Slug + UUID lookup:** resources with slugs (posts, gists, news, projects) accept either the slug or the bare UUID - see `resolve_by_slug()` in `database.py`. Slugs are generated via `make_combined_slug(title, uid)` which prefixes the first 8 chars of the UUID.
|
||||
- **Username:** 3-32 chars, letters/numbers/hyphens/underscores. **Password:** min 6 chars.
|
||||
- **Avatars:** Multiavatar SVG generated locally from the username seed in <5ms, in-memory cached (cleared on restart). URL `/avatar/multiavatar/{seed}?size={size}`. Falls back to initial-based SVG on error.
|
||||
- **Project visibility (`is_private`) and read-only (`read_only`)** are owner-controlled integer flags on the `projects` row (see `AGENTS.md` -> "Project visibility and read-only"). Two invariants: (1) private-read access is gated by the single `content.can_view_project(project, user)` predicate (owner or admin) at EVERY read surface - detail, file routes (`_load_viewable_project`), zip, listing, profile, sitemap - never re-implement the check inline; (2) read-only is enforced as a single data-layer guard `project_files._guard_writable(project_uid)` at the top of every mutation entrypoint in `project_files.py`, so it covers the HTTP routes, Devii (via the routes), and container sync at once - add the guard to any NEW file-mutating function, and never enforce read-only only in a route. Devii may flip read-only or visibility (private/public) only after explicit user confirmation, enforced by `dispatcher.confirmation_error` (the `CONFIRM_REQUIRED` set) before the HTTP call; mirror that pattern for any future irreversible agent action. Both owner toggle buttons (`project_detail.html`) likewise carry `data-confirm` so the human UI gates them through `app.dialog` too. **Deletions ALWAYS require confirmation:** `confirmation_error` additionally gates `project_delete_file`, `delete_project`, `container_instance_action` with `action="delete"`, and any `container_exec` whose command matches `dispatcher.DESTRUCTIVE_COMMAND` (rm/rmdir/unlink/shred/truncate/dd/mkfs/wipefs, `find -delete`, redirect over a file, SQL `drop`/`delete from`) - the agent must show the user the exact target and only then pass `confirm=true` (the `DELETING IS ALWAYS CONFIRMED` system-prompt rule mirrors this). The container controller ignores the extra `confirm` argument.
|
||||
- **Project visibility (`is_private`) and read-only (`read_only`)** are owner-controlled integer flags on the `projects` row (see `AGENTS.md` -> "Project visibility and read-only"). Two invariants: (1) private-read access is gated by the single `content.can_view_project(project, user)` predicate (owner or admin) at EVERY read surface - detail, file routes (`_load_viewable_project`), zip, listing, profile, sitemap - never re-implement the check inline; (2) read-only is enforced as a single data-layer guard `project_files._guard_writable(project_uid)` at the top of every mutation entrypoint in `project_files.py`, so it covers the HTTP routes, Devii (via the routes), and container sync at once - add the guard to any NEW file-mutating function, and never enforce read-only only in a route. Devii may flip read-only or visibility (private/public) only after explicit user confirmation, enforced by `dispatcher.confirmation_error` (the `CONFIRM_REQUIRED` set) before the HTTP call; mirror that pattern for any future irreversible agent action. Both owner toggle buttons (`project_detail.html`) likewise carry `data-confirm` so the human UI gates them through `app.dialog` too. **Deletions ALWAYS require confirmation:** `confirmation_error` gates EVERY content delete tool via the `CONFIRM_REQUIRED` set - `delete_post`, `delete_comment`, `delete_gist`, `delete_project`, `project_delete_file`, `delete_media`, `delete_attachment`, `admin_delete_news` - plus `container_instance_action` with `action="delete"` and any `container_exec` whose command matches `dispatcher.DESTRUCTIVE_COMMAND` (rm/rmdir/unlink/shred/truncate/dd/mkfs/wipefs, `find -delete`, redirect over a file, SQL `drop`/`delete from`). The first call is refused with a message; the agent must show the user the exact target and only then pass `confirm=true` (the `DELETING IS ALWAYS CONFIRMED` system-prompt rule mirrors this). Any new name added to `CONFIRM_REQUIRED` is auto-confirmed by a generic fallback in `confirmation_error`. **Load-bearing: every confirmation-gated tool MUST also declare a `confirm` boolean param in its catalog spec** (use the `confirm()` helper in `catalog.py`, or `arg("confirm", ..., kind="boolean")` for container actions). Tool schemas set `additionalProperties: false`, so a gated tool WITHOUT a declared `confirm` param can never receive `confirm=true` from the model - the gate refuses every call and the agent loops forever (this was a real bug on all the delete tools). The param is harmlessly forwarded to the HTTP endpoint (the routes do not declare it; FastAPI ignores undeclared form fields) and ignored by the LOCAL container/customization controllers. The delete tools stay `requires_auth` (not `requires_admin`) - members delete their own, the endpoint's owner-or-admin guard lets admins delete any.
|
||||
|
||||
## Modal pattern
|
||||
|
||||
@@ -198,6 +198,7 @@ Rules when touching this area:
|
||||
- **Any new read** (find/count/query) of a soft-deletable table MUST filter `deleted_at IS NULL`. Use the central helpers/chokepoints instead of inline deletes.
|
||||
- **Toggles revive, never duplicate:** look up the physical row ignoring `deleted_at`, stamp on toggle-off, clear on re-toggle.
|
||||
- **Cascades share one `stamp`** (`content.delete_content_item`) so the event restores/purges atomically; pass `deleted_by = actor`.
|
||||
- **Delete authz is owner-OR-admin on the endpoint** (`is_owner(...) or is_admin(user)`): posts/gists/projects (`content.delete_content_item`), `comments.delete_comment`, `project_files.project_file_delete`, `media.delete_media`, `uploads.delete_attachment_route`; news is admin-only. One endpoint check covers the UI and Devii (Devii only uses the API, as the signed-in user), so admins delete any member's content and members only their own. Any NEW content delete path must reuse this guard, soft-delete, and (for the Devii tool) be added to `dispatcher.CONFIRM_REQUIRED`.
|
||||
- **GC stays HARD** (job sweep, metrics ring, usage-ledger prune/reset, expired-session cleanup, fork rollback, news-sync image refresh, admin Purge). Logout is soft (auditable); only expiry GC is hard.
|
||||
- Admin **Trash** at `/admin/trash` (`AdminTrashOut`, `admin_trash.html`) restores/purges by event. Admin-only docs: `docs/soft-delete.html`. See `AGENTS.md` -> "Project-wide soft delete".
|
||||
|
||||
|
||||
Reference in New Issue
Block a user