diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-13 03:56:30 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-13 03:56:30 +0200 |
| commit | f0248975908ad670fa8a820f865bf22ea8d0172d (patch) | |
| tree | f4af64d36cacaccb4f6d13436e001aeb57e861e3 /packages/meshbay-hub/src/meshbay_hub/api/groups.py | |
| parent | 35130e5528a52161630fd1c93572e1b2b7cd911b (diff) | |
| download | meshbay-f0248975908ad670fa8a820f865bf22ea8d0172d.tar.gz | |
feat: Phase 12 — P2P crypto material, password split, node Ed25519 auth
Baseline commit capturing in-progress Phase 12 work that was already present
in the working tree (uncommitted) before the Phase 11.5 security remediation
begins. Committed as-is, without review or modification, so that remediation
changes arrive as a separable diff.
Contents: BundleStore (P2P GEK + keypair bundles), password split
(auth_key / bundle_key), node Ed25519 auth (POST /v1/nodes/auth, node-scoped
JWT), GEK-HMAC handshake proof with DTLS channel binding, Ed25519 admin
challenge-response, node local admin UI rewrite, browser key persistence.
Not authored in this session — captured to establish a baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/api/groups.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/api/groups.py | 77 |
1 files changed, 31 insertions, 46 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py index e0ea016..88af764 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py @@ -5,10 +5,10 @@ from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from meshbay_hub.api.deps import get_current_user +from meshbay_hub.api.deps import get_current_user, require_user_scope from meshbay_hub.db.engine import get_db from meshbay_hub.db.models import ( - FederatedGroup, GEKBundle, Group, GroupMember, + FederatedGroup, Group, GroupMember, IPLog, SwarmSource, User, ) @@ -37,6 +37,7 @@ async def my_groups( "join_policy": g.join_policy, "created_at": g.created_at.isoformat(), "is_admin": g.admin_id == current_user.id, + "description": g.description or "", } for g in groups ] @@ -56,6 +57,8 @@ async def group_online_nodes( group = await db.get(Group, group_id) if not group: raise HTTPException(status_code=404, detail="Group not found") + if group.status != "active": + raise HTTPException(status_code=403, detail="Group is suspended") node_ids = get_online_nodes_for_group(group_id) nodes = [] @@ -85,6 +88,7 @@ async def list_public_groups( groups = [ { "id": g.id, "name": g.name, "join_policy": g.join_policy, + "description": g.description or "", "created_at": g.created_at.isoformat(), "source": "local", } for g in local @@ -193,7 +197,7 @@ async def group_members( async def join_group( group_id: str, request: Request, - current_user: User = Depends(get_current_user), + current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): group = await db.get(Group, group_id) @@ -219,26 +223,23 @@ class GroupCreateRequest(BaseModel): name: str visibility: str = "private" # public|private join_policy: str = "invite" # open|request|invite - - -class GEKBundleRequest(BaseModel): - pk_eph_b64: str - nonce_b64: str - wrapped_b64: str + description: str | None = None @router.post("", status_code=201) async def create_group( body: GroupCreateRequest, request: Request, - current_user: User = Depends(get_current_user), + current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): + desc = (body.description or "")[:512] if body.description else None group = Group( name=body.name, admin_id=current_user.id, visibility=body.visibility, join_policy=body.join_policy, + description=desc, ) db.add(group) await db.flush() # get group.id @@ -251,12 +252,11 @@ async def create_group( return {"group_id": group.id, "name": group.name} -@router.post("/{group_id}/members/{username}/gek", status_code=201) -async def store_gek_bundle( +@router.post("/{group_id}/members/{username}", status_code=201) +async def add_group_member( group_id: str, username: str, - body: GEKBundleRequest, - current_user: User = Depends(get_current_user), + current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): group = await db.get(Group, group_id) @@ -270,26 +270,11 @@ async def store_gek_bundle( if not target: raise HTTPException(status_code=404, detail="User not found") - # Upsert GEK bundle - existing = await db.get(GEKBundle, (group_id, target.id)) new_member = False - if existing: - existing.pk_eph_b64 = body.pk_eph_b64 - existing.nonce_b64 = body.nonce_b64 - existing.wrapped_b64 = body.wrapped_b64 - else: - db.add(GEKBundle( - group_id=group_id, - user_id=target.id, - pk_eph_b64=body.pk_eph_b64, - nonce_b64=body.nonce_b64, - wrapped_b64=body.wrapped_b64, - )) - # Add member if not already in group - mem = await db.get(GroupMember, (group_id, target.id)) - if not mem: - db.add(GroupMember(group_id=group_id, user_id=target.id)) - new_member = True + mem = await db.get(GroupMember, (group_id, target.id)) + if not mem: + db.add(GroupMember(group_id=group_id, user_id=target.id)) + new_member = True if new_member: from meshbay_hub.api.notifications import create_notification @@ -303,26 +288,26 @@ async def store_gek_bundle( return {"status": "stored", "group_id": group_id, "username": username} -@router.get("/{group_id}/gek") -async def get_my_gek_bundle( +@router.delete("/{group_id}") +async def delete_group( group_id: str, - current_user: User = Depends(get_current_user), + request: Request, + current_user: User = Depends(require_user_scope), db: AsyncSession = Depends(get_db), ): group = await db.get(Group, group_id) if not group: raise HTTPException(status_code=404, detail="Group not found") + if group.admin_id != current_user.id: + raise HTTPException(status_code=403, detail="Only the group creator can delete") - bundle = await db.get(GEKBundle, (group_id, current_user.id)) - if not bundle: - raise HTTPException(status_code=404, detail="No GEK bundle for this user in this group") - - return { - "group_id": group_id, - "pk_eph_b64": bundle.pk_eph_b64, - "nonce_b64": bundle.nonce_b64, - "wrapped_b64": bundle.wrapped_b64, - } + from sqlalchemy import delete as sa_delete + await db.execute(sa_delete(GroupMember).where(GroupMember.group_id == group_id)) + db.add(IPLog(user_id=current_user.id, event="group_delete", + ip_address=_ip(request), detail=group.name)) + await db.delete(group) + await db.commit() + return {"status": "deleted", "group_id": group_id} def _ip(request: Request) -> str: |