Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • Dockerfile
  • feature_invite_validuntil_minmax
  • incremental-sync
  • jwt_encode_inconsistencies
  • master
  • redis-rate-limits
  • roles-recursive-cte
  • typehints
  • v1.0.x
  • v1.1.x
  • v1.2.x
  • v1.x.x
  • v0.1.2
  • v0.1.4
  • v0.1.5
  • v0.2.0
  • v0.3.0
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.1.0
  • v1.1.1
  • v1.1.2
  • v1.2.0
  • v2.0.0
  • v2.0.1
  • v2.1.0
  • v2.2.0
  • v2.3.0
  • v2.3.1
30 results

Target

Select target project
  • uffd/uffd
  • rixx/uffd
  • thies/uffd
  • leona/uffd
  • enbewe/uffd
  • strifel/uffd
  • thies/uffd-2
7 results
Select Git revision
  • Dockerfile
  • claims-in-idtoke
  • feature_invite_validuntil_minmax
  • incremental-sync
  • jwt_encode_inconsistencies
  • master
  • recovery-code-pwhash
  • redis-rate-limits
  • roles-recursive-cte
  • typehints
  • v1.0.x
  • v1.1.x
  • v1.2.x
  • v1.x.x
  • v0.1.2
  • v0.1.4
  • v0.1.5
  • v0.2.0
  • v0.3.0
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.1.0
  • v1.1.1
  • v1.1.2
  • v1.2.0
  • v2.0.0
  • v2.0.1
  • v2.1.0
  • v2.2.0
  • v2.3.0
  • v2.3.1
32 results
Show changes
Showing
with 467 additions and 107 deletions
"""Add mfa_method.totp_last_counter
Revision ID: a9b449776953
Revises: 23293f32b503
Create Date: 2023-11-07 12:09:23.843865
"""
from alembic import op
import sqlalchemy as sa
revision = 'a9b449776953'
down_revision = '23293f32b503'
branch_labels = None
depends_on = None
def upgrade():
meta = sa.MetaData(bind=op.get_bind())
mfa_method = sa.Table('mfa_method', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('type', sa.Enum('RECOVERY_CODE', 'TOTP', 'WEBAUTHN', create_constraint=True, name='ck_mfa_method_type'), nullable=False),
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('recovery_salt', sa.String(length=64), nullable=True),
sa.Column('recovery_hash', sa.String(length=256), nullable=True),
sa.Column('totp_key', sa.String(length=64), nullable=True),
sa.Column('webauthn_cred', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_mfa_method_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_mfa_method'))
)
with op.batch_alter_table('mfa_method', copy_from=mfa_method) as batch_op:
batch_op.add_column(sa.Column('totp_last_counter', sa.Integer(), nullable=True))
def downgrade():
meta = sa.MetaData(bind=op.get_bind())
mfa_method = sa.Table('mfa_method', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('type', sa.Enum('RECOVERY_CODE', 'TOTP', 'WEBAUTHN', create_constraint=True, name='ck_mfa_method_type'), nullable=False),
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('recovery_salt', sa.String(length=64), nullable=True),
sa.Column('recovery_hash', sa.String(length=256), nullable=True),
sa.Column('totp_key', sa.String(length=64), nullable=True),
sa.Column('totp_last_counter', sa.Integer(), nullable=True),
sa.Column('webauthn_cred', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_mfa_method_user_id_user'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_mfa_method'))
)
with op.batch_alter_table('mfa_method', copy_from=mfa_method) as batch_op:
batch_op.drop_column('totp_last_counter')
...@@ -26,7 +26,7 @@ def upgrade(): ...@@ -26,7 +26,7 @@ def upgrade():
sa.Column('primary_email_id', sa.Integer(), nullable=False), sa.Column('primary_email_id', sa.Integer(), nullable=False),
sa.Column('recovery_email_id', sa.Integer(), nullable=True), sa.Column('recovery_email_id', sa.Integer(), nullable=True),
sa.Column('pwhash', sa.Text(), nullable=True), sa.Column('pwhash', sa.Text(), nullable=True),
sa.Column('is_service_user', sa.Boolean(), nullable=False), sa.Column('is_service_user', sa.Boolean(create_constraint=True), nullable=False),
sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'), sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'),
sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'), sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_user')), sa.PrimaryKeyConstraint('id', name=op.f('pk_user')),
...@@ -60,17 +60,21 @@ def upgrade(): ...@@ -60,17 +60,21 @@ def upgrade():
sa.select([sa.func.max(user_table.c.unix_uid)]) sa.select([sa.func.max(user_table.c.unix_uid)])
.where(user_table.c.unix_uid <= current_app.config['USER_MAX_UID']) .where(user_table.c.unix_uid <= current_app.config['USER_MAX_UID'])
).scalar() or 0 ).scalar() or 0
insert_data = []
if max_user_uid: if max_user_uid:
for uid in range(current_app.config['USER_MIN_UID'], max_user_uid + 1): for uid in range(current_app.config['USER_MIN_UID'], max_user_uid + 1):
conn.execute(sa.insert(uid_allocation_table).values(id=uid)) insert_data.append({'id': uid})
op.bulk_insert(uid_allocation_table, insert_data)
max_service_uid = conn.execute( max_service_uid = conn.execute(
sa.select([sa.func.max(user_table.c.unix_uid)]) sa.select([sa.func.max(user_table.c.unix_uid)])
.where(user_table.c.unix_uid <= current_app.config['USER_SERVICE_MAX_UID']) .where(user_table.c.unix_uid <= current_app.config['USER_SERVICE_MAX_UID'])
).scalar() or 0 ).scalar() or 0
insert_data = []
if max_service_uid: if max_service_uid:
for uid in range(current_app.config['USER_SERVICE_MIN_UID'], max_service_uid + 1): for uid in range(current_app.config['USER_SERVICE_MIN_UID'], max_service_uid + 1):
if uid < current_app.config['USER_MIN_UID'] or uid > max_user_uid: if uid < current_app.config['USER_MIN_UID'] or uid > max_user_uid:
conn.execute(sa.insert(uid_allocation_table).values(id=uid)) insert_data.append({'id': uid})
op.bulk_insert(uid_allocation_table, insert_data)
# Also block all UIDs outside of both ranges that are in use # Also block all UIDs outside of both ranges that are in use
# (just to be sure, there should not be any) # (just to be sure, there should not be any)
conn.execute(sa.insert(uid_allocation_table).from_select(['id'], conn.execute(sa.insert(uid_allocation_table).from_select(['id'],
...@@ -105,9 +109,11 @@ def upgrade(): ...@@ -105,9 +109,11 @@ def upgrade():
sa.select([sa.func.max(group_table.c.unix_gid)]) sa.select([sa.func.max(group_table.c.unix_gid)])
.where(group_table.c.unix_gid <= current_app.config['GROUP_MAX_GID']) .where(group_table.c.unix_gid <= current_app.config['GROUP_MAX_GID'])
).scalar() or 0 ).scalar() or 0
insert_data = []
if max_group_gid: if max_group_gid:
for gid in range(current_app.config['GROUP_MIN_GID'], max_group_gid + 1): for gid in range(current_app.config['GROUP_MIN_GID'], max_group_gid + 1):
conn.execute(sa.insert(gid_allocation_table).values(id=gid)) insert_data.append({'id': gid})
op.bulk_insert(gid_allocation_table, insert_data)
# Also block out-of-range GIDs # Also block out-of-range GIDs
conn.execute(sa.insert(gid_allocation_table).from_select(['id'], conn.execute(sa.insert(gid_allocation_table).from_select(['id'],
sa.select([group_table.c.unix_gid]).where( sa.select([group_table.c.unix_gid]).where(
...@@ -131,7 +137,7 @@ def downgrade(): ...@@ -131,7 +137,7 @@ def downgrade():
sa.Column('primary_email_id', sa.Integer(), nullable=False), sa.Column('primary_email_id', sa.Integer(), nullable=False),
sa.Column('recovery_email_id', sa.Integer(), nullable=True), sa.Column('recovery_email_id', sa.Integer(), nullable=True),
sa.Column('pwhash', sa.Text(), nullable=True), sa.Column('pwhash', sa.Text(), nullable=True),
sa.Column('is_service_user', sa.Boolean(), nullable=False), sa.Column('is_service_user', sa.Boolean(create_constraint=True), nullable=False),
sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'), sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'),
sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'), sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['unix_uid'], ['uid_allocation.id'], name=op.f('fk_user_unix_uid_uid_allocation')), sa.ForeignKeyConstraint(['unix_uid'], ['uid_allocation.id'], name=op.f('fk_user_unix_uid_uid_allocation')),
......
...@@ -37,7 +37,7 @@ def upgrade(): ...@@ -37,7 +37,7 @@ def upgrade():
sa.Column('displayname', sa.String(length=128), nullable=False), sa.Column('displayname', sa.String(length=128), nullable=False),
sa.Column('mail', sa.String(length=128), nullable=False), sa.Column('mail', sa.String(length=128), nullable=False),
sa.Column('pwhash', sa.String(length=256), nullable=True), sa.Column('pwhash', sa.String(length=256), nullable=True),
sa.Column('is_service_user', sa.Boolean(), nullable=False), sa.Column('is_service_user', sa.Boolean(create_constraint=True), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_user')), sa.PrimaryKeyConstraint('id', name=op.f('pk_user')),
sa.UniqueConstraint('loginname', name=op.f('uq_user_loginname')), sa.UniqueConstraint('loginname', name=op.f('uq_user_loginname')),
sa.UniqueConstraint('unix_uid', name=op.f('uq_user_unix_uid')) sa.UniqueConstraint('unix_uid', name=op.f('uq_user_unix_uid'))
...@@ -71,7 +71,7 @@ def downgrade(): ...@@ -71,7 +71,7 @@ def downgrade():
sa.Column('displayname', sa.String(length=128), nullable=False), sa.Column('displayname', sa.String(length=128), nullable=False),
sa.Column('mail', sa.String(length=128), nullable=False), sa.Column('mail', sa.String(length=128), nullable=False),
sa.Column('pwhash', sa.Text(), nullable=True), sa.Column('pwhash', sa.Text(), nullable=True),
sa.Column('is_service_user', sa.Boolean(), nullable=False), sa.Column('is_service_user', sa.Boolean(create_constraint=True), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_user')), sa.PrimaryKeyConstraint('id', name=op.f('pk_user')),
sa.UniqueConstraint('loginname', name=op.f('uq_user_loginname')), sa.UniqueConstraint('loginname', name=op.f('uq_user_loginname')),
sa.UniqueConstraint('unix_uid', name=op.f('uq_user_unix_uid')) sa.UniqueConstraint('unix_uid', name=op.f('uq_user_unix_uid'))
......
...@@ -16,8 +16,22 @@ depends_on = None ...@@ -16,8 +16,22 @@ depends_on = None
def upgrade(): def upgrade():
with op.batch_alter_table('role', schema=None) as batch_op: with op.batch_alter_table('role', schema=None) as batch_op:
batch_op.add_column(sa.Column('is_default', sa.Boolean(name=op.f('ck_role_is_default')), nullable=False, default=False)) batch_op.add_column(sa.Column('is_default', sa.Boolean(create_constraint=True, name=op.f('ck_role_is_default')), nullable=False, default=False))
def downgrade(): def downgrade():
with op.batch_alter_table('role', schema=None) as batch_op: meta = sa.MetaData(bind=op.get_bind())
table = sa.Table('role', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=32), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('moderator_group_dn', sa.String(length=128), nullable=True),
sa.Column('locked', sa.Boolean(create_constraint=False), nullable=False),
sa.Column('is_default', sa.Boolean(create_constraint=False), nullable=False),
sa.CheckConstraint('locked IN (0, 1)', name=op.f('ck_role_locked')),
sa.CheckConstraint('is_default IN (0, 1)', name=op.f('ck_role_is_default')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_role')),
sa.UniqueConstraint('name', name=op.f('uq_role_name'))
)
with op.batch_alter_table('role', copy_from=table) as batch_op:
batch_op.drop_constraint(op.f('ck_role_is_default'), 'check')
batch_op.drop_column('is_default') batch_op.drop_column('is_default')
...@@ -36,7 +36,7 @@ def upgrade(): ...@@ -36,7 +36,7 @@ def upgrade():
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('address', sa.String(length=128), nullable=False), sa.Column('address', sa.String(length=128), nullable=False),
sa.Column('verified', sa.Boolean(), nullable=False), sa.Column('verified', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('verification_legacy_id', sa.Integer(), nullable=True), sa.Column('verification_legacy_id', sa.Integer(), nullable=True),
sa.Column('verification_secret', sa.Text(), nullable=True), sa.Column('verification_secret', sa.Text(), nullable=True),
sa.Column('verification_expires', sa.DateTime(), nullable=True), sa.Column('verification_expires', sa.DateTime(), nullable=True),
...@@ -50,7 +50,7 @@ def upgrade(): ...@@ -50,7 +50,7 @@ def upgrade():
) )
op.execute(user_email_table.insert().from_select( op.execute(user_email_table.insert().from_select(
['user_id', 'address', 'verified'], ['user_id', 'address', 'verified'],
sa.select([user_table.c.id, user_table.c.mail, sa.literal(True, sa.Boolean())]) sa.select([user_table.c.id, user_table.c.mail, sa.literal(True, sa.Boolean(create_constraint=True))])
)) ))
with op.batch_alter_table('user', schema=None) as batch_op: with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('primary_email_id', sa.Integer(), nullable=True)) batch_op.add_column(sa.Column('primary_email_id', sa.Integer(), nullable=True))
...@@ -67,7 +67,7 @@ def upgrade(): ...@@ -67,7 +67,7 @@ def upgrade():
sa.Column('primary_email_id', sa.Integer(), nullable=True), sa.Column('primary_email_id', sa.Integer(), nullable=True),
sa.Column('recovery_email_id', sa.Integer(), nullable=True), sa.Column('recovery_email_id', sa.Integer(), nullable=True),
sa.Column('pwhash', sa.Text(), nullable=True), sa.Column('pwhash', sa.Text(), nullable=True),
sa.Column('is_service_user', sa.Boolean(), nullable=False), sa.Column('is_service_user', sa.Boolean(create_constraint=True), nullable=False),
sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'), sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'),
sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'), sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_user')), sa.PrimaryKeyConstraint('id', name=op.f('pk_user')),
...@@ -110,7 +110,7 @@ def downgrade(): ...@@ -110,7 +110,7 @@ def downgrade():
sa.Column('primary_email_id', sa.Integer(), nullable=False), sa.Column('primary_email_id', sa.Integer(), nullable=False),
sa.Column('recovery_email_id', sa.Integer(), nullable=True), sa.Column('recovery_email_id', sa.Integer(), nullable=True),
sa.Column('pwhash', sa.Text(), nullable=True), sa.Column('pwhash', sa.Text(), nullable=True),
sa.Column('is_service_user', sa.Boolean(), nullable=False), sa.Column('is_service_user', sa.Boolean(create_constraint=True), nullable=False),
sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'), sa.ForeignKeyConstraint(['primary_email_id'], ['user_email.id'], name=op.f('fk_user_primary_email_id_user_email'), onupdate='CASCADE'),
sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'), sa.ForeignKeyConstraint(['recovery_email_id'], ['user_email.id'], name=op.f('fk_user_recovery_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_user')), sa.PrimaryKeyConstraint('id', name=op.f('pk_user')),
......
...@@ -20,16 +20,16 @@ def upgrade(): ...@@ -20,16 +20,16 @@ def upgrade():
sa.Column('service_id', sa.Integer(), nullable=False), sa.Column('service_id', sa.Integer(), nullable=False),
sa.Column('auth_username', sa.String(length=40), nullable=False), sa.Column('auth_username', sa.String(length=40), nullable=False),
sa.Column('auth_password', sa.Text(), nullable=False), sa.Column('auth_password', sa.Text(), nullable=False),
sa.Column('perm_users', sa.Boolean(), nullable=False), sa.Column('perm_users', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_checkpassword', sa.Boolean(), nullable=False), sa.Column('perm_checkpassword', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_mail_aliases', sa.Boolean(), nullable=False), sa.Column('perm_mail_aliases', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_remailer', sa.Boolean(), nullable=False), sa.Column('perm_remailer', sa.Boolean(create_constraint=True), nullable=False),
sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_api_client_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'), sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_api_client_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_api_client')), sa.PrimaryKeyConstraint('id', name=op.f('pk_api_client')),
sa.UniqueConstraint('auth_username', name=op.f('uq_api_client_auth_username')) sa.UniqueConstraint('auth_username', name=op.f('uq_api_client_auth_username'))
) )
with op.batch_alter_table('api_client', copy_from=api_client) as batch_op: with op.batch_alter_table('api_client', copy_from=api_client) as batch_op:
batch_op.add_column(sa.Column('perm_metrics', sa.Boolean(), nullable=False, server_default=sa.false())) batch_op.add_column(sa.Column('perm_metrics', sa.Boolean(create_constraint=True), nullable=False, server_default=sa.false()))
def downgrade(): def downgrade():
meta = sa.MetaData(bind=op.get_bind()) meta = sa.MetaData(bind=op.get_bind())
...@@ -38,11 +38,11 @@ def downgrade(): ...@@ -38,11 +38,11 @@ def downgrade():
sa.Column('service_id', sa.Integer(), nullable=False), sa.Column('service_id', sa.Integer(), nullable=False),
sa.Column('auth_username', sa.String(length=40), nullable=False), sa.Column('auth_username', sa.String(length=40), nullable=False),
sa.Column('auth_password', sa.Text(), nullable=False), sa.Column('auth_password', sa.Text(), nullable=False),
sa.Column('perm_users', sa.Boolean(), nullable=False), sa.Column('perm_users', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_checkpassword', sa.Boolean(), nullable=False), sa.Column('perm_checkpassword', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_mail_aliases', sa.Boolean(), nullable=False), sa.Column('perm_mail_aliases', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_remailer', sa.Boolean(), nullable=False), sa.Column('perm_remailer', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_metrics', sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column('perm_metrics', sa.Boolean(create_constraint=True), nullable=False, server_default=sa.false()),
sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_api_client_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'), sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_api_client_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_api_client')), sa.PrimaryKeyConstraint('id', name=op.f('pk_api_client')),
sa.UniqueConstraint('auth_username', name=op.f('uq_api_client_auth_username')) sa.UniqueConstraint('auth_username', name=op.f('uq_api_client_auth_username'))
......
...@@ -99,7 +99,7 @@ def upgrade(): ...@@ -99,7 +99,7 @@ def upgrade():
service_table = op.create_table('service', service_table = op.create_table('service',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('limit_access', sa.Boolean(), nullable=False), sa.Column('limit_access', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('access_group_id', sa.Integer(), nullable=True), sa.Column('access_group_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['access_group_id'], ['group.id'], name=op.f('fk_service_access_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'), sa.ForeignKeyConstraint(['access_group_id'], ['group.id'], name=op.f('fk_service_access_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_service')), sa.PrimaryKeyConstraint('id', name=op.f('pk_service')),
...@@ -118,9 +118,9 @@ def upgrade(): ...@@ -118,9 +118,9 @@ def upgrade():
sa.Column('service_id', sa.Integer(), nullable=False), sa.Column('service_id', sa.Integer(), nullable=False),
sa.Column('auth_username', sa.String(length=40), nullable=False), sa.Column('auth_username', sa.String(length=40), nullable=False),
sa.Column('auth_password', sa.Text(), nullable=False), sa.Column('auth_password', sa.Text(), nullable=False),
sa.Column('perm_users', sa.Boolean(), nullable=False), sa.Column('perm_users', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_checkpassword', sa.Boolean(), nullable=False), sa.Column('perm_checkpassword', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('perm_mail_aliases', sa.Boolean(), nullable=False), sa.Column('perm_mail_aliases', sa.Boolean(create_constraint=True), nullable=False),
sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_api_client_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'), sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_api_client_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_api_client')), sa.PrimaryKeyConstraint('id', name=op.f('pk_api_client')),
sa.UniqueConstraint('auth_username', name=op.f('uq_api_client_auth_username')) sa.UniqueConstraint('auth_username', name=op.f('uq_api_client_auth_username'))
...@@ -164,7 +164,7 @@ def upgrade(): ...@@ -164,7 +164,7 @@ def upgrade():
batch_op.create_foreign_key(batch_op.f('fk_device_login_initiation_oauth2_client_db_id_oauth2client'), 'oauth2client', ['oauth2_client_db_id'], ['db_id'], onupdate='CASCADE', ondelete='CASCADE') batch_op.create_foreign_key(batch_op.f('fk_device_login_initiation_oauth2_client_db_id_oauth2client'), 'oauth2client', ['oauth2_client_db_id'], ['db_id'], onupdate='CASCADE', ondelete='CASCADE')
device_login_initiation_table = sa.Table('device_login_initiation', meta, device_login_initiation_table = sa.Table('device_login_initiation', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('type', sa.Enum('OAUTH2', name='devicelogintype'), nullable=False), sa.Column('type', sa.Enum('OAUTH2', create_constraint=True, name='devicelogintype'), nullable=False),
sa.Column('code0', sa.String(length=32), nullable=False), sa.Column('code0', sa.String(length=32), nullable=False),
sa.Column('code1', sa.String(length=32), nullable=False), sa.Column('code1', sa.String(length=32), nullable=False),
sa.Column('secret', sa.String(length=128), nullable=False), sa.Column('secret', sa.String(length=128), nullable=False),
...@@ -293,7 +293,7 @@ def downgrade(): ...@@ -293,7 +293,7 @@ def downgrade():
batch_op.add_column(sa.Column('oauth2_client_id', sa.VARCHAR(length=40), nullable=True)) batch_op.add_column(sa.Column('oauth2_client_id', sa.VARCHAR(length=40), nullable=True))
device_login_initiation_table = sa.Table('device_login_initiation', meta, device_login_initiation_table = sa.Table('device_login_initiation', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('type', sa.Enum('OAUTH2', name='devicelogintype'), nullable=False), sa.Column('type', sa.Enum('OAUTH2', create_constraint=True, name='devicelogintype'), nullable=False),
sa.Column('code0', sa.String(length=32), nullable=False), sa.Column('code0', sa.String(length=32), nullable=False),
sa.Column('code1', sa.String(length=32), nullable=False), sa.Column('code1', sa.String(length=32), nullable=False),
sa.Column('secret', sa.String(length=128), nullable=False), sa.Column('secret', sa.String(length=128), nullable=False),
......
...@@ -31,7 +31,7 @@ def upgrade(): ...@@ -31,7 +31,7 @@ def upgrade():
batch_op.drop_column('id') batch_op.drop_column('id')
batch_op.alter_column('dn', new_column_name='group_dn', nullable=False, existing_type=sa.String(128)) batch_op.alter_column('dn', new_column_name='group_dn', nullable=False, existing_type=sa.String(128))
batch_op.alter_column('role_id', nullable=False, existing_type=sa.Integer()) batch_op.alter_column('role_id', nullable=False, existing_type=sa.Integer())
batch_op.add_column(sa.Column('requires_mfa', sa.Boolean(name=op.f('ck_role-group_requires_mfa')), nullable=False, default=False)) batch_op.add_column(sa.Column('requires_mfa', sa.Boolean(create_constraint=True, name=op.f('ck_role-group_requires_mfa')), nullable=False, default=False))
batch_op.create_primary_key(batch_op.f('pk_role-group'), ['role_id', 'group_dn']) batch_op.create_primary_key(batch_op.f('pk_role-group'), ['role_id', 'group_dn'])
def downgrade(): def downgrade():
...@@ -39,7 +39,7 @@ def downgrade(): ...@@ -39,7 +39,7 @@ def downgrade():
table = sa.Table('role-group', meta, table = sa.Table('role-group', meta,
sa.Column('role_id', sa.Integer(), nullable=False), sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('group_dn', sa.String(128), nullable=False), sa.Column('group_dn', sa.String(128), nullable=False),
sa.Column('requires_mfa', sa.Boolean(name=op.f('ck_role-group_requires_mfa')), nullable=False, default=False), sa.Column('requires_mfa', sa.Boolean(create_constraint=True, name=op.f('ck_role-group_requires_mfa')), nullable=False, default=False),
sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_role-group_role_id_role')), sa.ForeignKeyConstraint(['role_id'], ['role.id'], name=op.f('fk_role-group_role_id_role')),
sa.PrimaryKeyConstraint('role_id', 'group_dn', name=op.f('pk_role-group')) sa.PrimaryKeyConstraint('role_id', 'group_dn', name=op.f('pk_role-group'))
) )
......
...@@ -80,10 +80,10 @@ def upgrade(): ...@@ -80,10 +80,10 @@ def upgrade():
sa.Column('token', sa.String(length=128), nullable=False), sa.Column('token', sa.String(length=128), nullable=False),
sa.Column('created', sa.DateTime(), nullable=False), sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('valid_until', sa.DateTime(), nullable=False), sa.Column('valid_until', sa.DateTime(), nullable=False),
sa.Column('single_use', sa.Boolean(name=op.f('ck_invite_single_use')), nullable=False), sa.Column('single_use', sa.Boolean(create_constraint=True, name=op.f('ck_invite_single_use')), nullable=False),
sa.Column('allow_signup', sa.Boolean(name=op.f('ck_invite_allow_signup')), nullable=False), sa.Column('allow_signup', sa.Boolean(create_constraint=True, name=op.f('ck_invite_allow_signup')), nullable=False),
sa.Column('used', sa.Boolean(name=op.f('ck_invite_used')), nullable=False), sa.Column('used', sa.Boolean(create_constraint=True, name=op.f('ck_invite_used')), nullable=False),
sa.Column('disabled', sa.Boolean(name=op.f('ck_invite_disabled')), nullable=False), sa.Column('disabled', sa.Boolean(create_constraint=True, name=op.f('ck_invite_disabled')), nullable=False),
sa.PrimaryKeyConstraint('token', name=op.f('pk_invite')) sa.PrimaryKeyConstraint('token', name=op.f('pk_invite'))
) )
with op.batch_alter_table(table.name, copy_from=table, recreate='always') as batch_op: with op.batch_alter_table(table.name, copy_from=table, recreate='always') as batch_op:
...@@ -99,7 +99,7 @@ def upgrade(): ...@@ -99,7 +99,7 @@ def upgrade():
pass pass
table = sa.Table('mfa_method', meta, table = sa.Table('mfa_method', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('type', sa.Enum('RECOVERY_CODE', 'TOTP', 'WEBAUTHN', name='mfatype'), nullable=True), sa.Column('type', sa.Enum('RECOVERY_CODE', 'TOTP', 'WEBAUTHN', create_constraint=True, name='mfatype'), nullable=True),
sa.Column('created', sa.DateTime(), nullable=True), sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('name', sa.String(length=128), nullable=True), sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('dn', sa.String(length=128), nullable=True), sa.Column('dn', sa.String(length=128), nullable=True),
......
...@@ -15,7 +15,7 @@ depends_on = None ...@@ -15,7 +15,7 @@ depends_on = None
def upgrade(): def upgrade():
with op.batch_alter_table('service', schema=None) as batch_op: with op.batch_alter_table('service', schema=None) as batch_op:
batch_op.add_column(sa.Column('enable_email_preferences', sa.Boolean(), nullable=False, server_default=sa.false())) batch_op.add_column(sa.Column('enable_email_preferences', sa.Boolean(create_constraint=True), nullable=False, server_default=sa.false()))
with op.batch_alter_table('service_user', schema=None) as batch_op: with op.batch_alter_table('service_user', schema=None) as batch_op:
batch_op.add_column(sa.Column('service_email_id', sa.Integer(), nullable=True)) batch_op.add_column(sa.Column('service_email_id', sa.Integer(), nullable=True))
batch_op.create_foreign_key(batch_op.f('fk_service_user_service_email_id_user_email'), 'user_email', ['service_email_id'], ['id'], onupdate='CASCADE', ondelete='SET NULL') batch_op.create_foreign_key(batch_op.f('fk_service_user_service_email_id_user_email'), 'user_email', ['service_email_id'], ['id'], onupdate='CASCADE', ondelete='SET NULL')
...@@ -23,10 +23,10 @@ def upgrade(): ...@@ -23,10 +23,10 @@ def upgrade():
service = sa.Table('service', meta, service = sa.Table('service', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('limit_access', sa.Boolean(), nullable=False), sa.Column('limit_access', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('access_group_id', sa.Integer(), nullable=True), sa.Column('access_group_id', sa.Integer(), nullable=True),
sa.Column('use_remailer', sa.Boolean(), nullable=False), sa.Column('use_remailer', sa.Boolean(create_constraint=True), nullable=False),
sa.Column('enable_email_preferences', sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column('enable_email_preferences', sa.Boolean(create_constraint=True), nullable=False, server_default=sa.false()),
sa.ForeignKeyConstraint(['access_group_id'], ['group.id'], name=op.f('fk_service_access_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'), sa.ForeignKeyConstraint(['access_group_id'], ['group.id'], name=op.f('fk_service_access_group_id_group'), onupdate='CASCADE', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_service')), sa.PrimaryKeyConstraint('id', name=op.f('pk_service')),
sa.UniqueConstraint('name', name=op.f('uq_service_name')) sa.UniqueConstraint('name', name=op.f('uq_service_name'))
......
...@@ -26,14 +26,14 @@ def upgrade(): ...@@ -26,14 +26,14 @@ def upgrade():
sa.PrimaryKeyConstraint('service_id', 'user_id', name=op.f('pk_service_user')) sa.PrimaryKeyConstraint('service_id', 'user_id', name=op.f('pk_service_user'))
) )
with op.batch_alter_table('service_user', copy_from=service_user) as batch_op: with op.batch_alter_table('service_user', copy_from=service_user) as batch_op:
batch_op.add_column(sa.Column('remailer_overwrite_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode'), nullable=True)) batch_op.add_column(sa.Column('remailer_overwrite_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', create_constraint=True, name='remailermode'), nullable=True))
def downgrade(): def downgrade():
meta = sa.MetaData(bind=op.get_bind()) meta = sa.MetaData(bind=op.get_bind())
service_user = sa.Table('service_user', meta, service_user = sa.Table('service_user', meta,
sa.Column('service_id', sa.Integer(), nullable=False), sa.Column('service_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('remailer_overwrite_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', name='remailermode'), nullable=True), sa.Column('remailer_overwrite_mode', sa.Enum('DISABLED', 'ENABLED_V1', 'ENABLED_V2', create_constraint=True, name='remailermode'), nullable=True),
sa.Column('service_email_id', sa.Integer(), nullable=True), sa.Column('service_email_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['service_email_id'], ['user_email.id'], name=op.f('fk_service_user_service_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'), sa.ForeignKeyConstraint(['service_email_id'], ['user_email.id'], name=op.f('fk_service_user_service_email_id_user_email'), onupdate='CASCADE', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_service_user_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'), sa.ForeignKeyConstraint(['service_id'], ['service.id'], name=op.f('fk_service_user_service_id_service'), onupdate='CASCADE', ondelete='CASCADE'),
......
"""Migrate oauth2 state from user to session
Revision ID: e71e29cc605a
Revises: 99df71f0f4a0
Create Date: 2024-05-18 21:59:20.435912
"""
from alembic import op
import sqlalchemy as sa
revision = 'e71e29cc605a'
down_revision = '99df71f0f4a0'
branch_labels = None
depends_on = None
def upgrade():
op.drop_table('oauth2grant')
op.drop_table('oauth2token')
op.create_table('oauth2grant',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('expires', sa.DateTime(), nullable=False),
sa.Column('session_id', sa.Integer(), nullable=False),
sa.Column('client_db_id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=255), nullable=False),
sa.Column('redirect_uri', sa.String(length=255), nullable=True),
sa.Column('nonce', sa.Text(), nullable=True),
sa.Column('_scopes', sa.Text(), nullable=False),
sa.Column('claims', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['client_db_id'], ['oauth2client.db_id'], name=op.f('fk_oauth2grant_client_db_id_oauth2client'), onupdate='CASCADE', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['session_id'], ['session.id'], name=op.f('fk_oauth2grant_session_id_session'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth2grant'))
)
op.create_table('oauth2token',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('expires', sa.DateTime(), nullable=False),
sa.Column('session_id', sa.Integer(), nullable=False),
sa.Column('client_db_id', sa.Integer(), nullable=False),
sa.Column('token_type', sa.String(length=40), nullable=False),
sa.Column('access_token', sa.String(length=255), nullable=False),
sa.Column('refresh_token', sa.String(length=255), nullable=False),
sa.Column('_scopes', sa.Text(), nullable=False),
sa.Column('claims', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['client_db_id'], ['oauth2client.db_id'], name=op.f('fk_oauth2token_client_db_id_oauth2client'), onupdate='CASCADE', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['session_id'], ['session.id'], name=op.f('fk_oauth2token_session_id_session'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth2token')),
sa.UniqueConstraint('access_token', name=op.f('uq_oauth2token_access_token')),
sa.UniqueConstraint('refresh_token', name=op.f('uq_oauth2token_refresh_token'))
)
def downgrade():
# We don't drop and recreate the table here to improve fuzzy migration test coverage
meta = sa.MetaData(bind=op.get_bind())
session = sa.table('session',
sa.column('id', sa.Integer),
sa.column('user_id', sa.Integer()),
)
with op.batch_alter_table('oauth2token', schema=None) as batch_op:
batch_op.add_column(sa.Column('user_id', sa.INTEGER(), nullable=True))
oauth2token = sa.Table('oauth2token', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('expires', sa.DateTime(), nullable=False),
sa.Column('session_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('client_db_id', sa.Integer(), nullable=False),
sa.Column('token_type', sa.String(length=40), nullable=False),
sa.Column('access_token', sa.String(length=255), nullable=False),
sa.Column('refresh_token', sa.String(length=255), nullable=False),
sa.Column('_scopes', sa.Text(), nullable=False),
sa.Column('claims', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['client_db_id'], ['oauth2client.db_id'], name=op.f('fk_oauth2token_client_db_id_oauth2client'), onupdate='CASCADE', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['session_id'], ['session.id'], name=op.f('fk_oauth2token_session_id_session'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth2token')),
sa.UniqueConstraint('access_token', name=op.f('uq_oauth2token_access_token')),
sa.UniqueConstraint('refresh_token', name=op.f('uq_oauth2token_refresh_token'))
)
op.execute(oauth2token.update().values(user_id=sa.select([session.c.user_id]).where(oauth2token.c.session_id==session.c.id).as_scalar()))
op.execute(oauth2token.delete().where(oauth2token.c.user_id==None))
with op.batch_alter_table('oauth2token', copy_from=oauth2token) as batch_op:
batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer())
batch_op.create_foreign_key('fk_oauth2token_user_id_user', 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')
batch_op.drop_constraint(batch_op.f('fk_oauth2token_session_id_session'), type_='foreignkey')
batch_op.drop_column('session_id')
with op.batch_alter_table('oauth2grant', schema=None) as batch_op:
batch_op.add_column(sa.Column('user_id', sa.INTEGER(), nullable=True))
oauth2grant = sa.Table('oauth2grant', meta,
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('expires', sa.DateTime(), nullable=False),
sa.Column('session_id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('client_db_id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=255), nullable=False),
sa.Column('redirect_uri', sa.String(length=255), nullable=True),
sa.Column('nonce', sa.Text(), nullable=True),
sa.Column('_scopes', sa.Text(), nullable=False),
sa.Column('claims', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['client_db_id'], ['oauth2client.db_id'], name=op.f('fk_oauth2grant_client_db_id_oauth2client'), onupdate='CASCADE', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['session_id'], ['session.id'], name=op.f('fk_oauth2grant_session_id_session'), onupdate='CASCADE', ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_oauth2grant'))
)
op.execute(oauth2grant.update().values(user_id=sa.select([session.c.user_id]).where(oauth2grant.c.session_id==session.c.id).as_scalar()))
op.execute(oauth2grant.delete().where(oauth2grant.c.user_id==None))
with op.batch_alter_table('oauth2grant', copy_from=oauth2grant) as batch_op:
batch_op.alter_column('user_id', nullable=False, existing_type=sa.Integer())
batch_op.create_foreign_key('fk_oauth2grant_user_id_user', 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')
batch_op.drop_constraint(batch_op.f('fk_oauth2grant_session_id_session'), type_='foreignkey')
batch_op.drop_column('session_id')
...@@ -23,7 +23,10 @@ def upgrade(): ...@@ -23,7 +23,10 @@ def upgrade():
) )
service = sa.table('service', sa.column('id')) service = sa.table('service', sa.column('id'))
user = sa.table('user', sa.column('id')) user = sa.table('user', sa.column('id'))
op.execute(service_user.insert().from_select(['service_id', 'user_id'], sa.select([service.c.id, user.c.id]))) op.execute(service_user.insert().from_select(
['service_id', 'user_id'],
sa.select([service.c.id, user.c.id]).select_from(sa.join(service, user, sa.true()))
))
def downgrade(): def downgrade():
op.drop_table('service_user') op.drop_table('service_user')
...@@ -2,11 +2,11 @@ from .api import APIClient ...@@ -2,11 +2,11 @@ from .api import APIClient
from .invite import Invite, InviteGrant, InviteSignup from .invite import Invite, InviteGrant, InviteSignup
from .mail import Mail, MailReceiveAddress, MailDestinationAddress from .mail import Mail, MailReceiveAddress, MailDestinationAddress
from .mfa import MFAType, MFAMethod, RecoveryCodeMethod, TOTPMethod, WebauthnMethod from .mfa import MFAType, MFAMethod, RecoveryCodeMethod, TOTPMethod, WebauthnMethod
from .oauth2 import OAuth2Client, OAuth2RedirectURI, OAuth2LogoutURI, OAuth2Grant, OAuth2Token, OAuth2DeviceLoginInitiation from .oauth2 import OAuth2Client, OAuth2RedirectURI, OAuth2LogoutURI, OAuth2Grant, OAuth2Token, OAuth2DeviceLoginInitiation, OAuth2Key
from .role import Role, RoleGroup, RoleGroupMap from .role import Role, RoleGroup, RoleGroupMap
from .selfservice import PasswordToken from .selfservice import PasswordToken
from .service import RemailerMode, Service, ServiceUser, get_services from .service import RemailerMode, Service, ServiceUser, get_services
from .session import DeviceLoginType, DeviceLoginInitiation, DeviceLoginConfirmation from .session import Session, DeviceLoginType, DeviceLoginInitiation, DeviceLoginConfirmation
from .signup import Signup from .signup import Signup
from .user import User, UserEmail, Group, IDAllocator, IDRangeExhaustedError, IDAlreadyAllocatedError from .user import User, UserEmail, Group, IDAllocator, IDRangeExhaustedError, IDAlreadyAllocatedError
from .ratelimit import RatelimitEvent, Ratelimit, HostRatelimit, host_ratelimit, format_delay from .ratelimit import RatelimitEvent, Ratelimit, HostRatelimit, host_ratelimit, format_delay
......
...@@ -14,11 +14,11 @@ class APIClient(db.Model): ...@@ -14,11 +14,11 @@ class APIClient(db.Model):
auth_password = PasswordHashAttribute('_auth_password', HighEntropyPasswordHash) auth_password = PasswordHashAttribute('_auth_password', HighEntropyPasswordHash)
# Permissions are defined by adding an attribute named "perm_NAME" # Permissions are defined by adding an attribute named "perm_NAME"
perm_users = Column(Boolean(), default=False, nullable=False) perm_users = Column(Boolean(create_constraint=True), default=False, nullable=False)
perm_checkpassword = Column(Boolean(), default=False, nullable=False) perm_checkpassword = Column(Boolean(create_constraint=True), default=False, nullable=False)
perm_mail_aliases = Column(Boolean(), default=False, nullable=False) perm_mail_aliases = Column(Boolean(create_constraint=True), default=False, nullable=False)
perm_remailer = Column(Boolean(), default=False, nullable=False) perm_remailer = Column(Boolean(create_constraint=True), default=False, nullable=False)
perm_metrics = Column(Boolean(), default=False, nullable=False) perm_metrics = Column(Boolean(create_constraint=True), default=False, nullable=False)
@classmethod @classmethod
def permission_exists(cls, name): def permission_exists(cls, name):
......
...@@ -23,10 +23,10 @@ class Invite(db.Model): ...@@ -23,10 +23,10 @@ class Invite(db.Model):
creator_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE'), nullable=True) creator_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE'), nullable=True)
creator = relationship('User') creator = relationship('User')
valid_until = Column(DateTime, nullable=False) valid_until = Column(DateTime, nullable=False)
single_use = Column(Boolean, default=True, nullable=False) single_use = Column(Boolean(create_constraint=True), default=True, nullable=False)
allow_signup = Column(Boolean, default=True, nullable=False) allow_signup = Column(Boolean(create_constraint=True), default=True, nullable=False)
used = Column(Boolean, default=False, nullable=False) used = Column(Boolean(create_constraint=True), default=False, nullable=False)
disabled = Column(Boolean, default=False, nullable=False) disabled = Column(Boolean(create_constraint=True), default=False, nullable=False)
roles = relationship('Role', secondary=invite_roles) roles = relationship('Role', secondary=invite_roles)
signups = relationship('InviteSignup', back_populates='invite', lazy=True, cascade='all, delete-orphan') signups = relationship('InviteSignup', back_populates='invite', lazy=True, cascade='all, delete-orphan')
grants = relationship('InviteGrant', back_populates='invite', lazy=True, cascade='all, delete-orphan') grants = relationship('InviteGrant', back_populates='invite', lazy=True, cascade='all, delete-orphan')
...@@ -43,6 +43,8 @@ class Invite(db.Model): ...@@ -43,6 +43,8 @@ class Invite(db.Model):
def permitted(self): def permitted(self):
if self.creator is None: if self.creator is None:
return False # Creator does not exist (anymore) return False # Creator does not exist (anymore)
if self.creator.is_deactivated:
return False
if self.creator.is_in_group(current_app.config['ACL_ADMIN_GROUP']): if self.creator.is_in_group(current_app.config['ACL_ADMIN_GROUP']):
return True return True
if self.allow_signup and not self.creator.is_in_group(current_app.config['ACL_SIGNUP_GROUP']): if self.allow_signup and not self.creator.is_in_group(current_app.config['ACL_SIGNUP_GROUP']):
......
...@@ -8,17 +8,18 @@ import hmac ...@@ -8,17 +8,18 @@ import hmac
import hashlib import hashlib
import base64 import base64
import urllib.parse import urllib.parse
# imports for recovery codes
import crypt
from flask import request, current_app from flask import request, current_app
from sqlalchemy import Column, Integer, Enum, String, DateTime, Text, ForeignKey from sqlalchemy import Column, Integer, Enum, String, DateTime, Text, ForeignKey
from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import relationship, backref
from uffd.utils import nopad_b32decode, nopad_b32encode from uffd.utils import nopad_b32decode, nopad_b32encode
from uffd.password_hash import PasswordHashAttribute, CryptPasswordHash
from uffd.database import db from uffd.database import db
from .user import User from .user import User
User.mfa_recovery_codes = relationship('RecoveryCodeMethod', viewonly=True)
User.mfa_totp_methods = relationship('TOTPMethod', viewonly=True)
User.mfa_webauthn_methods = relationship('WebauthnMethod', viewonly=True)
User.mfa_enabled = property(lambda user: bool(user.mfa_totp_methods or user.mfa_webauthn_methods)) User.mfa_enabled = property(lambda user: bool(user.mfa_totp_methods or user.mfa_webauthn_methods))
class MFAType(enum.Enum): class MFAType(enum.Enum):
...@@ -29,7 +30,7 @@ class MFAType(enum.Enum): ...@@ -29,7 +30,7 @@ class MFAType(enum.Enum):
class MFAMethod(db.Model): class MFAMethod(db.Model):
__tablename__ = 'mfa_method' __tablename__ = 'mfa_method'
id = Column(Integer(), primary_key=True, autoincrement=True) id = Column(Integer(), primary_key=True, autoincrement=True)
type = Column(Enum(MFAType), nullable=False) type = Column(Enum(MFAType, create_constraint=True), nullable=False)
created = Column(DateTime(), nullable=False, default=datetime.datetime.utcnow) created = Column(DateTime(), nullable=False, default=datetime.datetime.utcnow)
name = Column(String(128)) name = Column(String(128))
user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False)
...@@ -45,9 +46,8 @@ class MFAMethod(db.Model): ...@@ -45,9 +46,8 @@ class MFAMethod(db.Model):
self.created = datetime.datetime.utcnow() self.created = datetime.datetime.utcnow()
class RecoveryCodeMethod(MFAMethod): class RecoveryCodeMethod(MFAMethod):
code_salt = Column('recovery_salt', String(64)) _code = Column('recovery_hash', String(256))
code_hash = Column('recovery_hash', String(256)) code = PasswordHashAttribute('_code', CryptPasswordHash)
user = relationship('User', backref='mfa_recovery_codes')
__mapper_args__ = { __mapper_args__ = {
'polymorphic_identity': MFAType.RECOVERY_CODE 'polymorphic_identity': MFAType.RECOVERY_CODE
...@@ -55,14 +55,11 @@ class RecoveryCodeMethod(MFAMethod): ...@@ -55,14 +55,11 @@ class RecoveryCodeMethod(MFAMethod):
def __init__(self, user): def __init__(self, user):
super().__init__(user, None) super().__init__(user, None)
# The code attribute is only available in newly created objects as only # self.code_value is not stored and only available on freshly initiated objects
# it's hash is stored in the database self.code = self.code_value = secrets.token_hex(8).replace(' ', '').lower()
self.code = secrets.token_hex(8).replace(' ', '').lower()
self.code_hash = crypt.crypt(self.code)
def verify(self, code): def verify(self, code):
code = code.replace(' ', '').lower() return self.code.verify(code.replace(' ', '').lower())
return secrets.compare_digest(crypt.crypt(code, self.code_hash), self.code_hash)
def _hotp(counter, key, digits=6): def _hotp(counter, key, digits=6):
'''Generates HMAC-based one-time password according to RFC4226 '''Generates HMAC-based one-time password according to RFC4226
...@@ -80,7 +77,7 @@ def _hotp(counter, key, digits=6): ...@@ -80,7 +77,7 @@ def _hotp(counter, key, digits=6):
class TOTPMethod(MFAMethod): class TOTPMethod(MFAMethod):
key = Column('totp_key', String(64)) key = Column('totp_key', String(64))
user = relationship('User', backref='mfa_totp_methods') last_counter = Column('totp_last_counter', Integer())
__mapper_args__ = { __mapper_args__ = {
'polymorphic_identity': MFAType.TOTP 'polymorphic_identity': MFAType.TOTP
...@@ -121,15 +118,17 @@ class TOTPMethod(MFAMethod): ...@@ -121,15 +118,17 @@ class TOTPMethod(MFAMethod):
:param code: String of digits (as entered by the user) :param code: String of digits (as entered by the user)
:returns: True if code is valid, False otherwise''' :returns: True if code is valid, False otherwise'''
counter = int(time.time()/30) current_counter = int(time.time()/30)
for valid_code in [_hotp(counter-1, self.raw_key), _hotp(counter, self.raw_key)]: for counter in (current_counter - 1, current_counter):
if counter > (self.last_counter or 0):
valid_code = _hotp(counter, self.raw_key)
if secrets.compare_digest(code, valid_code): if secrets.compare_digest(code, valid_code):
self.last_counter = counter
return True return True
return False return False
class WebauthnMethod(MFAMethod): class WebauthnMethod(MFAMethod):
_cred = Column('webauthn_cred', Text()) _cred = Column('webauthn_cred', Text())
user = relationship('User', backref='mfa_webauthn_methods')
__mapper_args__ = { __mapper_args__ = {
'polymorphic_identity': MFAType.WEBAUTHN 'polymorphic_identity': MFAType.WEBAUTHN
......
import datetime import datetime
import json import json
import secrets
import base64
from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, Boolean
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
import jwt
from uffd.database import db, CommaSeparatedList from uffd.database import db, CommaSeparatedList
from uffd.tasks import cleanup_task from uffd.tasks import cleanup_task
from uffd.password_hash import PasswordHashAttribute, HighEntropyPasswordHash from uffd.password_hash import PasswordHashAttribute, HighEntropyPasswordHash
from uffd.utils import token_urlfriendly
from .session import DeviceLoginInitiation, DeviceLoginType from .session import DeviceLoginInitiation, DeviceLoginType
from .service import ServiceUser from .service import ServiceUser
# pyjwt v1.7.x compat (Buster/Bullseye)
if not hasattr(jwt, 'get_algorithm_by_name'):
jwt.get_algorithm_by_name = lambda name: jwt.algorithms.get_default_algorithms()[name]
class OAuth2Client(db.Model): class OAuth2Client(db.Model):
__tablename__ = 'oauth2client' __tablename__ = 'oauth2client'
# Inconsistently named "db_id" instead of "id" because of the naming conflict # Inconsistently named "db_id" instead of "id" because of the naming conflict
...@@ -28,17 +36,9 @@ class OAuth2Client(db.Model): ...@@ -28,17 +36,9 @@ class OAuth2Client(db.Model):
redirect_uris = association_proxy('_redirect_uris', 'uri') redirect_uris = association_proxy('_redirect_uris', 'uri')
logout_uris = relationship('OAuth2LogoutURI', cascade='all, delete-orphan') logout_uris = relationship('OAuth2LogoutURI', cascade='all, delete-orphan')
@property
def client_type(self):
return 'confidential'
@property
def default_scopes(self):
return ['profile']
@property @property
def default_redirect_uri(self): def default_redirect_uri(self):
return self.redirect_uris[0] return self.redirect_uris[0] if len(self.redirect_uris) == 1 else None
def access_allowed(self, user): def access_allowed(self, user):
service_user = ServiceUser.query.get((self.service_id, user.id)) service_user = ServiceUser.query.get((self.service_id, user.id))
...@@ -69,48 +69,120 @@ class OAuth2Grant(db.Model): ...@@ -69,48 +69,120 @@ class OAuth2Grant(db.Model):
__tablename__ = 'oauth2grant' __tablename__ = 'oauth2grant'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) EXPIRES_IN = 100
user = relationship('User') expires = Column(DateTime, nullable=False, default=lambda: datetime.datetime.utcnow() + datetime.timedelta(seconds=OAuth2Grant.EXPIRES_IN))
session_id = Column(Integer(), ForeignKey('session.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False)
session = relationship('Session')
client_db_id = Column(Integer, ForeignKey('oauth2client.db_id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) client_db_id = Column(Integer, ForeignKey('oauth2client.db_id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False)
client = relationship('OAuth2Client') client = relationship('OAuth2Client')
code = Column(String(255), index=True, nullable=False) _code = Column('code', String(255), nullable=False, default=token_urlfriendly)
redirect_uri = Column(String(255), nullable=False) code = property(lambda self: f'{self.id}-{self._code}')
expires = Column(DateTime, nullable=False, default=lambda: datetime.datetime.utcnow() + datetime.timedelta(seconds=100)) redirect_uri = Column(String(255), nullable=True)
nonce = Column(Text(), nullable=True)
scopes = Column('_scopes', CommaSeparatedList(), nullable=False, default=tuple()) scopes = Column('_scopes', CommaSeparatedList(), nullable=False, default=tuple())
_claims = Column('claims', Text(), nullable=True)
@property
def claims(self):
return json.loads(self._claims) if self._claims is not None else None
@claims.setter
def claims(self, value):
self._claims = json.dumps(value) if value is not None else None
@property
def service_user(self):
return ServiceUser.query.get((self.client.service_id, self.session.user_id))
@hybrid_property @hybrid_property
def expired(self): def expired(self):
if self.expires is None: if self.expires is None:
return False return False
return self.expires < datetime.datetime.utcnow() return self.expires < datetime.datetime.utcnow()
@cleanup_task.delete_by_attribute('expired') @classmethod
def get_by_authorization_code(cls, code):
# pylint: disable=protected-access
if '-' not in code:
return None
grant_id, grant_code = code.split('-', 2)
grant = cls.query.filter_by(id=grant_id, expired=False).first()
if not grant or not secrets.compare_digest(grant._code, grant_code):
return None
if grant.session.expired or grant.session.user.is_deactivated:
return None
if not grant.service_user or not grant.service_user.has_access:
return None
return grant
def make_token(self, **kwargs):
return OAuth2Token(
session=self.session,
client=self.client,
scopes=self.scopes,
claims=self.claims,
**kwargs
)
# OAuth2Token objects are cleaned-up when the session expires and is
# auto-deleted (or the user manually revokes it).
class OAuth2Token(db.Model): class OAuth2Token(db.Model):
__tablename__ = 'oauth2token' __tablename__ = 'oauth2token'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer(), ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) EXPIRES_IN = 3600
user = relationship('User') expires = Column(DateTime, nullable=False, default=lambda: datetime.datetime.utcnow() + datetime.timedelta(seconds=OAuth2Token.EXPIRES_IN))
session_id = Column(Integer(), ForeignKey('session.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False)
session = relationship('Session')
client_db_id = Column(Integer, ForeignKey('oauth2client.db_id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False) client_db_id = Column(Integer, ForeignKey('oauth2client.db_id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False)
client = relationship('OAuth2Client') client = relationship('OAuth2Client')
# currently only bearer is supported # currently only bearer is supported
token_type = Column(String(40), nullable=False) token_type = Column(String(40), nullable=False, default='bearer')
access_token = Column(String(255), unique=True, nullable=False) _access_token = Column('access_token', String(255), unique=True, nullable=False, default=token_urlfriendly)
refresh_token = Column(String(255), unique=True, nullable=False) access_token = property(lambda self: f'{self.id}-{self._access_token}')
expires = Column(DateTime, nullable=False) _refresh_token = Column('refresh_token', String(255), unique=True, nullable=False, default=token_urlfriendly)
refresh_token = property(lambda self: f'{self.id}-{self._refresh_token}')
scopes = Column('_scopes', CommaSeparatedList(), nullable=False, default=tuple()) scopes = Column('_scopes', CommaSeparatedList(), nullable=False, default=tuple())
_claims = Column('claims', Text(), nullable=True)
@property
def claims(self):
return json.loads(self._claims) if self._claims is not None else None
@claims.setter
def claims(self, value):
self._claims = json.dumps(value) if value is not None else None
@property
def service_user(self):
return ServiceUser.query.get((self.client.service_id, self.session.user_id))
@hybrid_property @hybrid_property
def expired(self): def expired(self):
return self.expires < datetime.datetime.utcnow() return self.expires < datetime.datetime.utcnow()
def set_expires_in_seconds(self, seconds): @classmethod
self.expires = datetime.datetime.utcnow() + datetime.timedelta(seconds=seconds) def get_by_access_token(cls, access_token):
expires_in_seconds = property(fset=set_expires_in_seconds) # pylint: disable=protected-access
if '-' not in access_token:
return None
token_id, token_secret = access_token.split('-', 2)
token = cls.query.filter_by(id=token_id, expired=False).first()
if not token or not secrets.compare_digest(token._access_token, token_secret):
return None
if token.session.expired or token.session.user.is_deactivated:
return None
if not token.service_user or not token.service_user.has_access:
return None
return token
class OAuth2DeviceLoginInitiation(DeviceLoginInitiation): class OAuth2DeviceLoginInitiation(DeviceLoginInitiation):
__mapper_args__ = { __mapper_args__ = {
...@@ -122,3 +194,97 @@ class OAuth2DeviceLoginInitiation(DeviceLoginInitiation): ...@@ -122,3 +194,97 @@ class OAuth2DeviceLoginInitiation(DeviceLoginInitiation):
@property @property
def description(self): def description(self):
return self.client.service.name return self.client.service.name
class OAuth2Key(db.Model):
__tablename__ = 'oauth2_key'
id = Column(String(64), primary_key=True, default=token_urlfriendly)
created = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
active = Column(Boolean(create_constraint=False), default=True, nullable=False)
algorithm = Column(String(32), nullable=False)
private_key_jwk = Column(Text(), nullable=False)
public_key_jwk = Column(Text(), nullable=False)
def __init__(self, **kwargs):
if kwargs.get('algorithm') and kwargs.get('private_key') \
and not kwargs.get('private_key_jwk') \
and not kwargs.get('public_key_jwk'):
algorithm = jwt.get_algorithm_by_name(kwargs['algorithm'])
private_key = kwargs.pop('private_key')
kwargs['private_key_jwk'] = algorithm.to_jwk(private_key)
kwargs['public_key_jwk'] = algorithm.to_jwk(private_key.public_key())
super().__init__(**kwargs)
@property
def private_key(self):
# pylint: disable=protected-access,import-outside-toplevel
# cryptography performs expensive checks when loading RSA private keys.
# Since we only load keys we generated ourselves with help of cryptography,
# these checks are unnecessary.
import cryptography.hazmat.backends.openssl
cryptography.hazmat.backends.openssl.backend._rsa_skip_check_key = True
res = jwt.get_algorithm_by_name(self.algorithm).from_jwk(self.private_key_jwk)
cryptography.hazmat.backends.openssl.backend._rsa_skip_check_key = False
return res
@property
def public_key(self):
return jwt.get_algorithm_by_name(self.algorithm).from_jwk(self.public_key_jwk)
@property
def public_key_jwks_dict(self):
res = json.loads(self.public_key_jwk)
res['kid'] = self.id
res['alg'] = self.algorithm
res['use'] = 'sig'
# RFC7517 4.3 "The "use" and "key_ops" JWK members SHOULD NOT be used together [...]"
res.pop('key_ops', None)
return res
def encode_jwt(self, payload):
if not self.active:
raise jwt.exceptions.InvalidKeyError(f'Key {self.id} not active')
res = jwt.encode(payload, key=self.private_key, algorithm=self.algorithm, headers={'kid': self.id})
# pyjwt pre-v2 compat (Buster/Bullseye)
if isinstance(res, bytes):
res = res.decode()
return res
# Hash algorithm for at_hash/c_hash from OpenID Connect Core 1.0 section 3.1.3.6
def oidc_hash(self, value):
# pylint: disable=import-outside-toplevel
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend # Only required for Buster
hash_alg = jwt.get_algorithm_by_name(self.algorithm).hash_alg
digest = hashes.Hash(hash_alg(), backend=default_backend())
digest.update(value)
return base64.urlsafe_b64encode(
digest.finalize()[:hash_alg.digest_size // 2]
).decode('ascii').rstrip('=')
@classmethod
def get_preferred_key(cls, algorithm='RS256'):
return cls.query.filter_by(active=True, algorithm=algorithm).order_by(OAuth2Key.created.desc()).first()
@classmethod
def get_available_algorithms(cls):
return ['RS256']
@classmethod
def decode_jwt(cls, data, algorithms=('RS256',), **kwargs):
headers = jwt.get_unverified_header(data)
if 'kid' not in headers:
raise jwt.exceptions.InvalidKeyError('JWT without kid')
kid = headers['kid']
key = cls.query.get(kid)
if not key:
raise jwt.exceptions.InvalidKeyError(f'Key {kid} not found')
if not key.active:
raise jwt.exceptions.InvalidKeyError(f'Key {kid} not active')
return jwt.decode(data, key=key.public_key, algorithms=algorithms, **kwargs)
@classmethod
def generate_rsa_key(cls, public_exponent=65537, key_size=3072):
# pylint: disable=import-outside-toplevel
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend # Only required for Buster
return cls(algorithm='RS256', private_key=rsa.generate_private_key(public_exponent=public_exponent, key_size=key_size, backend=default_backend()))
...@@ -11,7 +11,7 @@ class RoleGroup(db.Model): ...@@ -11,7 +11,7 @@ class RoleGroup(db.Model):
role = relationship('Role', back_populates='groups') role = relationship('Role', back_populates='groups')
group_id = Column(Integer(), ForeignKey('group.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True) group_id = Column(Integer(), ForeignKey('group.id', onupdate='CASCADE', ondelete='CASCADE'), primary_key=True)
group = relationship('Group') group = relationship('Group')
requires_mfa = Column(Boolean(), default=False, nullable=False) requires_mfa = Column(Boolean(create_constraint=True), default=False, nullable=False)
# pylint: disable=E1101 # pylint: disable=E1101
role_members = db.Table('role_members', role_members = db.Table('role_members',
...@@ -99,9 +99,9 @@ class Role(db.Model): ...@@ -99,9 +99,9 @@ class Role(db.Model):
# Roles that are managed externally (e.g. by Ansible) can be locked to # Roles that are managed externally (e.g. by Ansible) can be locked to
# prevent accidental editing of name, moderator group, included roles # prevent accidental editing of name, moderator group, included roles
# and groups as well as deletion in the web interface. # and groups as well as deletion in the web interface.
locked = Column(Boolean(), default=False, nullable=False) locked = Column(Boolean(create_constraint=True), default=False, nullable=False)
is_default = Column(Boolean(), default=False, nullable=False) is_default = Column(Boolean(create_constraint=True), default=False, nullable=False)
@property @property
def members_effective(self): def members_effective(self):
......
...@@ -26,15 +26,16 @@ class Service(db.Model): ...@@ -26,15 +26,16 @@ class Service(db.Model):
# parameter meant no access restrictions. Representing this state by # parameter meant no access restrictions. Representing this state by
# setting access_group_id to NULL would lead to a bad/unintuitive ondelete # setting access_group_id to NULL would lead to a bad/unintuitive ondelete
# behaviour. # behaviour.
limit_access = Column(Boolean(), default=True, nullable=False) limit_access = Column(Boolean(create_constraint=True), default=True, nullable=False)
access_group_id = Column(Integer(), ForeignKey('group.id', onupdate='CASCADE', ondelete='SET NULL'), nullable=True) access_group_id = Column(Integer(), ForeignKey('group.id', onupdate='CASCADE', ondelete='SET NULL'), nullable=True)
access_group = relationship('Group') access_group = relationship('Group')
oauth2_clients = relationship('OAuth2Client', back_populates='service', cascade='all, delete-orphan') oauth2_clients = relationship('OAuth2Client', back_populates='service', cascade='all, delete-orphan')
api_clients = relationship('APIClient', back_populates='service', cascade='all, delete-orphan') api_clients = relationship('APIClient', back_populates='service', cascade='all, delete-orphan')
remailer_mode = Column(Enum(RemailerMode), default=RemailerMode.DISABLED, nullable=False) remailer_mode = Column(Enum(RemailerMode, create_constraint=True), default=RemailerMode.DISABLED, nullable=False)
enable_email_preferences = Column(Boolean(), default=False, nullable=False) enable_email_preferences = Column(Boolean(create_constraint=True), default=False, nullable=False)
hide_deactivated_users = Column(Boolean(create_constraint=True), default=False, nullable=False)
class ServiceUser(db.Model): class ServiceUser(db.Model):
'''Service-related configuration and state for a user '''Service-related configuration and state for a user
...@@ -62,7 +63,7 @@ class ServiceUser(db.Model): ...@@ -62,7 +63,7 @@ class ServiceUser(db.Model):
def has_email_preferences(self): def has_email_preferences(self):
return self.has_access and self.service.enable_email_preferences return self.has_access and self.service.enable_email_preferences
remailer_overwrite_mode = Column(Enum(RemailerMode), default=None, nullable=True) remailer_overwrite_mode = Column(Enum(RemailerMode, create_constraint=True), default=None, nullable=True)
@property @property
def effective_remailer_mode(self): def effective_remailer_mode(self):
...@@ -115,6 +116,16 @@ class ServiceUser(db.Model): ...@@ -115,6 +116,16 @@ class ServiceUser(db.Model):
return remailer.build_v2_address(self.service_id, self.user_id) return remailer.build_v2_address(self.service_id, self.user_id)
return self.real_email return self.real_email
# User.primary_email and ServiceUser.service_email can only be set to
# verified addresses, so this should always return True
@property
def email_verified(self):
if self.effective_remailer_mode != RemailerMode.DISABLED:
return True
if self.has_email_preferences and self.service_email:
return self.service_email.verified
return self.user.primary_email.verified
@classmethod @classmethod
def filter_query_by_email(cls, query, email): def filter_query_by_email(cls, query, email):
'''Filter query of ServiceUser by ServiceUser.email''' '''Filter query of ServiceUser by ServiceUser.email'''
...@@ -177,7 +188,7 @@ def create_service_users(session, flush_context): # pylint: disable=unused-argum ...@@ -177,7 +188,7 @@ def create_service_users(session, flush_context): # pylint: disable=unused-argum
return return
db.session.execute(db.insert(ServiceUser).from_select( db.session.execute(db.insert(ServiceUser).from_select(
['service_id', 'user_id'], ['service_id', 'user_id'],
db.select([Service.id, User.id]).where(db.or_( db.select([Service.id, User.id]).select_from(db.join(Service, User, db.true())).where(db.or_(
Service.id.in_(new_service_ids), Service.id.in_(new_service_ids),
User.id.in_(new_user_ids), User.id.in_(new_user_ids),
)) ))
...@@ -192,7 +203,7 @@ def create_missing_service_users(): ...@@ -192,7 +203,7 @@ def create_missing_service_users():
# pylint: disable=no-member # pylint: disable=no-member
db.session.execute(db.insert(ServiceUser).from_select( db.session.execute(db.insert(ServiceUser).from_select(
['service_id', 'user_id'], ['service_id', 'user_id'],
db.select([Service.id, User.id]).where(db.not_( db.select([Service.id, User.id]).select_from(db.join(Service, User, db.true())).where(db.not_(
ServiceUser.query.filter( ServiceUser.query.filter(
ServiceUser.service_id == Service.id, ServiceUser.service_id == Service.id,
ServiceUser.user_id == User.id ServiceUser.user_id == User.id
......