gabriel / musehub public
feat BREAKING feat/224-user-crud-admin-delete #1 / 1
AI Agent gabriel · 2 days ago · Oct 22, 2026 · Diff

feat: full CRUD for user/identity -- admin delete, CLI/MCP parity (musehub#224)

Audit: user/identity was an outlier against this workspace's full Rails-style CRUD convention -- DELETE /api/me (GDPR self-delete) existed server-side with zero integration test coverage and no CLI/MCP wrapper; there was no way at all to delete an account that wasn't the caller's own, blocking cleanup of 3 throwaway agent identities from musehub#99 testing.

Investigation found clean precedent for everything this needed: - TokenClaims = MSignContext and require_valid_token = require_signed_request are literal aliases in musehub/auth/dependencies.py -- DELETE /api/me already uses MSign auth, same as every CLI-facing route. No auth-layer fix needed for a CLI wrapper (unlike orgs.py's separate, already-tracked MSign gap). - musehub/api/routes/api/admin.py already establishes the admin pattern: /api/admin/* routes gated by claims.is_admin on the same MSignContext. - POST /api/users (profile creation, distinct from identity/keypair registration via `muse auth register`) already existed server-side with zero CLI wrapper, alongside existing read/update.

Design decisions (confirmed via AskUserQuestion): - Admin delete: DELETE /api/admin/users/{handle}, gated by claims.is_admin, wired into the SAME `muse hub user delete --handle <other>` CLI subcommand rather than a separate admin namespace -- server already enforces admin-only, CLI just routes by whether --handle differs from the caller's own identity. - muse hub user create: added for CRUD symmetry, wrapping the existing POST /api/users endpoint.

Implementation: - New musehub/services/musehub_account.py::delete_identity_account -- shared by both DELETE /api/me (refactored to use it) and the new DELETE /api/admin/users/{handle}, so both paths apply the identical effect (hard-delete auth keys, soft-delete identity) and can never drift. - MCP parity: musehub_create_user_profile and musehub_delete_user_account tools, the latter doing its own is_admin check (MCP calls executor functions directly, bypassing the FastAPI auth dependency layer).

TDD: wrote tests/test_account_deletion.py (self-delete regression coverage that never existed, + 4 new admin-delete tests: success, 403 non-admin, 404 unknown handle, 404 already-deleted handle) and tests/test_mcp_user_account_tools.py (7 tests for the new MCP tools) first. Confirmed genuine RED via muse-dev shelf save/pop isolation on both the HTTP/admin layer and the MCP layer. Also added CLI-side RED tests in muse's tests/test_cli_hub.py (command-construction tests mirroring the existing read/update pattern) before implementing run_user_create/ run_user_delete.

Regression: 188 passed across admin/identity/mcp/compliance/forks test files in musehub; 110 passed in muse's test_cli_hub.py.

Documented both paths in muse/docs/agent-guide.md (Gitism Glossary + Users/topics/forks command reference), per this ticket's own acceptance criteria.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:cb80b9ff286cfe86683166a3a87b9e154cbd890cc4f53071288633672e7df333 sha
+71 ~5 −1 symbols
8 changed · 1105 in snapshot files
sha256:7ed4c364570d19e3d17897d80f6af11765da8c50f771076726db215beb820830 snapshot
+71
symbols added
~5
symbols modified
−1
symbol removed
8
files changed
1105
files in snapshot
0
dead code introduced
Semantic Changes 77 symbols
~ musehub/services/musehub_account.py .py 9 symbols added
+ delete_identity_account function async_function delete_identity_account L21–30
+ AsyncSession import import AsyncSession L13–13
+ MusehubAuthKey import import MusehubAuthKey L15–15
+ MusehubIdentity import import MusehubIdentity L16–16
+ datetime import import datetime L10–10
+ delete import import delete L12–12
+ logging import import logging L9–9
+ timezone import import timezone L10–10
+ logger variable variable logger L18–18
~ tests/test_account_deletion.py .py 30 symbols added
+ _COUNTER variable variable _COUNTER L40–40
+ _NOW variable variable _NOW L39–39
+ _key_count function async_function _key_count L100–104
+ _key_for function function _key_for L48–50
+ _make_ctx function function _make_ctx L53–61
+ _seed_identity_with_key function async_function _seed_identity_with_key L64–97
+ _uid function function _uid L43–45
+ AsyncClient import import AsyncClient L27–27
+ AsyncSession import import AsyncSession L29–29
+ MSignContext import import MSignContext L32–32
+ MusehubAuthKey import import MusehubAuthKey L35–35
+ MusehubIdentity import import MusehubIdentity L36–36
+ _hashlib import import _hashlib L23–23
+ annotations import import annotations L21–21
+ app import import app L37–37
+ compute_identity_id import import compute_identity_id L33–33
+ compute_key_id import import compute_key_id L33–33
+ datetime import import datetime L24–24
+ encode_pubkey import import encode_pubkey L31–31
+ key_fingerprint import import key_fingerprint L34–34
+ long_id import import long_id L31–31
+ pytest import import pytest L26–26
+ require_signed_request import import require_signed_request L32–32
+ select import import select L28–28
+ timezone import import timezone L24–24
+ test_ad01_self_delete_soft_deletes_identity_and_hard_deletes_keys function async_function test_ad01_self_delete_soft_deletes_identity_and_hard_deletes_keys L111–130
+ test_ad02_admin_can_delete_a_different_account function async_function test_ad02_admin_can_delete_a_different_account L137–156
+ test_ad03_non_admin_gets_403 function async_function test_ad03_non_admin_gets_403 L163–175
+ test_ad04_unknown_handle_gets_404 function async_function test_ad04_unknown_handle_gets_404 L182–188
+ test_ad05_already_deleted_handle_gets_404 function async_function test_ad05_already_deleted_handle_gets_404 L195–208
~ tests/test_mcp_user_account_tools.py .py 29 symbols added
+ TestCreateUserProfile class class TestCreateUserProfile L76–111
+ test_create_rejects_duplicate_username method async_method test_create_rejects_duplicate_username L91–98
+ test_create_rejects_second_profile_for_same_actor method async_method test_create_rejects_second_profile_for_same_actor L100–111
+ test_create_succeeds_for_new_username method async_method test_create_succeeds_for_new_username L77–89
+ TestDeleteUserAccount class class TestDeleteUserAccount L114–169
+ test_admin_can_delete_a_different_account method async_method test_admin_can_delete_a_different_account L130–143
+ test_non_admin_cannot_delete_a_different_account method async_method test_non_admin_cannot_delete_a_different_account L145–156
+ test_self_delete_hard_deletes_keys_and_soft_deletes_identity method async_method test_self_delete_hard_deletes_keys_and_soft_deletes_identity L115–128
+ test_unknown_username_returns_user_not_found method async_method test_unknown_username_returns_user_not_found L158–169
+ _COUNTER variable variable _COUNTER L30–30
+ _key_count function async_function _key_count L69–73
+ _make_identity_with_key function async_function _make_identity_with_key L38–66
+ _uid function function _uid L33–35
+ AsyncSession import import AsyncSession L15–15
+ MusehubAuthKey import import MusehubAuthKey L20–20
+ MusehubIdentity import import MusehubIdentity L21–21
+ _hashlib import import _hashlib L11–11
+ annotations import import annotations L8–8
+ base64 import import base64 L10–10
+ compute_key_id import import compute_key_id L18–18
+ encode_pubkey import import encode_pubkey L17–17
+ execute_create_user_profile import import execute_create_user_profile L22–22
+ execute_delete_user_account import import execute_delete_user_account L22–22
+ execute_read_user_profile import import execute_read_user_profile L22–22
+ fake_id import import fake_id L17–17
+ key_fingerprint import import key_fingerprint L19–19
+ pytest import import pytest L13–13
+ select import import select L14–14
+ pytestmark variable variable pytestmark L28–28
~ musehub/api/routes/api/admin.py .py 1 symbol added
+ admin_delete_user function async_function admin_delete_user L167–200
~ musehub/api/routes/musehub/users.py .py 1 symbol removed, 3 symbols modified
− delete import import delete L23–23
~ or_
~ select
~ musehub/mcp/dispatcher.py .py 1 symbol modified
+ execute_create_user_profile function async_function execute_create_user_profile L3259–3314
+ execute_delete_user_account function async_function execute_delete_user_account L3383–3435
Files Changed
+3 ~5
1105 in snapshot
← Older Oldest on feat/224-user-crud-admin-delete
All commits
Newer → Latest on feat/224-user-crud-admin-delete

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:cb80b9ff286cfe86683166a3a87b9e154cbd890cc4f53071288633672e7df333 --body "your comment"