guest role; guide section ticking

This commit is contained in:
2026-08-27 13:02:35 -04:00
parent d38a599292
commit 4da6c6aa36
45 changed files with 830 additions and 243 deletions

View File

@@ -6,7 +6,8 @@ from werkzeug.security import generate_password_hash
from shopdb.extensions import db
from shopdb.core.models import (
User, Role, Permission, AuditLog, full_permission_catalog)
User, Role, Permission, AuditLog, full_permission_catalog,
guest_grantable_permissions)
from shopdb.utils.responses import success_response, error_response, ErrorCodes
from shopdb.utils.authz import require_role
@@ -266,6 +267,9 @@ def list_permissions():
def list_roles():
"""List all roles with their permissions."""
roles = Role.query.order_by(Role.rolename).all()
# What a guest MAY be granted, so the grid can grey out the rest rather
# than letting somebody tick a box the API will refuse.
guestgrantable = sorted(guest_grantable_permissions())
return success_response([{
'roleid': r.roleid,
'rolename': r.rolename,
@@ -273,7 +277,9 @@ def list_roles():
'color': r.color,
'usercount': r.users.count(),
'permissions': [p.name for p in r.permissions],
'isadmin': r.rolename == 'admin'
'isadmin': r.rolename == 'admin',
'isguest': r.rolename == Role.GUEST,
'grantable': guestgrantable if r.rolename == Role.GUEST else None,
} for r in roles])
@@ -344,6 +350,20 @@ def update_role(roleid: int):
# Update permissions
if 'permissions' in data and role.rolename != 'admin':
# The guest role is anyone with no account, so what it may hold is
# bounded: reads, plus the handful of writes a plugin has explicitly
# declared safe to do anonymously. Refused at the door rather than
# warned about - a checkbox that quietly makes deleting an asset
# anonymous is not a warning anybody reads twice.
if role.rolename == Role.GUEST:
allowed = guest_grantable_permissions()
refused = sorted(set(data['permissions']) - allowed)
if refused:
return error_response(
ErrorCodes.VALIDATION_ERROR,
'The guest role cannot hold {}. A guest may be granted any '
'view permission, and only writes a plugin has declared '
'safe to do without an account.'.format(', '.join(refused)))
old_perms = [p.name for p in role.permissions]
perms = Permission.query.filter(Permission.name.in_(data['permissions'])).all()
new_perms = [p.name for p in perms]
@@ -377,6 +397,14 @@ def delete_role(roleid: int):
if role.rolename == 'admin':
return error_response(ErrorCodes.VALIDATION_ERROR, 'Cannot delete the admin role')
# Deleting the guest role does not close the door, it removes the only
# place the door is described. Revoke its permissions instead.
if role.rolename == Role.GUEST:
return error_response(
ErrorCodes.VALIDATION_ERROR,
'Cannot delete the guest role. To close guest access, clear its '
'permissions.')
if role.users.count() > 0:
return error_response(ErrorCodes.VALIDATION_ERROR, f'Role is assigned to {role.users.count()} user(s)')