"""API token permission scopes (apitokens.scopes) Adds a nullable scopes column to apitokens: a JSON array of permission-name strings. NULL means the token is unscoped and acts with the full authority of its owner (the original behavior). A scoped token grants ONLY the listed permissions, intersected with what the owner actually holds, and suspends the admin-role bypass so a scoped token minted by an admin is genuinely limited. Idempotent guard so it is safe on a partially-migrated box; real downgrade. Revision ID: 7d22_apitokens_scopes Revises: 7d21_apitokens Create Date: 2026-07-12 """ from alembic import op import sqlalchemy as sa revision = '7d22_apitokens_scopes' down_revision = '7d21_apitokens' branch_labels = None depends_on = None def upgrade(): bind = op.get_bind() insp = sa.inspect(bind) if 'apitokens' not in insp.get_table_names(): return columns = {c['name'] for c in insp.get_columns('apitokens')} if 'scopes' in columns: return op.add_column('apitokens', sa.Column('scopes', sa.Text(), nullable=True)) def downgrade(): bind = op.get_bind() insp = sa.inspect(bind) if 'apitokens' not in insp.get_table_names(): return columns = {c['name'] for c in insp.get_columns('apitokens')} if 'scopes' not in columns: return op.drop_column('apitokens', 'scopes')