Files
pocketpaw/ee/cloud/models/group.py
Rohit Kushwaha d1b5870278 feat(ee-cloud): workspace RBAC consolidation + invite flow + visibility defaults
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>
2026-04-14 10:21:16 +05:30

57 lines
2.0 KiB
Python

"""Group document — multi-user channels with agent participants."""
from __future__ import annotations
from datetime import datetime
from typing import Literal
from beanie import Indexed
from pydantic import BaseModel, Field
from ee.cloud.models.base import TimestampedDocument
# Group member role tiers (ordered by privilege, ascending):
# "view" — read-only
# "edit" — post/react (the default; absence from member_roles means "edit")
# "admin" — can modify group settings, add/remove members & agents
# The group's `owner` field is the implicit top tier (not stored here).
MemberRole = Literal["view", "edit", "admin"]
class GroupAgent(BaseModel):
"""Agent assigned to a group with a respond mode."""
agent: str # Agent ID
role: str = "assistant" # assistant | listener | moderator
respond_mode: str = "mention_only" # mention_only | auto | silent | smart
class Group(TimestampedDocument):
"""Chat group/channel — like Slack channels with AI agents."""
workspace: Indexed(str) # type: ignore[valid-type]
name: str
slug: str = ""
description: str = ""
icon: str = ""
color: str = ""
# Default "private": only explicit members can see/read. Workspace-wide
# readable groups are opt-in via type="public" or type="channel".
type: str = Field(default="private", pattern="^(public|private|dm|channel)$")
members: list[str] = Field(default_factory=list) # User IDs
# Per-member role override: "view" = read-only; absent = "edit" (default).
# Owner is implicit and not stored here.
member_roles: dict[str, MemberRole] = Field(default_factory=dict)
agents: list[GroupAgent] = Field(default_factory=list)
pinned_messages: list[str] = Field(default_factory=list) # Message IDs
owner: str # User ID
archived: bool = False
last_message_at: datetime | None = None
message_count: int = 0
class Settings:
name = "groups"
indexes = [
[("workspace", 1), ("slug", 1)],
]