mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-03 18:46:47 +09:00
Clusters E+H of plan 38 (plans 24/29/33). - security_policy_service: require-2FA policy (ok/nudge/enforce), SSO-exempt, grace window (plan 29); restored can_operate_app/can_admin_app on resource_grant_service for the member-action callers — 23 tests green - notifications/digest: per-user daily/weekly digest rollups over queued_digest deliveries, respecting cadence + last_sent_at (plans 24/33) - notifications/service: deep-link action_path/action_label on send + inbox category/severity filters (plan 24 Phase 5); catalog link builder - chat_webhook_service: org chat/webhook connections, HMAC-signed delivery, category fan-out through the notify bus (plan 33) - restored digest_cadence/digest_last_sent_at on NotificationPreferences Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
4.8 KiB
Python
138 lines
4.8 KiB
Python
"""Proving matrix for the per-app capability tiers (plan 29 / plan 19).
|
|
|
|
Locks the four-rung access family down against the single tier seam in
|
|
``rbac.app_access_tier`` ('viewer' < 'member' < 'admin' < 'owner'):
|
|
|
|
can_access_app — read (owner/admin/any grant)
|
|
can_operate_app — member+ (owner, panel admin, editor grant, ws member+)
|
|
can_edit_app — editor grant (or owner/admin)
|
|
can_admin_app — admin+ (owner, panel admin, ws admin/owner)
|
|
|
|
``can_operate_app`` / ``can_admin_app`` were lost in the recovery and are
|
|
reconstructed here (a surviving caller, member_action_service, depends on them).
|
|
"""
|
|
from werkzeug.security import generate_password_hash
|
|
|
|
from app import db
|
|
from app.models import Application, User
|
|
from app.models.workspace import Workspace, WorkspaceMember
|
|
from app.middleware.rbac import app_access_tier, _APP_ROLE_RANK
|
|
from app.services.resource_grant_service import ResourceGrantService as RGS
|
|
|
|
|
|
def _user(username, role='developer'):
|
|
u = User(email=f'{username}@t.local', username=username,
|
|
password_hash=generate_password_hash('x'), role=role, is_active=True)
|
|
db.session.add(u)
|
|
db.session.commit()
|
|
return u
|
|
|
|
|
|
def _app(owner, name, workspace_id=None):
|
|
a = Application(name=name, app_type='php', user_id=owner.id, workspace_id=workspace_id)
|
|
db.session.add(a)
|
|
db.session.commit()
|
|
return a
|
|
|
|
|
|
def _workspace(slug):
|
|
ws = Workspace(name=slug, slug=slug)
|
|
db.session.add(ws)
|
|
db.session.commit()
|
|
return ws
|
|
|
|
|
|
def _member(ws, user, role):
|
|
db.session.add(WorkspaceMember(workspace_id=ws.id, user_id=user.id, role=role))
|
|
db.session.commit()
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Grant-based personas
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_owner_reaches_every_tier(app):
|
|
owner = _user('s_owner')
|
|
a = _app(owner, 's-owner-app')
|
|
assert app_access_tier(owner, a) == 'owner'
|
|
assert RGS.can_access_app(owner, a)
|
|
assert RGS.can_operate_app(owner, a)
|
|
assert RGS.can_edit_app(owner, a)
|
|
assert RGS.can_admin_app(owner, a)
|
|
|
|
|
|
def test_panel_admin_reaches_every_tier(app):
|
|
owner = _user('s_o2')
|
|
admin = _user('s_admin', role='admin')
|
|
a = _app(owner, 's-admin-app')
|
|
assert admin.is_admin
|
|
assert app_access_tier(admin, a) == 'owner'
|
|
assert RGS.can_operate_app(admin, a)
|
|
assert RGS.can_admin_app(admin, a)
|
|
|
|
|
|
def test_editor_grant_operates_but_not_admins(app):
|
|
owner = _user('s_o3')
|
|
editor = _user('s_editor')
|
|
a = _app(owner, 's-editor-app')
|
|
RGS.grant(editor.id, 'application', a.id, granted_by=owner.id, role='editor')
|
|
assert app_access_tier(editor, a) == 'member'
|
|
assert RGS.can_access_app(editor, a)
|
|
assert RGS.can_operate_app(editor, a)
|
|
assert RGS.can_edit_app(editor, a)
|
|
assert not RGS.can_admin_app(editor, a)
|
|
|
|
|
|
def test_viewer_grant_is_read_only(app):
|
|
owner = _user('s_o4')
|
|
viewer = _user('s_viewer')
|
|
a = _app(owner, 's-viewer-app')
|
|
RGS.grant(viewer.id, 'application', a.id, granted_by=owner.id, role='viewer')
|
|
assert app_access_tier(viewer, a) == 'viewer'
|
|
assert RGS.can_access_app(viewer, a)
|
|
assert not RGS.can_operate_app(viewer, a)
|
|
assert not RGS.can_edit_app(viewer, a)
|
|
assert not RGS.can_admin_app(viewer, a)
|
|
|
|
|
|
def test_stranger_reaches_nothing(app):
|
|
owner = _user('s_o5')
|
|
stranger = _user('s_stranger')
|
|
a = _app(owner, 's-stranger-app')
|
|
assert app_access_tier(stranger, a) is None
|
|
assert not RGS.can_access_app(stranger, a)
|
|
assert not RGS.can_operate_app(stranger, a)
|
|
assert not RGS.can_edit_app(stranger, a)
|
|
assert not RGS.can_admin_app(stranger, a)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Workspace-membership personas (widen operate/admin, not grants)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_workspace_member_can_operate_not_admin(app):
|
|
owner = _user('w_owner')
|
|
ws = _workspace('ws-ops')
|
|
member = _user('w_member')
|
|
_member(ws, member, WorkspaceMember.ROLE_MEMBER)
|
|
a = _app(owner, 'w-ops-app', workspace_id=ws.id)
|
|
assert app_access_tier(member, a) == 'member'
|
|
assert RGS.can_operate_app(member, a)
|
|
assert not RGS.can_admin_app(member, a)
|
|
|
|
|
|
def test_workspace_admin_can_admin(app):
|
|
owner = _user('w_owner2')
|
|
ws = _workspace('ws-adm')
|
|
wsadmin = _user('w_admin')
|
|
_member(ws, wsadmin, WorkspaceMember.ROLE_ADMIN)
|
|
a = _app(owner, 'w-adm-app', workspace_id=ws.id)
|
|
assert app_access_tier(wsadmin, a) == 'admin'
|
|
assert RGS.can_operate_app(wsadmin, a)
|
|
assert RGS.can_admin_app(wsadmin, a)
|
|
|
|
|
|
def test_tier_rank_ordering_is_consistent(app):
|
|
assert (_APP_ROLE_RANK['viewer'] < _APP_ROLE_RANK['member']
|
|
< _APP_ROLE_RANK['admin'] < _APP_ROLE_RANK['owner'])
|