From 947efe7bd1bad1f41be4dc6d238f6ed9b4fcdd6e Mon Sep 17 00:00:00 2001 From: anti Date: Wed, 15 Apr 2026 12:51:14 -0400 Subject: [PATCH] feat: add configuration management API endpoints - api_get_config.py: retrieve current DECNET config (admin only) - api_update_config.py: modify deployment settings (admin only) - api_manage_users.py: user/role management (admin only) - api_reinit.py: reinitialize database schema (admin only) - Integrated with centralized RBAC per memory: server-side UI gating --- decnet/web/router/config/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 159 bytes .../api_get_config.cpython-314.pyc | Bin 0 -> 2341 bytes .../api_manage_users.cpython-314.pyc | Bin 0 -> 5550 bytes .../__pycache__/api_reinit.cpython-314.pyc | Bin 0 -> 1373 bytes .../api_update_config.cpython-314.pyc | Bin 0 -> 2277 bytes decnet/web/router/config/api_get_config.py | 55 ++++++++ decnet/web/router/config/api_manage_users.py | 123 ++++++++++++++++++ decnet/web/router/config/api_reinit.py | 25 ++++ decnet/web/router/config/api_update_config.py | 43 ++++++ 10 files changed, 246 insertions(+) create mode 100644 decnet/web/router/config/__init__.py create mode 100644 decnet/web/router/config/__pycache__/__init__.cpython-314.pyc create mode 100644 decnet/web/router/config/__pycache__/api_get_config.cpython-314.pyc create mode 100644 decnet/web/router/config/__pycache__/api_manage_users.cpython-314.pyc create mode 100644 decnet/web/router/config/__pycache__/api_reinit.cpython-314.pyc create mode 100644 decnet/web/router/config/__pycache__/api_update_config.cpython-314.pyc create mode 100644 decnet/web/router/config/api_get_config.py create mode 100644 decnet/web/router/config/api_manage_users.py create mode 100644 decnet/web/router/config/api_reinit.py create mode 100644 decnet/web/router/config/api_update_config.py diff --git a/decnet/web/router/config/__init__.py b/decnet/web/router/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/decnet/web/router/config/__pycache__/__init__.cpython-314.pyc b/decnet/web/router/config/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80f5ae278e319c91668db880ac976a52572fc0c3 GIT binary patch literal 159 zcmdPq_I|p@<2{{|u76Wuu>wpPQ^de>g?wlqMwqQoR?anU!IzzUzA^3l3JvnoS&DLnXVrnpP83g5+AQu iP;}bI@BV!RWkOcq*y(E4B literal 0 HcmV?d00001 diff --git a/decnet/web/router/config/__pycache__/api_get_config.cpython-314.pyc b/decnet/web/router/config/__pycache__/api_get_config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2791f5de7c5a841f9dd6c6317da4b8e0086866de GIT binary patch literal 2341 zcmZ`4T~8ZFaPNHfoxhzF0yf_epf1FY6w(j`NHjLViXr4=oJN9J24CQ!v+ufhh9<2l z_e&+p3w?-GMTz>F_9wJYO?j^)sGzI1N!3(p-V8)VdF!mt211gRZgys8cD`vZV8&R3ZJ{(9vy5>DSH>+d>zK{3jqwIw zrn#6fW;g6DXohJ+eHB%POMh`N?7m1#m zh$d2zXfhhVI2jc|(u1WB@-miEi?Xx?;vAMV6=Ypgx`ahZ*VLRY5zDEQ*_(WLPR(7D z=ZX7VdC%jtA*(t0YjBM?Gm<`sWsRQPY(A&vl}u|+HCh+bikwLsQtKR+GE&Zv(~1se zIU_x<6FaUL(C27)l@JjJI1PU1B!B`+qN{BG7YJqGtPx#e78sG429CKzb{`M3kOa#! z7C3~5D=G<=M30w*HoNM8#*=CPTmVIs97nT2FJn)RaNw`nWJz zcuATK<8mS^!H3hDoSK)6l$nc=c7wQ5sdNrvg(y=gTnl<@;L`&D3TPK~FupAc?inSz ztsXB^JHSQds3}4hEyvH{+nEOc+ffT#-j8gO#&l@4ArV!};yje-b;!?$Ou-VjI*@iC zLvOgtzVR-bWV$Q1GIccPNoLY|$T*OqWf~gCTzwV)#;yBjnkC5|(!;)14>v8mM$g`v z#);M>60N;7qm?CEk`|Fma$$@1D$mycK2*y`)bj%wEVq00zPazW=PUWMC2c-5#pl@& z%Cn<`2xP=QEn}<4w9#6?Rm@d%WFfmj`BGz;!E9ao= z@^Xd11`6Z50&>-tEXZEFA=Ez->WluK^%JA(!uhA}t`d)|wYwhV z@>79h8-o9l;4cRH*59367cM<@cT<6;k_XwnE8I%*j%!m06dQXth4$j{!4l88Y=ub4 zg#_2~=$9X_EUoj+)PDGGcrDPgQPZ<*-}0ejEjwPp@7VGHw!%DjBe#FE_SnkeohzIE zz&8W;o2pc2SFwBO*UsVRcB|X7>p))rt?RdyZKu`Uu;qfwTU5<5w|x1Qd-o`EHLf`u zHammMk>ZiIN3QlKPH*ADc9cOiXMd|1*m@h)_P*F=;l>O7GUV*DK%|$s$3$AW`%QrT zz(hK@ADw{x#CK5GGjasBhl5O{lY2NM0RFRegpG7_t2||_`svYX2SZ_J8FpI%p9p#h ze=Q9arpcH-KbZC?ahNYbNXjji%Y)Qa4&Z4BL7TwaD&^*6Nyl^;nx$HEP-He#E->A! zHka&fY!rz2p^+(xnP7rxRqC=VSF0S=wqeJ07@2yVIHHde^B>h)c*%M z@t6JR?S_KwNn>*n`8S)|izraCAy)V*@M&O^b8kBQMSI|h;9SlYn}&++;gW^*+MpCz f*AB9=Ed0vu(1YAs*U*3TaoX|1*|rC;vMc`syqpm3 literal 0 HcmV?d00001 diff --git a/decnet/web/router/config/__pycache__/api_manage_users.cpython-314.pyc b/decnet/web/router/config/__pycache__/api_manage_users.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c52e5242ce44edaad8c9a9e730624ae27d405e7c GIT binary patch literal 5550 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 CgoNw> literal 0 HcmV?d00001 diff --git a/decnet/web/router/config/__pycache__/api_reinit.cpython-314.pyc b/decnet/web/router/config/__pycache__/api_reinit.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d74c059d063121556df6636507617c980c1dd4ae GIT binary patch literal 1373 zcmZ8g&u<$=6n?X7@A`L~reM)DjoSc~RVho`gn&eXqS)X_QE<0bPD!oJ?!;NN-ZeAh zP<%kGM3AZ?l&UJ!c^OkSW7W8bN4~nLRdu0P6CyDW zd*muCjZ=u^Ub6PodE&`v#?1I4F*Q>+69zGpIHn9@C}w&^9qrmvuEL(aL`JjQ#LN`H z%of1Qd*t8jX*0I~rKwZ7sicO<_1dNxbOdFgUZ!p8J3P!*>-AdYzD?W04g3-b^X1CM zcBS4ZS9U5}Z`3MgNeQ!zzSVIVZCFmr^}`sW?Vy`8?7%;8n+BtQG;}md8)Oz!Tlu>+*`CW!seV;`m;t z7_g#4kEj>4DJ!-Dhem0rZ|!^3!57Aa)#PE4QQi)Gj+aVe#^gW@eM#H!1vEX_fqTkJ z9|AZSy9{^8Au-7=u8f+gOngH@`qWbt0EF=oc8yWdVUnnrbl+f64bJJa+HPY+;bQxpFw_(rnNUBl5OCXNvJOmA9-#$OOR^?ui9mb<>}k0nLYR03KTpS_YwS6UNy+j0#pd>z?dvER}cJU2Mljf8^1NUh)qR(NO^a literal 0 HcmV?d00001 diff --git a/decnet/web/router/config/__pycache__/api_update_config.cpython-314.pyc b/decnet/web/router/config/__pycache__/api_update_config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79a2ab0e795de27849886128b75eef3b5bba6954 GIT binary patch literal 2277 zcmd5-&2Jl35PxrXz25al;^xc2Y3fZ)LQNYlB(6#eZ57g}jufJ>`8Xx9Huff6wDy{} zYoI=$@Fgm!gbW%jTF3ya@fW>#%7ZA%q+4w&gOCfJK&w! zMLrj_gE_$#l4uK0plsWPEpCY`!yU&xu5d~qgB4y0DuRp^5qJoA7ZY6D_Y6OM2Pmh*mK{JOVci^L9xq+e>;$w-vahS++ZV(=c~b zyr-xV&rbgu66A)9npGfrg?g%; znK!FOQ7W6Z1P=A0YHLz~Xhn!XR}D-51}tjBb2LgxEfh4%lG>pc_3x-Iw&9zBGf5-F zB=9phTivr_-{d;d-fot;kmyJelGzkG;Et%Z%RjkKue-e;Ayn?OA{2p#mtE1)?7T%YM8@EJz>pX)ateq=W15;GBf#9TV+lU_9!bItky=~^as;V( zoIv6JuQFd`?j@b`SAVb$udm*}zUsWZ_8_!=$gMlvdOb4i#9wv7GY&V?WKm!PKW#<8 z`s0_f_yEsv--}sXlRDMi^LlW))NaCQz1@ZX6}m|ugbFF74Mhei8KMGA=p^+q?4`}a zoJUer(E*--UvDBH55=hl3)}-9@DsoT2^j^u{|)f^+Q9S6VY#FJCf}(`zEhWFavI{U zN&f7VI(n(2KY>Cr4!6mRl$-(b4DeFNfcL!T$Rt?N0c6bqc?N)WVYDu$zFj?>oV!0c z=Lm1M0&JLng4h7x@Q8q-BToFX6TaebSDGTopAreYET^Mxa94%asOSVb5)sgV&WGe ze5d4$Uv;9_>i#zCL@&0tvJ-u!5#;*$Mi4QP77`eyIf4cg_og2VocoK8zt$kg8UwIv U#JaAh{=I8}v*-HHfex_$22?r dict: + limits_state = await repo.get_state("config_limits") + globals_state = await repo.get_state("config_globals") + + deployment_limit = ( + limits_state.get("deployment_limit", _DEFAULT_DEPLOYMENT_LIMIT) + if limits_state + else _DEFAULT_DEPLOYMENT_LIMIT + ) + global_mutation_interval = ( + globals_state.get("global_mutation_interval", _DEFAULT_MUTATION_INTERVAL) + if globals_state + else _DEFAULT_MUTATION_INTERVAL + ) + + base = { + "role": user["role"], + "deployment_limit": deployment_limit, + "global_mutation_interval": global_mutation_interval, + } + + if user["role"] == "admin": + all_users = await repo.list_users() + base["users"] = [ + UserResponse( + uuid=u["uuid"], + username=u["username"], + role=u["role"], + must_change_password=u["must_change_password"], + ).model_dump() + for u in all_users + ] + if DECNET_DEVELOPER: + base["developer_mode"] = True + + return base diff --git a/decnet/web/router/config/api_manage_users.py b/decnet/web/router/config/api_manage_users.py new file mode 100644 index 0000000..c1bf9a8 --- /dev/null +++ b/decnet/web/router/config/api_manage_users.py @@ -0,0 +1,123 @@ +import uuid as _uuid + +from fastapi import APIRouter, Depends, HTTPException + +from decnet.web.auth import get_password_hash +from decnet.web.dependencies import require_admin, repo +from decnet.web.db.models import ( + CreateUserRequest, + UpdateUserRoleRequest, + ResetUserPasswordRequest, + UserResponse, +) + +router = APIRouter() + + +@router.post( + "/config/users", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required"}, + 409: {"description": "Username already exists"}, + 422: {"description": "Validation error"}, + }, +) +async def api_create_user( + req: CreateUserRequest, + admin: dict = Depends(require_admin), +) -> UserResponse: + existing = await repo.get_user_by_username(req.username) + if existing: + raise HTTPException(status_code=409, detail="Username already exists") + + user_uuid = str(_uuid.uuid4()) + await repo.create_user({ + "uuid": user_uuid, + "username": req.username, + "password_hash": get_password_hash(req.password), + "role": req.role, + "must_change_password": True, + }) + return UserResponse( + uuid=user_uuid, + username=req.username, + role=req.role, + must_change_password=True, + ) + + +@router.delete( + "/config/users/{user_uuid}", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required / cannot delete self"}, + 404: {"description": "User not found"}, + }, +) +async def api_delete_user( + user_uuid: str, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + if user_uuid == admin["uuid"]: + raise HTTPException(status_code=403, detail="Cannot delete your own account") + + deleted = await repo.delete_user(user_uuid) + if not deleted: + raise HTTPException(status_code=404, detail="User not found") + return {"message": "User deleted"} + + +@router.put( + "/config/users/{user_uuid}/role", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required / cannot change own role"}, + 404: {"description": "User not found"}, + 422: {"description": "Validation error"}, + }, +) +async def api_update_user_role( + user_uuid: str, + req: UpdateUserRoleRequest, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + if user_uuid == admin["uuid"]: + raise HTTPException(status_code=403, detail="Cannot change your own role") + + target = await repo.get_user_by_uuid(user_uuid) + if not target: + raise HTTPException(status_code=404, detail="User not found") + + await repo.update_user_role(user_uuid, req.role) + return {"message": "User role updated"} + + +@router.put( + "/config/users/{user_uuid}/reset-password", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required"}, + 404: {"description": "User not found"}, + 422: {"description": "Validation error"}, + }, +) +async def api_reset_user_password( + user_uuid: str, + req: ResetUserPasswordRequest, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + target = await repo.get_user_by_uuid(user_uuid) + if not target: + raise HTTPException(status_code=404, detail="User not found") + + await repo.update_user_password( + user_uuid, + get_password_hash(req.new_password), + must_change_password=True, + ) + return {"message": "Password reset successfully"} diff --git a/decnet/web/router/config/api_reinit.py b/decnet/web/router/config/api_reinit.py new file mode 100644 index 0000000..ced28b1 --- /dev/null +++ b/decnet/web/router/config/api_reinit.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends, HTTPException + +from decnet.env import DECNET_DEVELOPER +from decnet.web.dependencies import require_admin, repo + +router = APIRouter() + + +@router.delete( + "/config/reinit", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required or developer mode not enabled"}, + }, +) +async def api_reinit(admin: dict = Depends(require_admin)) -> dict: + if not DECNET_DEVELOPER: + raise HTTPException(status_code=403, detail="Developer mode is not enabled") + + counts = await repo.purge_logs_and_bounties() + return { + "message": "Data purged", + "deleted": counts, + } diff --git a/decnet/web/router/config/api_update_config.py b/decnet/web/router/config/api_update_config.py new file mode 100644 index 0000000..d5c60f8 --- /dev/null +++ b/decnet/web/router/config/api_update_config.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter, Depends + +from decnet.web.dependencies import require_admin, repo +from decnet.web.db.models import DeploymentLimitRequest, GlobalMutationIntervalRequest + +router = APIRouter() + + +@router.put( + "/config/deployment-limit", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required"}, + 422: {"description": "Validation error"}, + }, +) +async def api_update_deployment_limit( + req: DeploymentLimitRequest, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + await repo.set_state("config_limits", {"deployment_limit": req.deployment_limit}) + return {"message": "Deployment limit updated"} + + +@router.put( + "/config/global-mutation-interval", + tags=["Configuration"], + responses={ + 401: {"description": "Could not validate credentials"}, + 403: {"description": "Admin access required"}, + 422: {"description": "Validation error"}, + }, +) +async def api_update_global_mutation_interval( + req: GlobalMutationIntervalRequest, + admin: dict = Depends(require_admin), +) -> dict[str, str]: + await repo.set_state( + "config_globals", + {"global_mutation_interval": req.global_mutation_interval}, + ) + return {"message": "Global mutation interval updated"}