slides: gate management on slides.manage permission (grantable to non-admin curator)
The lobby-display and screensaver slide manager was admin-only. Add a shared slides.manage permission so a curator can manage both surfaces without full admin. Admins keep access via the require_permission admin bypass. Backend: - plugins/slides/api/routes.py: all 5 management routes require slides.manage - plugins/slides/plugin.py: declare it via get_permissions(); nav item carries the permission so the frontend can gate visibility - shopdb/core/api/auth.py: login response now returns the user's permissions (matches /me) so the frontend authStore has them on fresh login Frontend: - stores/auth.js: hasPermission(name) getter (admin true, else granted list) - router/index.js: guard supports requiresPermission - views/AppLayout.vue: hide nav items whose permission the user lacks - plugins/slides/frontend/routes.js: slide manager gated requiresPermission Tests: no-perm user 403, curator role with the perm 200 (+ login advertises it), admin 200 via bypass. Deploy: run `flask seed permissions` to create the row, then grant it to a role in Settings > Users & Roles.
This commit is contained in:
@@ -23,3 +23,67 @@ def test_slides_feed_unknown_surface_falls_back(client, db):
|
||||
resp = client.get('/api/slides/feed?surface=../etc')
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()['surface'] == 'lobby'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Management routes are gated on the slides.manage permission (both surfaces).
|
||||
# A curator role holding it works without full admin; auth alone does not.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _headers_for(client, username, password):
|
||||
resp = client.post('/api/auth/login',
|
||||
json={'username': username, 'password': password})
|
||||
assert resp.status_code == 200, f'Login failed: {resp.get_json()}'
|
||||
return {'Authorization': f"Bearer {resp.get_json()['data']['access_token']}"}
|
||||
|
||||
|
||||
def test_slides_manage_requires_permission(client, db):
|
||||
"""Authenticated but permission-less user cannot list slides (403)."""
|
||||
from werkzeug.security import generate_password_hash
|
||||
from shopdb.core.models import User
|
||||
|
||||
user = User(username='plainuser', email='plain@test.local',
|
||||
passwordhash=generate_password_hash('testpass'))
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
resp = client.get('/api/slides/lobby',
|
||||
headers=_headers_for(client, 'plainuser', 'testpass'))
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_slides_manage_curator_allowed(client, db):
|
||||
"""A non-admin role holding slides.manage can reach the management routes."""
|
||||
from werkzeug.security import generate_password_hash
|
||||
from shopdb.core.models import User, Role, Permission
|
||||
|
||||
perm = Permission(name='slides.manage',
|
||||
description='Manage lobby display and screensaver slides',
|
||||
category='slides')
|
||||
role = Role(rolename='slidecurator', description='Slide curator')
|
||||
role.permissions.append(perm)
|
||||
db.session.add_all([perm, role])
|
||||
db.session.flush()
|
||||
|
||||
user = User(username='curator', email='curator@test.local',
|
||||
passwordhash=generate_password_hash('testpass'))
|
||||
user.roles.append(role)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
headers = _headers_for(client, 'curator', 'testpass')
|
||||
|
||||
# The login payload advertises the permission to the frontend authStore.
|
||||
login = client.post('/api/auth/login',
|
||||
json={'username': 'curator', 'password': 'testpass'})
|
||||
assert 'slides.manage' in login.get_json()['data']['user']['permissions']
|
||||
|
||||
# And the management route is reachable (200, not 403).
|
||||
resp = client.get('/api/slides/lobby', headers=headers)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_slides_manage_admin_allowed(client, db, auth_headers):
|
||||
"""Admin keeps access via the require_permission admin bypass."""
|
||||
resp = client.get('/api/slides/lobby', headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user