Files
ServerKit/backend/tests/test_git_webhook_authz.py

82 lines
3.6 KiB
Python

"""Plan 29 Phase 4 (#11) — git-webhook READ reclassification.
Webhook/deploy reads move off panel-wide @viewer_required onto the linked app's
workspace visibility (can_access_app on the webhook's app_id). A foreign-workspace
viewer loses visibility of an app-linked webhook; a workspace member keeps it.
Webhooks with no app linkage stay panel-wide viewer-visible. Mutations stay
admin-only (explicitly re-deferred) and are not touched here.
"""
import pytest
def _mk_webhook(db, name, app_id=None):
from app.models import GitWebhook
w = GitWebhook(
name=name, source='github',
source_repo_url='https://example.com/r.git',
secret='s', webhook_token=f'tok-{name}',
app_id=app_id,
)
db.session.add(w)
db.session.commit()
return w
@pytest.fixture
def webhook_rbac(app, scoping_rbac):
"""One app-linked webhook (on scoping_rbac's app) + one unlinked webhook."""
from types import SimpleNamespace
from app import db
linked = _mk_webhook(db, 'linked', app_id=scoping_rbac.app_id)
unlinked = _mk_webhook(db, 'unlinked', app_id=None)
return SimpleNamespace(linked_id=linked.id, unlinked_id=unlinked.id, s=scoping_rbac)
def test_webhook_get_scoped_to_app_visibility(client, webhook_rbac):
"""The app-linked webhook is reachable by owner/member/viewer/admin of its
workspace, but a foreign caller gets a 404 (sealed-from-open, no leak)."""
s = webhook_rbac.s
url = f'/api/v1/git/webhooks/{webhook_rbac.linked_id}'
for persona in ('owner', 'member', 'viewer', 'admin'):
assert client.get(url, headers=getattr(s, persona)).status_code == 200, persona
assert client.get(url, headers=s.foreign).status_code == 404
def test_webhook_logs_scoped_to_app_visibility(client, webhook_rbac):
"""Webhook logs follow the same app-visibility seam as the webhook itself."""
s = webhook_rbac.s
url = f'/api/v1/git/webhooks/{webhook_rbac.linked_id}/logs'
assert client.get(url, headers=s.member).status_code == 200
assert client.get(url, headers=s.foreign).status_code == 404
def test_webhook_list_filters_to_visible(client, webhook_rbac):
"""The collection lists only webhooks the caller can see: a member sees the
app-linked + unlinked ones; a foreign caller sees only the unlinked one."""
s = webhook_rbac.s
member_ids = {w['id'] for w in client.get('/api/v1/git/webhooks', headers=s.member).get_json()['webhooks']}
assert webhook_rbac.linked_id in member_ids
assert webhook_rbac.unlinked_id in member_ids
foreign_ids = {w['id'] for w in client.get('/api/v1/git/webhooks', headers=s.foreign).get_json()['webhooks']}
assert webhook_rbac.linked_id not in foreign_ids # lost visibility of the app-linked hook
assert webhook_rbac.unlinked_id in foreign_ids # unlinked stays panel-wide visible
def test_webhook_unlinked_visible_to_all(client, webhook_rbac):
"""A webhook with no app linkage is a panel-wide resource — any authenticated
viewer (even a foreign one) can read it directly."""
s = webhook_rbac.s
url = f'/api/v1/git/webhooks/{webhook_rbac.unlinked_id}'
assert client.get(url, headers=s.foreign).status_code == 200
def test_webhook_mutations_stay_admin_only(client, webhook_rbac):
"""Deferred by design: mutations remain admin-only. A member (who can now READ
the linked webhook) still cannot delete or toggle it."""
s = webhook_rbac.s
wid = webhook_rbac.linked_id
assert client.delete(f'/api/v1/git/webhooks/{wid}', headers=s.member).status_code == 403
assert client.post(f'/api/v1/git/webhooks/{wid}/toggle', headers=s.member).status_code == 403