Skip to content

Feat: authdb tables cleanup#815

Draft
HeloiseJoffe wants to merge 4 commits intoDIRACGrid:mainfrom
HeloiseJoffe:feat/authdb-tables-cleanup
Draft

Feat: authdb tables cleanup#815
HeloiseJoffe wants to merge 4 commits intoDIRACGrid:mainfrom
HeloiseJoffe:feat/authdb-tables-cleanup

Conversation

@HeloiseJoffe
Copy link
Contributor

Closes DIRACGrid/diracx-charts#222

Changes:

  • Add functions to cleanup expired refresh tokens and flows from the AuthDB
  • Add tests for each cleanup function in the DB

The global cleanup function can be called with this kubernetes cronjob :

metadata:
  name: diracx-cleanup-authdb
  namespace: diracx
spec:
  schedule: "0 0 1 * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: cleanup
              image: ghcr.io/diracgrid/diracx/services:dev
              command:
                - /bin/sh
                - -c
                - |
                  /opt/conda/bin/python -c "
                  import asyncio
                  import os
                  from diracx.core.settings import AuthSettings
                  from diracx.db.sql import AuthDB
                  from diracx.logic.auth.token import cleanup_expired_data
                  import logging

                  logging.basicConfig(
                    level=logging.INFO
                  )

                  async def main():
                    db_url = os.getenv('DIRACX_DB_URL_AUTHDB')
                    settings = AuthSettings()
                    db = AuthDB(db_url)

                    async with db.engine_context():
                      async with db:
                        await cleanup_expired_data(db, settings)

                  asyncio.run(main())
                  "
              envFrom:
                - secretRef:
                    name: diracx-secrets
                - secretRef:
                    name: diracx-sql-connection-urls
                - secretRef:
                    name: diracx-dynamic-secrets
              volumeMounts:
                - name: keystore
                  mountPath: /keystore/jwks.json
                  subPath: jwks.json
          volumes:
            - name: keystore
              secret: 
                secretName: diracx-jwks
                items:
                  - key: jwks.json
                    path: jwks.json

Where could I add documentation to guide administrators in setting up the cronjob?

@read-the-docs-community
Copy link

read-the-docs-community bot commented Mar 3, 2026

Documentation build overview

📚 diracx | 🛠️ Build #31738700 | 📁 Comparing f143436 against latest (70bf3c7)


🔍 Preview build

Show files changed (1 files in total): 📝 1 modified | ➕ 0 added | ➖ 0 deleted
File Status
admin/reference/env-variables/index.html 📝 modified

@aldbr aldbr requested a review from natthan-pigoux March 4, 2026 08:17
@aldbr
Copy link
Contributor

aldbr commented Mar 4, 2026

I have not started to review it, but could you also open a PR in diracx-charts with the cron job and the documentation please?

@HeloiseJoffe HeloiseJoffe force-pushed the feat/authdb-tables-cleanup branch from db91f3a to db5091d Compare March 5, 2026 08:07
@HeloiseJoffe HeloiseJoffe marked this pull request as draft March 5, 2026 09:24
@HeloiseJoffe HeloiseJoffe force-pushed the feat/authdb-tables-cleanup branch from db5091d to f143436 Compare March 10, 2026 09:14
@HeloiseJoffe HeloiseJoffe marked this pull request as ready for review March 10, 2026 10:13
@HeloiseJoffe HeloiseJoffe requested a review from aldbr March 10, 2026 10:13
)

async def clean_expired_refresh_token(
self, max_validity: int, max_retention: int
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment: it might be clearer to mention the unit of max_validity in the docstring to avoid any ambiguous usage (the most expected guess if you don't read the code is that max_validity is expressed in seconds, at least in my mind)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for max_retention by the way


return res_auth.rowcount

async def clean_expired_device_flows(self, max_retention: int) -> int:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, especially that you have 2 max_retention and they are not even in the same units.
To improve clarity, we could even always express max_validity and max_retention using the same units throughout the diracx-db layer to avoid any confusion.
What do you think?

.values(status=RefreshTokenStatus.REVOKED)
)

async def clean_expired_refresh_token(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
async def clean_expired_refresh_token(
async def clean_expired_refresh_tokens(

max_retention=settings.revoked_refresh_token_retention_days,
)
logger.info(
f"Deleted {expired_tokens} expired and {revoked_tokens} revoked refresh tokens"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid string formatting if the log level is disabled.

Suggested change
f"Deleted {expired_tokens} expired and {revoked_tokens} revoked refresh tokens"
"Deleted %d expired and %d revoked refresh tokens", expired_tokens, revoked_tokens)

"cleanup-authdb", help="Delete expired tokens and flows from the AuthDB"
)
cleanup_authdb_parser.add_argument(
"--db-url", required=True, help="URL to the AuthDB"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need a db-url parameter?
Can't you get the DB URL from an environment variable ($DIRACX_DB_URL_AUTHDB)?

Comment on lines +350 to +363
stmt_expired = delete(RefreshTokens).where(
RefreshTokens.status == RefreshTokenStatus.CREATED,
RefreshTokens.jti < expired_date,
)
res_expired = await self.conn.execute(stmt_expired)

revoked_date = str(
uuid7_from_datetime(substract_date(days=max_retention), randomize=False)
)
stmt_revoked = delete(RefreshTokens).where(
RefreshTokens.status == RefreshTokenStatus.REVOKED,
RefreshTokens.jti < revoked_date,
)
res_revoked = await self.conn.execute(stmt_revoked)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment but this could be split into 2 different functions to respect the single-responsibility principle: one function to delete expired refresh tokens, another one to delete old revoked refresh tokens.

return await revoke_refresh_token_by_jti(auth_db=auth_db, subject=subject, jti=jti)


async def cleanup_expired_data(auth_db: AuthDB, settings: AuthSettings) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PENDING and READY flows never transition to a terminal state on their own from what I can see, do you confirm?
If one abandons the flow (stops polling, closes browser), the row stays forever.

I don't think we need any intermediate EXPIRED status because the status field is never consulted for expiration. The error messages are already returned based on time alone.

You should also delete PENDING/READY flows older than their max_retention time I think (note: we should make sure that max_retention is bigger than device/authorize_flow_expiration_seconds).

Any opinion?

@DIRACGridBot DIRACGridBot marked this pull request as draft March 13, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cleanup of the AuthDB tables

4 participants