mirror of
https://github.com/pocketpaw/pocketpaw.git
synced 2026-05-19 16:31:15 +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>
107 lines
3.4 KiB
Python
107 lines
3.4 KiB
Python
"""Tests for cloud model changes — pure Pydantic validation, no DB needed.
|
|
|
|
Uses model_construct() to bypass Beanie's __init__ (which requires a live
|
|
MongoDB collection). We then verify default values and field acceptance via
|
|
Pydantic's model_validate (construct=True).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from ee.cloud.models.group import Group
|
|
from ee.cloud.models.invite import Invite
|
|
from ee.cloud.models.message import Message
|
|
from ee.cloud.models.notification import Notification
|
|
from ee.cloud.models.pocket import Pocket
|
|
from ee.cloud.models.session import Session
|
|
from ee.cloud.models.workspace import Workspace
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Group
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_group_supports_dm_type():
|
|
g = Group.model_construct(
|
|
workspace="w1", name="DM", type="dm", owner="u1", members=["u1", "u2"]
|
|
)
|
|
assert g.type == "dm"
|
|
|
|
|
|
def test_group_has_last_message_at():
|
|
g = Group.model_construct(workspace="w1", name="test", owner="u1")
|
|
assert g.last_message_at is None
|
|
|
|
|
|
def test_group_has_message_count():
|
|
g = Group.model_construct(workspace="w1", name="test", owner="u1")
|
|
assert g.message_count == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Message
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_message_has_edited_at():
|
|
m = Message.model_construct(group="g1", sender="u1", content="hello")
|
|
assert m.edited_at is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pocket
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_pocket_sharing_fields():
|
|
p = Pocket.model_construct(workspace="w1", name="test", owner="u1")
|
|
assert p.share_link_token is None
|
|
assert p.share_link_access == "view"
|
|
assert p.visibility == "workspace"
|
|
assert p.shared_with == []
|
|
|
|
|
|
def test_pocket_visibility_values():
|
|
for v in ("private", "workspace", "public"):
|
|
p = Pocket.model_construct(workspace="w1", name="test", owner="u1", visibility=v)
|
|
assert p.visibility == v
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Invite
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_invite_has_revoked():
|
|
i = Invite.model_construct(workspace="w1", email="a@b.com", invited_by="u1", token="tok1")
|
|
assert i.revoked is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Workspace
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_workspace_has_deleted_at():
|
|
w = Workspace.model_construct(name="test", slug="test", owner="u1")
|
|
assert w.deleted_at is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Session
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_session_has_deleted_at():
|
|
s = Session.model_construct(sessionId="s1", workspace="w1", owner="u1")
|
|
assert s.deleted_at is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Notification
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_notification_has_expires_at():
|
|
n = Notification.model_construct(workspace="w1", recipient="u1", type="mention", title="test")
|
|
assert n.expires_at is None
|