-
Notifications
You must be signed in to change notification settings - Fork 687
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use special "deleted" journalist for associations with deleted users
Currently when a journalist is deleted, most referential tables are updated with `journalist_id=NULL`, forcing all clients to accomodate that case. Instead, we are now going to either re-associate those rows with a special "deleted" journalist account, or delete them outright. All journalist_id columns are now NOT NULL to enforce this. Tables with rows migrated to "deleted": * replies * seen_files * seen_messages * seen_replies Tables with rows that are deleted outright: * journalist_login_attempt * revoked_tokens The "deleted" journalist account is a real account that exists in the database, but cannot be logged into and has no passphase set. It is not possible to delete it nor is it shown in the admin listing of journalists. It is lazily created on demand using a special DeletedJournalist subclass that bypasses username and passphrase validation. Journalist objects must now be deleted by calling the new delete() function on them. Trying to directly `db.session.delete(journalist)` will most likely fail with an Exception because of rows that weren't migrated first. The migration step looks for any existing rows in those tables with journalist_id=NULL and either migrates them to "deleted" or deletes them. Then all the columns are changed to be NOT NULL. Fixes #6192.
- Loading branch information
Showing
7 changed files
with
360 additions
and
21 deletions.
There are no files selected for viewing
128 changes: 128 additions & 0 deletions
128
securedrop/alembic/versions/2e24fc7536e8_make_journalist_id_non_nullable.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
"""make journalist_id non-nullable | ||
Revision ID: 2e24fc7536e8 | ||
Revises: de00920916bf | ||
Create Date: 2022-01-12 19:31:06.186285 | ||
""" | ||
import uuid | ||
|
||
from alembic import op | ||
import sqlalchemy as sa | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = '2e24fc7536e8' | ||
down_revision = 'de00920916bf' | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def create_deleted() -> int: | ||
"""manually insert a "deleted" journalist user. | ||
We need to do it this way since the model will reflect the current state of | ||
the schema, not what it is at the current migration step | ||
It should be basically identical to what DeletedJournalist.get() does | ||
""" | ||
op.execute(sa.text( | ||
f"""\ | ||
INSERT INTO journalists (uuid, username, session_nonce) | ||
VALUES (:uuid, "deleted", 0); | ||
""" | ||
).bindparams(uuid=str(uuid.uuid4()))) | ||
# Get the autoincrement ID back | ||
conn = op.get_bind() | ||
result = conn.execute('SELECT id FROM journalists WHERE username="deleted";').fetchall() | ||
return result[0][0] | ||
|
||
|
||
def migrate_nulls(): | ||
"""migrate existing journalist_id=NULL over to deleted or delete them""" | ||
op.execute("DELETE FROM journalist_login_attempt WHERE journalist_id IS NULL;") | ||
op.execute("DELETE FROM revoked_tokens WHERE journalist_id IS NULL;") | ||
# Look to see if we have data to migrate | ||
tables = ('replies', 'seen_files', 'seen_messages', 'seen_replies') | ||
needs_migration = [] | ||
conn = op.get_bind() | ||
for table in tables: | ||
result = conn.execute(f'SELECT 1 FROM {table} WHERE journalist_id IS NULL;').first() | ||
if result is not None: | ||
needs_migration.append(table) | ||
|
||
if not needs_migration: | ||
return | ||
|
||
deleted_id = create_deleted() | ||
for table in needs_migration: | ||
op.execute(sa.text( | ||
f'UPDATE {table} SET journalist_id=:journalist_id WHERE journalist_id IS NULL;' | ||
).bindparams(journalist_id=deleted_id)) | ||
|
||
|
||
def upgrade(): | ||
migrate_nulls() | ||
|
||
with op.batch_alter_table('journalist_login_attempt', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=False) | ||
|
||
with op.batch_alter_table('replies', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=False) | ||
|
||
with op.batch_alter_table('revoked_tokens', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=False) | ||
|
||
with op.batch_alter_table('seen_files', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=False) | ||
|
||
with op.batch_alter_table('seen_messages', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=False) | ||
|
||
with op.batch_alter_table('seen_replies', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=False) | ||
|
||
|
||
def downgrade(): | ||
# We do not un-migrate the data back to journalist_id=NULL | ||
|
||
with op.batch_alter_table('seen_replies', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=True) | ||
|
||
with op.batch_alter_table('seen_messages', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=True) | ||
|
||
with op.batch_alter_table('seen_files', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=True) | ||
|
||
with op.batch_alter_table('revoked_tokens', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=True) | ||
|
||
with op.batch_alter_table('replies', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=True) | ||
|
||
with op.batch_alter_table('journalist_login_attempt', schema=None) as batch_op: | ||
batch_op.alter_column('journalist_id', | ||
existing_type=sa.INTEGER(), | ||
nullable=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.