"""Anmeldung ueber einen fremden Dienst (OpenID Connect). Drei Aenderungen: ``user_identities`` welches Konto bei welchem Anbieter zu welchem Nutzer gehoert. Eigene Tabelle, weil jemand mehrere haben kann. ``oidc_states`` begonnene Anmeldeversuche, bis der Nutzer zurueckkommt. ``users`` ein Passwort-Hash, der leer sein DARF – ein reiner SSO-Nutzer hat nie eines gesetzt. Bewusst KEINE E-Mail-Spalte am Nutzer: Ueber die E-Mail automatisch zusammenzufuehren waere der bequeme Weg und der gefaehrliche. Wer beim Anbieter eine fremde Adresse eintragen kann, uebernaehme damit ein fremdes Konto. Verknuepft wird ausdruecklich, im angemeldeten Zustand. Revision ID: 0033 Revises: 0032 """ import sqlalchemy as sa from alembic import op revision = "0033" down_revision = "0032" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "user_identities", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), sa.Column("provider", sa.String(length=32), nullable=False), sa.Column("subject", sa.String(length=255), nullable=False), sa.Column("email", sa.String(length=255), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), sa.Column("last_login_at", sa.DateTime(timezone=True), nullable=True), # Eine Kennung gehoert zu genau EINEM Nutzer – sonst landete derselbe # Anmeldende mal hier und mal dort. sa.UniqueConstraint("provider", "subject", name="uq_identity_provider_subject"), ) op.create_index("ix_user_identities_user_id", "user_identities", ["user_id"]) op.create_table( "oidc_states", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("state", sa.String(length=64), nullable=False, unique=True), sa.Column("provider", sa.String(length=32), nullable=False), sa.Column("nonce", sa.String(length=64), nullable=False), sa.Column("code_verifier", sa.String(length=128), nullable=False), sa.Column("redirect_uri", sa.String(length=500), nullable=False), sa.Column("link_user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=True), sa.Column("weiter", sa.String(length=255), nullable=True), sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), ) op.create_index("ix_oidc_states_state", "oidc_states", ["state"]) # password_hash darf jetzt leer sein. Das geht auf SQLite nur ueber einen # Tabellen-Umbau; migrations/env.py schaltet die Fremdschluessel dafuer ab, # sonst risse das implizite DELETE beim DROP saemtliche Daten des Nutzers # mit (siehe tests/test_migrations.py). with op.batch_alter_table("users") as batch: batch.alter_column("password_hash", existing_type=sa.String(length=255), nullable=True) def downgrade() -> None: with op.batch_alter_table("users") as batch: batch.alter_column("password_hash", existing_type=sa.String(length=255), nullable=False) op.drop_index("ix_oidc_states_state", table_name="oidc_states") op.drop_table("oidc_states") op.drop_index("ix_user_identities_user_id", table_name="user_identities") op.drop_table("user_identities")