47 lines
1.4 KiB
Python
47 lines
1.4 KiB
Python
"""C4A — grants can expire
|
|
|
|
`user_roles.expires_at`, nullable. Null means permanent, which is every existing
|
|
row, so this migration changes nobody's authority.
|
|
|
|
Temporary elevation, contractor access and just-in-time grants all need this, and
|
|
all three are ordinary enterprise asks. Without it the only way to end a grant is
|
|
to remember to revoke it, and "remember to" is not an access control.
|
|
|
|
Expired rows are **not** deleted. They stay, and they become history — which is
|
|
what C4B reads to answer "who had access in March". A swept-away row answers
|
|
nothing.
|
|
|
|
Revision ID: c4_0_grant_expiry
|
|
Revises: c1_1_user_roles_tenant_id
|
|
"""
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision = "c4_0_grant_expiry"
|
|
down_revision = "c1_1_user_roles_tenant_id"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.add_column(
|
|
"user_roles",
|
|
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True),
|
|
)
|
|
|
|
# Partial: only live grants are ever queried by expiry, and indexing the
|
|
# permanent ones (which is all of them today) would be indexing a column
|
|
# that is null for every row.
|
|
op.create_index(
|
|
"ix_user_roles_expires_at",
|
|
"user_roles",
|
|
["expires_at"],
|
|
postgresql_where=sa.text("expires_at IS NOT NULL"),
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("ix_user_roles_expires_at", table_name="user_roles")
|
|
op.drop_column("user_roles", "expires_at")
|