diff --git a/alembic/versions/20240404_be8ed331efcc_add_last_signed_in_column_to_.py b/alembic/versions/20240404_be8ed331efcc_add_last_signed_in_column_to_.py new file mode 100644 index 000000000..f845990cd --- /dev/null +++ b/alembic/versions/20240404_be8ed331efcc_add_last_signed_in_column_to_.py @@ -0,0 +1,32 @@ +"""Add last_signed_in column to admincredentials + +Revision ID: be8ed331efcc +Revises: 4915a361b082 +Create Date: 2024-04-04 11:34:33.755399+00:00 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "be8ed331efcc" +down_revision = "4915a361b082" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "admincredentials", + sa.Column( + "last_signed_in", + sa.DateTime(), + server_default=sa.text("now()"), + nullable=False, + ), + ) + + +def downgrade() -> None: + op.drop_column("admincredentials", "last_signed_in") diff --git a/api/admin/controller/sign_in.py b/api/admin/controller/sign_in.py index 49b55f745..f7c029778 100644 --- a/api/admin/controller/sign_in.py +++ b/api/admin/controller/sign_in.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from datetime import datetime, timezone from urllib.parse import urlsplit import flask @@ -101,6 +102,7 @@ def ekirjasto_auth_finish(self): try: credentials = get_one(self._db, AdminCredential, external_id=user_info.sub) if credentials: + credentials.last_signed_in = datetime.now(timezone.utc) admin = credentials.admin else: admin = self._create_admin_with_external_credentials(user_info) diff --git a/core/model/admin.py b/core/model/admin.py index bd8f949c8..53bea3b21 100644 --- a/core/model/admin.py +++ b/core/model/admin.py @@ -9,6 +9,7 @@ from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer from sqlalchemy import ( Column, + DateTime, ForeignKey, Index, Integer, @@ -346,6 +347,7 @@ class AdminCredential(Base): id = Column(Integer, primary_key=True) external_id = Column(Unicode, nullable=False) + last_signed_in = Column(DateTime, nullable=False, server_default=func.now()) admin_id = Column( Integer, ForeignKey("admins.id", ondelete="CASCADE"), index=True, nullable=False diff --git a/core/monitor.py b/core/monitor.py index b7e3b011d..8617344e9 100644 --- a/core/monitor.py +++ b/core/monitor.py @@ -30,6 +30,7 @@ get_one, get_one_or_create, ) +from core.model.admin import Admin, AdminCredential from core.model.configuration import ConfigurationSetting from core.service.container import container_instance from core.util.datetime_helpers import utc_now @@ -1072,3 +1073,20 @@ class PatronNeighborhoodScrubber(ScrubberMonitor): ReaperMonitor.REGISTRY.append(PatronNeighborhoodScrubber) + + +class EkirjastoInactiveAdminReaperMonitor(ReaperMonitor): + """Reaper for cleaning up inactive externally authenticated admins""" + + MODEL_CLASS = AdminCredential + TIMESTAMP_FIELD = "last_signed_in" + MAX_AGE = datetime.timedelta(days=180) + + def delete(self, row: AdminCredential): + self._db.delete(row.admin) + + def query(self): + return self._db.query(AdminCredential).join(Admin).filter(self.where_clause) + + +ReaperMonitor.REGISTRY.append(EkirjastoInactiveAdminReaperMonitor)