Skip to content

Commit

Permalink
Add a new key table to hold some of our sitewide keys (PP-893) (#1664)
Browse files Browse the repository at this point in the history
* Add keys table
* Add migration
* Fix comment
  • Loading branch information
jonathangreen authored Feb 8, 2024
1 parent 626d446 commit 6540ea0
Show file tree
Hide file tree
Showing 25 changed files with 1,076 additions and 387 deletions.
121 changes: 121 additions & 0 deletions alembic/versions/20240207_fc3c9ccf0ad8_add_keys_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Add keys table
Revision ID: fc3c9ccf0ad8
Revises: 993729d4bf97
Create Date: 2024-02-07 17:51:44.823725+00:00
"""
import datetime
import uuid
from collections.abc import Callable

import sqlalchemy as sa
from jwcrypto import jwk
from sqlalchemy.dialects import postgresql
from sqlalchemy.engine import Connection

from alembic import op
from core.migration.util import migration_logger
from core.util.datetime_helpers import utc_now
from core.util.string_helpers import random_key

# revision identifiers, used by Alembic.
revision = "fc3c9ccf0ad8"
down_revision = "993729d4bf97"
branch_labels = None
depends_on = None

log = migration_logger(revision)


def get_sitewide_config(connection: Connection, key: str) -> str | None:
result = connection.execute(
"SELECT value from configurationsettings where key = %s and library_id is null and external_integration_id is null",
key,
).one_or_none()

if result is None:
return None

return result.value


def insert_key(
connection: Connection, key_type: str, value: str, created: datetime.datetime
) -> None:
connection.execute(
"INSERT INTO keys (id, created, value, type) VALUES (%s, %s, %s, %s)",
(uuid.uuid4(), created, value, key_type),
)


def migrate_configuration_setting(
connection: Connection,
key_type: str,
setting_value: str | None,
generate: Callable[[], str],
) -> None:
unknown_creation_time = datetime.datetime(
year=1970, month=1, day=1, tzinfo=datetime.timezone.utc
)

if setting_value:
log.info(f"Migrating {key_type} to new keys table")
insert_key(connection, key_type, setting_value, unknown_creation_time)
else:
log.warning(f"No {key_type} found. Generating a new one.")
insert_key(connection, key_type, generate(), utc_now())


def upgrade() -> None:
op.create_table(
"keys",
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), nullable=False),
sa.Column("value", sa.Unicode(), nullable=False),
sa.Column(
"type",
sa.Enum(
"AUTH_TOKEN_JWE",
"BEARER_TOKEN_SIGNING",
"ADMIN_SECRET_KEY",
name="keytype",
),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_keys_created"), "keys", ["created"], unique=False)
op.create_index(op.f("ix_keys_type"), "keys", ["type"], unique=False)

# Migrate in the data from the old table.
connection = op.get_bind()

admin_secret_key = get_sitewide_config(connection, "secret_key")
bearer_token_signing_key = get_sitewide_config(
connection, "bearer_token_signing_secret"
)
auth_token_jwe_key = get_sitewide_config(connection, "PATRON_JWE_KEY")

migrate_configuration_setting(
connection, "ADMIN_SECRET_KEY", admin_secret_key, lambda: random_key(48)
)
migrate_configuration_setting(
connection,
"BEARER_TOKEN_SIGNING",
bearer_token_signing_key,
lambda: random_key(48),
)
migrate_configuration_setting(
connection,
"AUTH_TOKEN_JWE",
auth_token_jwe_key,
lambda: jwk.JWK.generate(kty="oct", size=256).export(),
)


def downgrade() -> None:
op.drop_index(op.f("ix_keys_type"), table_name="keys")
op.drop_index(op.f("ix_keys_created"), table_name="keys")
op.drop_table("keys")
sa.Enum(name="keytype").drop(op.get_bind(), checkfirst=False)
15 changes: 11 additions & 4 deletions api/admin/password_admin_authentication_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
reset_password_template,
sign_in_template,
)
from api.config import Configuration
from core.model import Admin, ConfigurationSetting
from core.model import Admin, Key
from core.model.key import KeyType
from core.util.email import EmailManager
from core.util.problem_detail import ProblemDetail

Expand All @@ -33,6 +33,13 @@ class PasswordAdminAuthenticationProvider(AdminAuthenticationProvider):
label=label_style, input=input_style, button=button_style
)

@staticmethod
def get_secret_key(db: Session) -> str:
key = Key.get_key(db, KeyType.ADMIN_SECRET_KEY, raise_exception=True).value
# We know .value is a str because its a non-null column in the DB, so
# we use an ignore to tell mypy to trust us.
return key # type: ignore[return-value]

def sign_in_template(self, redirect):
password_sign_in_url = url_for("password_auth")
forgot_password_url = url_for("admin_forgot_password")
Expand Down Expand Up @@ -83,7 +90,7 @@ def active_credentials(self, admin):
return True

def generate_reset_password_token(self, admin: Admin, _db: Session) -> str:
secret_key = ConfigurationSetting.sitewide_secret(_db, Configuration.SECRET_KEY)
secret_key = self.get_secret_key(_db)

reset_password_token = admin.generate_reset_password_token(secret_key)

Expand All @@ -109,7 +116,7 @@ def send_reset_password_email(self, admin: Admin, reset_password_url: str) -> No
def validate_token_and_extract_admin(
self, reset_password_token: str, admin_id: int, _db: Session
) -> Admin | ProblemDetail:
secret_key = ConfigurationSetting.sitewide_secret(_db, Configuration.SECRET_KEY)
secret_key = self.get_secret_key(_db)

return Admin.validate_reset_password_token_and_fetch_admin(
reset_password_token, admin_id, _db, secret_key
Expand Down
7 changes: 5 additions & 2 deletions api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
)
from core.app_server import ErrorHandler
from core.flask_sqlalchemy_session import flask_scoped_session
from core.model import ConfigurationSetting, SessionManager
from core.model import Key, SessionManager
from core.model.key import KeyType
from core.service.container import Services, container_instance
from core.util import LanguageCodes
from core.util.cache import CachedData
Expand Down Expand Up @@ -61,7 +62,9 @@ def initialize_admin(_db=None):
setup_admin_controllers(app.manager)
_db = _db or app._db
# The secret key is used for signing cookies for admin login
app.secret_key = ConfigurationSetting.sitewide_secret(_db, Configuration.SECRET_KEY)
app.secret_key = Key.get_key(
_db, KeyType.ADMIN_SECRET_KEY, raise_exception=True
).value


def initialize_circulation_manager(container: Services):
Expand Down
Loading

0 comments on commit 6540ea0

Please sign in to comment.