mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-27 15:17:18 +09:00
82 lines
3.6 KiB
Python
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
|