Admin Console
The Admin Console is at /dashboard/admin on the Singularity Marketplace site. It exposes two tabs (Users, Packages) backed by admin-scoped endpoints under /api/v1/admin/... and an audit-log search. Access requires publishers.is_admin = TRUE AND a MFA factor enrolled AND an interactive web session — API keys are blocked on every write.
What it does
Section titled “What it does”| Capability | UI | Backend |
|---|---|---|
| List/search publishers (incl. disabled / deleted / banned) | /dashboard/admin/users | GET /api/v1/admin/publishers |
| Inspect a publisher (audit history, current license, ban status) | drawer | GET /api/v1/admin/publishers/{id} |
| Disable / re-enable an account | drawer action | PATCH /api/v1/admin/publishers/{id} |
| Soft-delete an account (PII-scrub, cascade-disable packages) | drawer action | DELETE /api/v1/admin/publishers/{id} |
| Hard-delete an account (30-day buffer + dual-control) | drawer action | DELETE /api/v1/admin/publishers/{id}?hard=true |
| Change a publisher’s Snippbot license tier | drawer dropdown | PATCH /api/v1/admin/publishers/{id}/license |
| List/search packages (incl. unpublished / admin-disabled) | /dashboard/admin/packages | GET /api/v1/admin/packages |
| Admin-disable / re-enable a package | drawer action | POST /api/v1/admin/packages/{id}/{disable,enable} |
| Hard-delete a package (dual-control when installs > 100) | drawer action | DELETE /api/v1/admin/packages/{id} |
| Yank / unyank a version | per-version button | POST /api/v1/admin/packages/{id}/versions/{v}/{yank,unyank} |
Bootstrap
Section titled “Bootstrap”The first admin must be set in the database directly. There is no self-promotion path via HTTP.
psql -d singularity_registry <<SQLUPDATE publishers SET is_admin = TRUE WHERE name = 'your-handle';SQLNotes:
- A trigger in migration 033 flips
mfa_required = TRUEautomatically whenis_adminbecomes TRUE. The admin must enrol a TOTP or WebAuthn factor at/dashboard/settings/mfabefore they can log in again. mfa_requiredis sticky — once TRUE it can never go back to FALSE. Demoting an admin keeps the MFA requirement (prevents the “demote then re-promote without MFA” attack).- The grant operation should run through
pg_audit/log_statement = 'all'so it leaves a Postgres-level trail. - Always keep ≥ 2 admins. The application refuses to disable, delete, or demote the last remaining active admin.
Security gates
Section titled “Security gates”Every destructive action passes through six gates before it touches the database:
- Admin guard —
_require_admin(request, write=True)rejects unauthenticated and non-admin requests. - API-key block — admin writes refuse
Authorization: Bearer sgty_…API keys. Reads still allow API keys. - CSRF — double-submit token (
singularity_csrfcookie ↔X-CSRF-Tokenheader). - Sudo-mode — a fresh password challenge issued via
POST /api/v1/auth/sudoproduces a 5-minute one-shot token sent inX-Admin-Sudo-Token. - Rate limits — per-admin per-action caps (e.g. 20 disables/hour, 1 hard-delete/day). Returns 429 with
Retry-After. - Self/last-admin guards — refuses self-actions and never lets the platform reach zero active admins.
Hard-delete (publishers) and high-impact package delete (> 100 installs) additionally require dual control: one admin submits via POST /api/v1/admin/pending-actions, a different admin approves via POST /api/v1/admin/pending-actions/{id}/approve, then the original endpoint is called with pending_id=.
Disable vs ban vs delete
Section titled “Disable vs ban vs delete”| State | Reversible by | PII intact | Can re-register | Affects |
|---|---|---|---|---|
| Disabled | any admin | yes | n/a (same account) | login + UI visibility |
Banned (via ban_violation) | any admin | yes | NO (banned_identifiers) | login + email/IP re-registration |
| Soft-deleted | any admin (within 30 days) | NO (scrubbed) | NO (name reserved) | account removed from app, recoverable manually |
| Hard-deleted | nobody | NO | depends on force_reuse flag | row permanently removed |
The same vocabulary applies to packages: admin-disable (reversible, distinct from owner-initiated unpublish) → admin-delete (hard, version history retained in audit_logs only).
Audit log
Section titled “Audit log”Every successful write inserts exactly one row into audit_logs. The details JSONB column stores the operator’s stated reason verbatim plus action-specific metadata (e.g. cascade summary on a publisher delete). Reads of detail endpoints also write audit rows so slow exfiltration is traceable.
Schema enforcement:
- The application DB role has only
INSERTandSELECTonaudit_logs.UPDATEandDELETEare blocked at the role grant level. - A
BEFORE UPDATE OR DELETEtrigger raises an exception as defence-in-depth so the policy survives a futureGRANTmistake. - Hard-deleting audit rows requires DBA-level access AND is itself logged at the Postgres WAL layer.
Query the log via the existing endpoint:
curl -H "Authorization: Bearer $TOKEN" \ "https://api.singularitymarketplace.com/api/v1/admin/audit-log?resource_type=publisher&action_prefix=publisher.disabled"What’s intentionally NOT here
Section titled “What’s intentionally NOT here”- Impersonation — deliberately deferred. High blast radius; needs its own plan.
- Admin-promote/demote UI — promotion stays a
psqloperation (small surface area, easier to audit). - GDPR data-export — handled by
snippbot exporton the daemon side, not the registry. - Per-package permissions — admin powers are coarse-grained. Fine-grained permissions are a separate proposal.
Related
Section titled “Related”PLAN_ADMIN_CONSOLE.md— the implementation plan this console was built from.- Bootstrap a new admin — the operational process for the first admin.
- Auth model — how
_require_adminplugs into the request pipeline.