"""Per-user direct permission grants. Permissions were reachable only through roles. At 5-7 operators per instance that forces a role to be minted for every one-off capability - "Bob, and only Bob, may push scripts" - until the role list is a mixture of job titles and single-capability flags and nobody can tell which is which. A row here is an ORDINARY permission grant. It is identical in power to one held through a role: it counts toward the API token scope ceiling, it is folded by both User.haspermission() and User.getpermissions(), and it is revoked the same way. There is deliberately no second class of permission. Nothing is granted by this migration. The table is created empty; existing authority is untouched. Revision ID: 7d43_userpermissions Revises: 7d42_split_endpoints_deploy """ from alembic import op import sqlalchemy as sa revision = '7d43_userpermissions' down_revision = '7d42_split_endpoints_deploy' branch_labels = None depends_on = None def upgrade(): bind = op.get_bind() inspector = sa.inspect(bind) if 'userpermissions' in set(inspector.get_table_names()): return op.create_table( 'userpermissions', sa.Column('userid', sa.Integer(), nullable=False), sa.Column('permissionid', sa.Integer(), nullable=False), # Cascade on both sides: a direct grant is meaningless once either end # is gone, and leaving orphans would let a recycled id silently inherit # someone else's capability. sa.ForeignKeyConstraint(['userid'], ['users.userid'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['permissionid'], ['permissions.permissionid'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('userid', 'permissionid'), ) op.create_index('idx_userpermissions_userid', 'userpermissions', ['userid']) def downgrade(): bind = op.get_bind() inspector = sa.inspect(bind) if 'userpermissions' not in set(inspector.get_table_names()): return op.drop_index('idx_userpermissions_userid', table_name='userpermissions') op.drop_table('userpermissions')