Add prebuilt user scopes and guard invalid Reporter grants
A newly created user held no roles and no permissions, so the sidebar filtered every nav item away and the account appeared broken. Adds three prebuilt scopes (Reporter, Operator, Technician) derived from the live permission catalog by verb rather than hand-listed, so a new plugin permission lands in the right scope automatically instead of silently going ungranted. users.view is granted to all three since a user who cannot see the directory cannot collaborate; users.create/edit/delete stay admin-only. Role.seedscopes() is additive and idempotent, seeded via flask seed permissions, and preserves custom grants on rerun. Reporter guard: the auth model is additive across admin, direct grants and roles, so Reporter cannot be expressed as merely fewer grants - a direct endpoint-action grant would silently re-arm a read-only account. Rather than change the global permission predicate and alter every route's semantics, _check_scope_grants() rejects the invalid combination at write time on both create and update. The update path validates the effective post-update state, so demoting a Technician to Reporter is refused while a stale endpoint grant remains rather than quietly leaving a read-only user with write authority. Validation runs before the User object is constructed. Assigning user.roles first put the relationship into the session autoflush path while the User was still unattached; nothing broke because the refusal returns before db.session.add(), but it left a trap where any query added below that point would autoflush a half-built user and surface later as an orphan row. Verified with -W error::sqlalchemy.exc.SAWarning: 25 passed.
This commit is contained in:
@@ -810,7 +810,7 @@ def fix_controls_direction():
|
||||
@seed_cli.command('permissions')
|
||||
@with_appcontext
|
||||
def seed_permissions():
|
||||
"""Seed predefined permissions, and the guest role that can hold them."""
|
||||
"""Seed predefined permissions, the guest role, and the prebuilt scopes."""
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Permission, Role
|
||||
|
||||
@@ -819,11 +819,23 @@ def seed_permissions():
|
||||
# on the upgrade it already runs. It holds nothing until somebody grants
|
||||
# it something, so seeding it changes no behaviour.
|
||||
guest = Role.seedguest()
|
||||
# AFTER Permission.seed(), which is load-bearing: seedscopes only links
|
||||
# permissions that exist as rows, so seeding the catalog first is what
|
||||
# lets a newly installed plugin's permissions reach the scopes in the
|
||||
# same command rather than on the next run.
|
||||
scopes = Role.seedscopes()
|
||||
db.session.commit()
|
||||
click.echo(click.style(f"{created} permissions created.", fg='green'))
|
||||
if guest:
|
||||
click.echo(click.style(
|
||||
"guest role created (holds no permissions).", fg='green'))
|
||||
for rolename in scopes['created']:
|
||||
click.echo(click.style(f"{rolename} role created.", fg='green'))
|
||||
for rolename, count in sorted(scopes['granted'].items()):
|
||||
click.echo(click.style(
|
||||
f"{rolename}: {count} permission(s) granted.", fg='green'))
|
||||
if not scopes['created'] and not scopes['granted']:
|
||||
click.echo("Prebuilt scopes already up to date.")
|
||||
|
||||
|
||||
@seed_cli.command('settings')
|
||||
|
||||
Reference in New Issue
Block a user