mirror of
https://github.com/pocketpaw/pocketpaw.git
synced 2026-05-13 21:21:53 +00:00
Consolidates the two parallel RBAC frameworks into a single route-level
authorization system and adds the missing pieces needed for a managed-onboarding
pilot: an endpoint to list pending invites, sane visibility defaults for
pockets/groups so invited teammates actually see shared work, and audit
logging on every denial.
RBAC consolidation
- New ACTIONS table (src/pocketpaw/ee/guards/actions.py) is the single source
of truth for every guarded action → (minimum role, stable deny code).
Covers workspace, group, message, pocket, agent, session, KB, invite,
and billing — 32 rows total.
- New audit helpers (src/pocketpaw/ee/guards/audit.py): log_denial() +
log_privileged_action() backed by the existing append-only audit log.
- New FastAPI deps: require_action(), require_action_any_workspace(),
require_group_action(), require_membership(), require_agent_owner_or_admin(),
require_pocket_edit(), require_pocket_owner().
- Route-level enforcement across workspace/chat/pockets/agents/sessions/kb
routers; permission checks removed from service bodies where now redundant.
- Group role model extended to 3 tiers (owner > admin > member), with per-member
override via Group.member_roles. Admin tier enforced via resolve_group_role()
and the updated _require_group_admin helper.
- Legacy ee/cloud/shared/permissions.py deleted; legacy require_role shim
removed from ee/cloud/shared/deps.py.
- Matrix test (tests/cloud/test_rbac_matrix.py) iterates every ACTIONS entry
across every peer role, verifying both allow and deny paths and the exact
Forbidden.code. Meta-test enforces coverage.
- Redundant tests/cloud/test_permissions.py removed.
Invite list endpoint
- GET /workspaces/{workspace_id}/invites returns all pending invites,
admin-only via require_action("invite.create"). Wires the admin UX for
listing + copy-link + revoke.
Visibility defaults
- Pocket.visibility default flipped "private" → "workspace" so new pockets
are visible to all workspace members out of the box. Owners can tighten
per-pocket via the Share tab.
- Group.type default flipped "public" → "private" so new groups are
invite-only. Public channels remain explicit via type="channel" or "public".
- CreateGroupRequest and UpdateGroupRequest updated; GroupService.update_group
now supports type changes. list_groups() treats "channel" same as "public"
for workspace-wide readability.
- Existing tests updated to match new defaults.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
3.7 KiB
Python
150 lines
3.7 KiB
Python
"""Request/response and WebSocket message schemas for chat."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# REST — Requests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class CreateGroupRequest(BaseModel):
|
|
name: str = Field(min_length=1, max_length=100)
|
|
description: str = ""
|
|
type: Literal["public", "private", "dm", "channel"] = "private"
|
|
member_ids: list[str] = Field(default_factory=list)
|
|
icon: str = ""
|
|
color: str = ""
|
|
|
|
|
|
class UpdateGroupRequest(BaseModel):
|
|
name: str | None = None
|
|
description: str | None = None
|
|
icon: str | None = None
|
|
color: str | None = None
|
|
# Toggle visibility — "private" (members-only) vs "public"/"channel"
|
|
# (any workspace member can read). DMs cannot be retyped.
|
|
type: Literal["public", "private", "channel"] | None = None
|
|
|
|
|
|
class AddGroupMembersRequest(BaseModel):
|
|
user_ids: list[str]
|
|
role: Literal["edit", "view"] = "edit"
|
|
|
|
|
|
class UpdateMemberRoleRequest(BaseModel):
|
|
role: Literal["edit", "view"]
|
|
|
|
|
|
class AddGroupAgentRequest(BaseModel):
|
|
agent_id: str
|
|
role: str = "assistant"
|
|
respond_mode: str = "auto"
|
|
|
|
|
|
class UpdateGroupAgentRequest(BaseModel):
|
|
respond_mode: str
|
|
|
|
|
|
class SendMessageRequest(BaseModel):
|
|
content: str = Field(min_length=1, max_length=10_000)
|
|
reply_to: str | None = None
|
|
mentions: list[dict] = Field(default_factory=list)
|
|
attachments: list[dict] = Field(default_factory=list)
|
|
|
|
|
|
class EditMessageRequest(BaseModel):
|
|
content: str = Field(min_length=1, max_length=10_000)
|
|
|
|
|
|
class ReactRequest(BaseModel):
|
|
emoji: str = Field(min_length=1, max_length=50)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# REST — Responses
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class MessageResponse(BaseModel):
|
|
id: str
|
|
group: str
|
|
sender: str | None
|
|
sender_type: str
|
|
sender_name: str = ""
|
|
content: str
|
|
mentions: list[dict]
|
|
reply_to: str | None
|
|
attachments: list[dict]
|
|
reactions: list[dict]
|
|
edited: bool
|
|
edited_at: datetime | None
|
|
deleted: bool
|
|
created_at: datetime
|
|
|
|
|
|
class GroupResponse(BaseModel):
|
|
id: str
|
|
workspace: str
|
|
name: str
|
|
slug: str
|
|
description: str
|
|
type: str
|
|
icon: str
|
|
color: str
|
|
owner: str
|
|
members: list[Any] # User IDs or populated objects
|
|
agents: list[Any]
|
|
pinned_messages: list[str]
|
|
archived: bool
|
|
last_message_at: datetime | None
|
|
message_count: int
|
|
created_at: datetime
|
|
|
|
|
|
class CursorPage(BaseModel):
|
|
"""Cursor-based pagination response."""
|
|
|
|
items: list[MessageResponse]
|
|
next_cursor: str | None = None
|
|
has_more: bool = False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# WebSocket Schemas
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class WsInbound(BaseModel):
|
|
"""Validated inbound WebSocket message from client."""
|
|
|
|
type: Literal[
|
|
"message.send",
|
|
"message.edit",
|
|
"message.delete",
|
|
"message.react",
|
|
"typing.start",
|
|
"typing.stop",
|
|
"presence.update",
|
|
"read.ack",
|
|
]
|
|
group_id: str | None = None
|
|
message_id: str | None = None
|
|
content: str | None = None
|
|
reply_to: str | None = None
|
|
mentions: list[dict] = Field(default_factory=list)
|
|
attachments: list[dict] = Field(default_factory=list)
|
|
emoji: str | None = None
|
|
status: str | None = None
|
|
|
|
|
|
class WsOutbound(BaseModel):
|
|
"""Outbound WebSocket message to client."""
|
|
|
|
type: str
|
|
data: dict = Field(default_factory=dict)
|