-
Notifications
You must be signed in to change notification settings - Fork 45
fix: added policies to RefreshToken DB #950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,10 @@ | |
|
|
||
| import base64 | ||
| import hashlib | ||
| import logging | ||
| import re | ||
| from datetime import datetime, timedelta, timezone | ||
| from typing import cast | ||
| from typing import Any, cast | ||
|
|
||
| from joserfc import jwt | ||
| from joserfc.jwt import Claims | ||
|
|
@@ -37,6 +38,8 @@ | |
| verify_dirac_refresh_token, | ||
| ) | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
Stellatsuu marked this conversation as resolved.
|
||
|
|
||
|
|
||
| async def get_oidc_token( | ||
| grant_type: GrantType, | ||
|
|
@@ -45,6 +48,7 @@ async def get_oidc_token( | |
| config: Config, | ||
| settings: AuthSettings, | ||
| available_properties: set[SecurityProperty], | ||
| policies: dict[str, Any], | ||
| device_code: str | None = None, | ||
| code: str | None = None, | ||
| redirect_uri: str | None = None, | ||
|
|
@@ -87,6 +91,7 @@ async def get_oidc_token( | |
| return await exchange_token( | ||
| auth_db, | ||
| scope, | ||
| policies, | ||
| oidc_token_info, | ||
| config, | ||
| settings, | ||
|
|
@@ -235,6 +240,7 @@ async def perform_legacy_exchange( | |
| expected_api_key: str, | ||
| preferred_username: str, | ||
| scope: str, | ||
| policies: dict[str, Any], | ||
| authorization: str, | ||
| auth_db: AuthDB, | ||
| available_properties: set[SecurityProperty], | ||
|
|
@@ -261,6 +267,7 @@ async def perform_legacy_exchange( | |
| return await exchange_token( | ||
| auth_db, | ||
| scope, | ||
| policies, | ||
| {"sub": sub, "preferred_username": preferred_username}, | ||
| config, | ||
| settings, | ||
|
|
@@ -273,6 +280,7 @@ async def perform_legacy_exchange( | |
| async def exchange_token( | ||
| auth_db: AuthDB, | ||
| scope: str, | ||
| policies: dict[str, Any], | ||
| oidc_token_info: dict, | ||
| config: Config, | ||
| settings: AuthSettings, | ||
|
|
@@ -316,14 +324,23 @@ async def exchange_token( | |
| # Merge the VO with the subject to get a unique DIRAC sub | ||
| sub = f"{vo}:{sub}" | ||
|
|
||
| # Enrich the token with policy specific content | ||
| dirac_access_policies = {} | ||
| dirac_refresh_policies = {} | ||
| for policy_name, policy in policies.items(): | ||
| access_extra, refresh_extra = policy.enrich_tokens() | ||
| if access_extra: | ||
| dirac_access_policies[policy_name] = access_extra | ||
| if refresh_extra: | ||
| dirac_refresh_policies[policy_name] = refresh_extra | ||
|
Comment on lines
+327
to
+335
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you renew the refresh token, you don't reuse the policies that you initially stored in the DB, is that expected? (then what's the point in storing the policies in the DB?) What if the policies changed in the meantime? Then you get a different refresh token.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think so, no? The issue was asking to add the policies to the DB, since they were only JWT-side, it's true that I don't see the point in storing them in DB if we don't reuse them but I also don't see a point of reusing them if the refresh token will always be re-created by calling
Since we don't reuse the policies from the DB and the token is re-created anyway, you'll have a new token with the new policies? |
||
|
|
||
| refresh_payload: RefreshTokenPayload | None = None | ||
|
|
||
| if include_refresh_token: | ||
| # Insert the refresh token with user details into the RefreshTokens table | ||
| # User details are needed to regenerate access tokens later | ||
| refresh_jti = await insert_refresh_token( | ||
| auth_db=auth_db, | ||
| subject=sub, | ||
| scope=scope, | ||
| auth_db=auth_db, subject=sub, scope=scope, policies=dirac_refresh_policies | ||
| ) | ||
|
|
||
| # Generate refresh token payload | ||
|
|
@@ -338,7 +355,7 @@ async def exchange_token( | |
| # legacy_exchange is used to indicate that the original refresh token | ||
| # was obtained from the legacy_exchange endpoint | ||
| legacy_exchange=legacy_exchange, | ||
| dirac_policies={}, | ||
| dirac_policies=dirac_refresh_policies, | ||
| ) | ||
|
|
||
| # Generate access token payload | ||
|
|
@@ -357,7 +374,7 @@ async def exchange_token( | |
| preferred_username=preferred_username, | ||
| dirac_group=dirac_group, | ||
| exp=access_exp, | ||
| dirac_policies={}, | ||
| dirac_policies=dirac_access_policies, | ||
| ) | ||
|
|
||
| return access_payload, refresh_payload | ||
|
|
@@ -391,9 +408,7 @@ def _sign_token_payload(claims: dict, settings: AuthSettings) -> str: | |
|
|
||
|
|
||
| async def insert_refresh_token( | ||
| auth_db: AuthDB, | ||
| subject: str, | ||
| scope: str, | ||
| auth_db: AuthDB, subject: str, scope: str, policies: dict[str, Any] | ||
| ) -> UUID: | ||
| """Insert a refresh token into the database and return the JWT ID.""" | ||
| # Generate a JWT ID | ||
|
|
@@ -404,6 +419,7 @@ async def insert_refresh_token( | |
| jti=jti, | ||
| subject=subject, | ||
| scope=scope, | ||
| policies=policies, | ||
| ) | ||
| return jti | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,10 +31,6 @@ | |
| from fastapi import Depends | ||
|
|
||
| from diracx.core.extensions import DiracEntryPoint, select_from_extension | ||
| from diracx.core.models import ( | ||
| AccessTokenPayload, | ||
| RefreshTokenPayload, | ||
| ) | ||
| from diracx.core.settings import DevelopmentSettings | ||
| from diracx.routers.dependencies import auto_inject | ||
| from diracx.routers.utils import AuthorizedUserInfo, verify_dirac_access_token | ||
|
|
@@ -90,15 +86,10 @@ async def policy(policy_name: str, user_info: AuthorizedUserInfo, /): | |
| return | ||
|
|
||
| @staticmethod | ||
| def enrich_tokens( | ||
| access_payload: AccessTokenPayload, refresh_payload: RefreshTokenPayload | None | ||
| ) -> tuple[dict, dict]: | ||
| def enrich_tokens() -> tuple[dict, dict]: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason for modifying the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These changes were made due to my comment here: #950 (comment) I wasn't sure Also, if we need the payload to enrich the token, how can we do that? Because currently, the payload is created after we insert the token in the DB, and so, after we enrich it too: # Enrich the token with policy specific content
dirac_access_policies = {}
dirac_refresh_policies = {}
for policy_name, policy in policies.items():
access_extra, refresh_extra = policy.enrich_tokens() # --> with my refactor, token is enriched first here
if access_extra:
dirac_access_policies[policy_name] = access_extra
if refresh_extra:
dirac_refresh_policies[policy_name] = refresh_extra
refresh_payload: RefreshTokenPayload | None = None
if include_refresh_token:
# Insert the refresh token with user details into the RefreshTokens table
# User details are needed to regenerate access tokens later
refresh_jti = await insert_refresh_token(
auth_db=auth_db, subject=sub, scope=scope, policies=dirac_refresh_policies
) # --> we need to retrieve the JTI here
# Generate refresh token payload
if refresh_token_expire_minutes is None:
refresh_token_expire_minutes = settings.refresh_token_expire_minutes
refresh_exp = uuid7_to_datetime(refresh_jti) + timedelta(
minutes=refresh_token_expire_minutes
)
refresh_payload = RefreshTokenPayload(
jti=str(refresh_jti),
exp=refresh_exp,
# legacy_exchange is used to indicate that the original refresh token
# was obtained from the legacy_exchange endpoint
legacy_exchange=legacy_exchange,
dirac_policies=dirac_refresh_policies,
) # --> the payload is only created hereMaybe one way to do that would be to create an "update_token_policies" function and call it at the end, so it would be:
What do you think? Do you have any other idea? |
||
| """Add content to access or refresh payload when issuing a token. | ||
|
|
||
| Content can be whatever is desired inside the access or refresh payload. | ||
|
|
||
| :param access_payload: access token payload | ||
| :param refresh_payload: refresh token payload | ||
| :returns: extra content for both payload | ||
| """ | ||
| return {}, {} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be
nullby default, at least for the migration (because old refresh tokens don't havePolicies. Also it would probably bynullpermanently, am I wrong?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree about the
nullby default and I think it's automatically assigned for older values when you add the column in the DB withALTER TABLE RefreshTokens ADD COLUMN Policies JSON;(sql-alchemyonly auto-check for new tables, not new columns, I don't know if there's a setting for that), at least, it did it for my older tokens. I will re-check that if you want and add a default anyway.I don't understand that:
As I mentionned in the PR, the policies are stored correctly in the DB, it's not
null:If you're talking about this case: #950 (comment), I will take a look at it.