From c8f6c5fc32373b658231a969a9430f5623c69a02 Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Wed, 11 Dec 2024 12:53:21 -0800 Subject: [PATCH] Convert internal scopes representation to set Rather than keeping token scopes as lists internally and converting them to sets for set math, always keep token scopes as sets everywhere. This is a more natural and correct representation, but it required a lot of tedious refactoring since the lists leaked into a lot of internal APIs. --- src/gafaelfawr/cache.py | 29 ++-- src/gafaelfawr/handlers/ingress.py | 8 +- src/gafaelfawr/models/auth.py | 6 +- src/gafaelfawr/models/token.py | 8 +- src/gafaelfawr/pydantic.py | 16 +- src/gafaelfawr/services/kubernetes.py | 4 +- src/gafaelfawr/services/token.py | 62 ++++--- src/gafaelfawr/services/token_cache.py | 38 ++--- src/gafaelfawr/services/userinfo.py | 6 +- src/gafaelfawr/storage/token.py | 12 +- tests/cli_test.py | 10 +- tests/handlers/api_admins_test.py | 2 +- tests/handlers/api_history_test.py | 26 +-- tests/handlers/api_login_test.py | 2 +- tests/handlers/api_tokens_test.py | 40 ++--- tests/handlers/ingress_load_test.py | 4 +- tests/handlers/ingress_logging_test.py | 8 +- tests/handlers/ingress_test.py | 40 ++--- tests/handlers/logout_test.py | 4 +- tests/handlers/oidc_test.py | 4 +- tests/handlers/quota_test.py | 4 +- tests/operator/tokens_test.py | 6 +- tests/services/oidc_test.py | 4 +- tests/services/token_cache_test.py | 38 ++--- tests/services/token_test.py | 213 +++++++++++++------------ tests/storage/token_test.py | 2 +- tests/support/selenium.py | 2 +- tests/support/tokens.py | 16 +- 28 files changed, 315 insertions(+), 299 deletions(-) diff --git a/src/gafaelfawr/cache.py b/src/gafaelfawr/cache.py index d685b5d7c..056bc275d 100644 --- a/src/gafaelfawr/cache.py +++ b/src/gafaelfawr/cache.py @@ -335,18 +335,18 @@ class InternalTokenCache(TokenCache): """Cache for internal tokens.""" def get( - self, token_data: TokenData, service: str, scopes: list[str] + self, token_data: TokenData, service: str, scopes: set[str] ) -> Token | None: """Retrieve an internal token from the cache. Parameters ---------- token_data - The authentication data for the parent token. + Authentication data for the parent token. service - The service of the internal token. + Service of the internal token. scopes - The scopes the internal token should have. + Scopes the internal token should have. Returns ------- @@ -367,7 +367,7 @@ def store( self, token_data: TokenData, service: str, - scopes: list[str], + scopes: set[str], token: Token, ) -> None: """Store an internal token in the cache. @@ -377,19 +377,19 @@ def store( Parameters ---------- token_data - The authentication data for the parent token. + Authentication data for the parent token. service - The service of the internal token. + Service of the internal token. scopes - The scopes the internal token should have. + Scopes the internal token should have. token - The token to cache. + Token to cache. """ key = self._build_key(token_data, service, scopes) self._cache[key] = token def _build_key( - self, token_data: TokenData, service: str, scopes: list[str] + self, token_data: TokenData, service: str, scopes: set[str] ) -> tuple[str, ...]: """Build the cache key for an internal token. @@ -407,9 +407,12 @@ def _build_key( tuple An object suitable for use as a hash key for this internal token. """ - expires = str(token_data.expires) if token_data.expires else "None" - scope = ",".join(sorted(scopes)) - return (token_data.token.key, expires, service, scope) + return ( + token_data.token.key, + str(token_data.expires) if token_data.expires else "None", + service, + " ".join(sorted(scopes)), + ) class NotebookTokenCache(TokenCache): diff --git a/src/gafaelfawr/handlers/ingress.py b/src/gafaelfawr/handlers/ingress.py index 38bdc46f8..61938c9eb 100644 --- a/src/gafaelfawr/handlers/ingress.py +++ b/src/gafaelfawr/handlers/ingress.py @@ -597,18 +597,18 @@ async def build_delegated_token( return str(token) elif auth_config.delegate_to: # Delegated scopes are optional; if the authenticating token doesn't - # have the scope, it's omitted from the delegated token. (To make it + # have the scope, it's omitted from the delegated token. (To make it # mandatory, require that scope via the scope parameter as well, and - # then the authenticating token will always have it.) Therefore, + # then the authenticating token will always have it.) Therefore, # reduce the scopes of the internal token to the intersection between # the requested delegated scopes and the scopes of the authenticating # token. - delegate_scopes = auth_config.delegate_scopes & set(token_data.scopes) + delegate_scopes = auth_config.delegate_scopes & token_data.scopes token_service = context.factory.create_token_service() token = await token_service.get_internal_token( token_data, service=auth_config.delegate_to, - scopes=sorted(delegate_scopes), + scopes=delegate_scopes, ip_address=context.ip_address, minimum_lifetime=auth_config.minimum_lifetime, ) diff --git a/src/gafaelfawr/models/auth.py b/src/gafaelfawr/models/auth.py index 45cbc7f86..f526ffcf1 100644 --- a/src/gafaelfawr/models/auth.py +++ b/src/gafaelfawr/models/auth.py @@ -8,6 +8,8 @@ from pydantic import BaseModel, Field +from ..pydantic import Scopes + __all__ = [ "APIConfig", "APILoginResponse", @@ -165,11 +167,11 @@ class APILoginResponse(BaseModel): examples=["someuser"], ) - scopes: list[str] = Field( + scopes: Scopes = Field( ..., title="Access scopes", description="Access scopes for this authenticated user", - examples=["read:all", "user:token"], + examples=[["read:all", "user:token"]], ) config: APIConfig = Field( diff --git a/src/gafaelfawr/models/token.py b/src/gafaelfawr/models/token.py index 246f39fdf..c34f07bf2 100644 --- a/src/gafaelfawr/models/token.py +++ b/src/gafaelfawr/models/token.py @@ -313,7 +313,7 @@ def bootstrap_token(cls) -> Self: token=Token(), username="", token_type=TokenType.service, - scopes=["admin:token"], + scopes={"admin:token"}, ) @classmethod @@ -332,7 +332,7 @@ def internal_token(cls) -> Self: token=Token(), username="", token_type=TokenType.service, - scopes=["admin:token"], + scopes={"admin:token"}, ) @@ -380,7 +380,7 @@ class AdminTokenRequest(BaseModel): ) scopes: Scopes = Field( - [], + set(), title="Token scopes", examples=[["read:all"]], ) @@ -486,7 +486,7 @@ class UserTokenRequest(BaseModel): ) scopes: Scopes = Field( - [], + set(), title="Token scope", examples=[["read:all"]], ) diff --git a/src/gafaelfawr/pydantic.py b/src/gafaelfawr/pydantic.py index 82f4d2115..97f1b5d68 100644 --- a/src/gafaelfawr/pydantic.py +++ b/src/gafaelfawr/pydantic.py @@ -47,7 +47,7 @@ def _normalize_ip_address(v: str | IPv4Address | IPv6Address) -> str: """ -def _normalize_scopes(v: str | Iterable[str]) -> list[str]: +def _normalize_scopes(v: str | Iterable[str]) -> set[str]: """Pydantic validator for scope fields. Scopes are stored in the database as a comma-delimited, sorted list. @@ -65,15 +65,21 @@ def _normalize_scopes(v: str | Iterable[str]) -> list[str]: Scopes as a set. """ if isinstance(v, str): - return [] if not v else sorted(v.split(",")) + return set() if not v else set(v.split(",")) else: - return sorted(v) + return set(v) -Scopes: TypeAlias = Annotated[list[str], PlainValidator(_normalize_scopes)] +Scopes: TypeAlias = Annotated[ + set[str], + PlainValidator(_normalize_scopes), + PlainSerializer( + lambda s: sorted(s), return_type=list[str], when_used="json" + ), +] """Type for a list of scopes. -The scopes will be forced to sorted order by validation. +The scopes will be forced to sorted order on serialization. """ diff --git a/src/gafaelfawr/services/kubernetes.py b/src/gafaelfawr/services/kubernetes.py index fcd03fb74..a794776d4 100644 --- a/src/gafaelfawr/services/kubernetes.py +++ b/src/gafaelfawr/services/kubernetes.py @@ -327,7 +327,7 @@ async def _create_token(self, parent: GafaelfawrServiceToken) -> Token: request = AdminTokenRequest( username=parent.spec.service, token_type=TokenType.service, - scopes=parent.spec.scopes, + scopes=set(parent.spec.scopes), ) return await self._token_service.create_token_from_admin_request( request, TokenData.internal_token(), ip_address=None @@ -342,7 +342,7 @@ async def _is_token_valid( return False if token_data.username != parent.spec.service: return False - return sorted(token_data.scopes) == sorted(parent.spec.scopes) + return token_data.scopes == set(parent.spec.scopes) async def _secret_needs_update( self, parent: GafaelfawrServiceToken, secret: V1Secret | None diff --git a/src/gafaelfawr/services/token.py b/src/gafaelfawr/services/token.py index dea9d6499..37e19a8fa 100644 --- a/src/gafaelfawr/services/token.py +++ b/src/gafaelfawr/services/token.py @@ -186,7 +186,7 @@ async def audit(self, *, fix: bool = False) -> list[str]: return alerts async def create_session_token( - self, user_info: TokenUserInfo, *, scopes: list[str], ip_address: str + self, user_info: TokenUserInfo, *, scopes: set[str], ip_address: str ) -> Token: """Create a new session token. @@ -217,9 +217,7 @@ async def create_session_token( async with self._session.begin(): admins = await self._admin_store.list() if any(user_info.username == a.username for a in admins): - scopes = sorted({*scopes, "admin:token"}) - else: - scopes = sorted(scopes) + scopes.add("admin:token") data = TokenData( token=token, @@ -256,7 +254,7 @@ async def create_session_token( token_key=token.key, token_username=data.username, token_expires=format_datetime_for_logging(expires), - token_scopes=scopes, + token_scopes=sorted(scopes), token_userinfo=data.to_userinfo_dict(), ) @@ -294,7 +292,7 @@ async def create_oidc_token( token=token, username=auth_data.username, token_type=TokenType.oidc, - scopes=[], + scopes=set(), created=created, expires=expires, name=auth_data.name, @@ -342,7 +340,7 @@ async def create_user_token( username: str, *, token_name: str, - scopes: list[str], + scopes: set[str], expires: datetime | None = None, ip_address: str, ) -> Token: @@ -393,7 +391,6 @@ async def create_user_token( self._validate_scopes(scopes, auth_data) if expires: expires = expires.replace(microsecond=0) - scopes = sorted(scopes) token = Token() created = current_datetime() @@ -498,7 +495,7 @@ async def create_token_from_admin_request( token=token, username=request.username, token_type=request.token_type, - scopes=sorted(request.scopes), + scopes=request.scopes, created=created, expires=expires, name=request.name, @@ -537,7 +534,7 @@ async def create_token_from_admin_request( token_username=request.username, token_expires=format_datetime_for_logging(expires), token_name=request.token_name, - token_scopes=data.scopes, + token_scopes=sorted(data.scopes), token_userinfo=data.to_userinfo_dict(), ) else: @@ -546,7 +543,7 @@ async def create_token_from_admin_request( token_key=token.key, token_username=request.username, token_expires=format_datetime_for_logging(expires), - token_scopes=data.scopes, + token_scopes=sorted(data.scopes), token_userinfo=data.to_userinfo_dict(), ) return token @@ -751,7 +748,7 @@ async def get_internal_token( self, token_data: TokenData, service: str, - scopes: list[str], + scopes: set[str], *, ip_address: str, minimum_lifetime: timedelta | None = None, @@ -783,7 +780,6 @@ async def get_internal_token( """ self._validate_scopes(scopes, token_data) self._validate_username(token_data.username) - scopes = sorted(scopes) return await self._token_cache.get_internal_token( token_data, service, @@ -919,7 +915,7 @@ async def modify_token( *, ip_address: str, token_name: str | None = None, - scopes: list[str] | None = None, + scopes: set[str] | None = None, expires: datetime | None = None, no_expire: bool = False, ) -> TokenInfo | None: @@ -992,7 +988,7 @@ async def modify_token( username=info.username, token_type=TokenType.user, token_name=token_name if token_name else info.token_name, - scopes=sorted(scopes) if scopes is not None else info.scopes, + scopes=scopes if scopes is not None else info.scopes, expires=info.expires if not (expires or no_expire) else expires, actor=auth_data.username, action=TokenChange.edit, @@ -1006,7 +1002,7 @@ async def modify_token( info = await self._token_db_store.modify( key, token_name=token_name, - scopes=sorted(scopes) if scopes else scopes, + scopes=scopes, expires=expires, no_expire=no_expire, ) @@ -1111,7 +1107,7 @@ async def _audit_token( mismatches.append("username") if db.token_type != redis.token_type: mismatches.append("type") - if db.scopes != sorted(redis.scopes): + if db.scopes != redis.scopes: # There was a bug where Redis wasn't updated when the scopes were # changed but the database was. Redis is canonical, so set the # database scopes to match. @@ -1168,19 +1164,18 @@ def _audit_unknown_scopes(self, tokens: Iterable[TokenData]) -> list[str]: alerts = [] for token_data in tokens: known_scopes = set(self._config.known_scopes.keys()) - for scope in token_data.scopes: - if scope not in known_scopes: - self._logger.warning( - "Token has unknown scope", - token=token_data.token.key, - user=token_data.username, - scope=scope, - ) - alerts.append( - f"Token `{token_data.token.key}` for" - f" `{token_data.username}` has unknown scope" - f" (`{scope}`)" - ) + for scope in token_data.scopes - known_scopes: + self._logger.warning( + "Token has unknown scope", + token=token_data.token.key, + user=token_data.username, + scope=scope, + ) + alerts.append( + f"Token `{token_data.token.key}` for" + f" `{token_data.username}` has unknown scope" + f" (`{scope}`)" + ) return alerts def _check_authorization( @@ -1419,7 +1414,7 @@ def _validate_expires(self, expires: datetime | None) -> None: def _validate_scopes( self, - scopes: list[str], + scopes: set[str], auth_data: TokenData | None = None, ) -> None: """Check that the requested scopes are valid. @@ -1439,12 +1434,11 @@ def _validate_scopes( """ if not scopes: return - scopes_set = set(scopes) if auth_data and "admin:token" not in auth_data.scopes: - if not (scopes_set <= set(auth_data.scopes)): + if not (scopes <= auth_data.scopes): msg = "Requested scopes are broader than your current scopes" raise InvalidScopesError(msg) - if not (scopes_set <= self._config.known_scopes.keys()): + if not (scopes <= set(self._config.known_scopes.keys())): msg = "Unknown scopes requested" raise InvalidScopesError(msg) diff --git a/src/gafaelfawr/services/token_cache.py b/src/gafaelfawr/services/token_cache.py index 3ceb379c5..6543709f7 100644 --- a/src/gafaelfawr/services/token_cache.py +++ b/src/gafaelfawr/services/token_cache.py @@ -100,7 +100,7 @@ async def get_internal_token( self, token_data: TokenData, service: str, - scopes: list[str], + scopes: set[str], ip_address: str, *, minimum_lifetime: timedelta | None = None, @@ -118,20 +118,20 @@ async def get_internal_token( Parameters ---------- token_data - The authentication data for the parent token. + Authentication data for the parent token. service - The service of the internal token. + Service of the internal token. scopes - The scopes the internal token should have. + Scopes the internal token should have. ip_address - The IP address from which the request came. + IP address from which the request came. minimum_lifetime If set, the minimum required lifetime of the token. Returns ------- Token - The cached token or newly-created token. + Cached token or newly-created token. """ # Awkward code is to convince mypy that token is not None. token = self._internal_cache.get(token_data, service, scopes) @@ -200,7 +200,7 @@ async def _create_internal_token( self, token_data: TokenData, service: str, - scopes: list[str], + scopes: set[str], ip_address: str, minimum_lifetime: timedelta | None = None, ) -> Token: @@ -213,20 +213,20 @@ async def _create_internal_token( Parameters ---------- token_data - The authentication data for the parent token. + Authentication data for the parent token. service - The service of the internal token. + Service of the internal token. scopes - The scopes the internal token should have. + Scopes the internal token should have. ip_address - The IP address from which the request came. + IP address from which the request came. minimum_lifetime If set, the minimum required lifetime of the token. Returns ------- Token - The retrieved or newly-created internal token. + Retrieved or newly-created internal token. """ # See if there's already a matching internal token. key = await self._token_db_store.get_internal_token_key( @@ -381,7 +381,7 @@ async def _is_token_valid( self, token: Token | None, minimum_lifetime: timedelta | None = None, - scopes: list[str] | None = None, + scopes: set[str] | None = None, ) -> bool: """Check whether a token is valid. @@ -392,13 +392,13 @@ async def _is_token_valid( Parameters ---------- token - The token to check for validity. `None` is accepted to simplify - type checking, but will always return `False`. + Token to check for validity. `None` is accepted to simplify type + checking, but will always return `False`. scopes If provided, ensure that the token has scopes that are a subset of - this scope list. This is used to force a cache miss if an - internal token is requested but the requesting token no longer has - the scopes that the internal token provides. + this scope list. This is used to force a cache miss if an internal + token is requested but the requesting token no longer has the + scopes that the internal token provides. minimum_lifetime If set, the minimum required lifetime of the token. @@ -412,7 +412,7 @@ async def _is_token_valid( data = await self._token_redis_store.get_data(token) if not data: return False - if scopes is not None and not (set(data.scopes) <= set(scopes)): + if scopes is not None and not (data.scopes <= scopes): return False if data.expires: if minimum_lifetime: diff --git a/src/gafaelfawr/services/userinfo.py b/src/gafaelfawr/services/userinfo.py index 19db62693..760aed112 100644 --- a/src/gafaelfawr/services/userinfo.py +++ b/src/gafaelfawr/services/userinfo.py @@ -123,7 +123,7 @@ async def get_user_info_from_token( quota=self._calculate_quota(groups), ) - async def get_scopes(self, user_info: TokenUserInfo) -> list[str] | None: + async def get_scopes(self, user_info: TokenUserInfo) -> set[str] | None: """Get scopes from user information. Used to determine the scope claim of a token issued based on an OpenID @@ -136,7 +136,7 @@ async def get_scopes(self, user_info: TokenUserInfo) -> list[str] | None: Returns ------- - list of str or None + set of str or None The scopes generated from the group membership based on the ``group_mapping`` configuration parameter, or `None` if the user was not a member of any known group. @@ -157,7 +157,7 @@ async def get_scopes(self, user_info: TokenUserInfo) -> list[str] | None: for group in groups: scopes.update(self._config.get_scopes_for_group(group)) - return sorted(scopes | {"user:token"}) if scopes else None + return (scopes | {"user:token"}) if scopes else None async def invalidate_cache(self, username: str) -> None: """Invalidate any cached data for a given user. diff --git a/src/gafaelfawr/storage/token.py b/src/gafaelfawr/storage/token.py index 889448efe..cfe1303a1 100644 --- a/src/gafaelfawr/storage/token.py +++ b/src/gafaelfawr/storage/token.py @@ -235,7 +235,7 @@ async def get_internal_token_key( self, token_data: TokenData, service: str, - scopes: list[str], + scopes: set[str], min_expires: datetime, ) -> str | None: """Retrieve an existing internal child token. @@ -398,7 +398,7 @@ async def modify( key: str, *, token_name: str | None = None, - scopes: list[str] | None = None, + scopes: set[str] | None = None, expires: datetime | None = None, no_expire: bool = False, ) -> TokenInfo | None: @@ -407,13 +407,13 @@ async def modify( Parameters ---------- token - The token to modify. + Token to modify. token_name - The new name for the token. + New name for the token. scopes - The new scopes for the token. + New scopes for the token. expires - The new expiration time for the token. + New expiration time for the token. no_expire If set, the token should not expire. This is a separate parameter because passing `None` to ``expires`` is ambiguous. diff --git a/tests/cli_test.py b/tests/cli_test.py index aabedeef5..16dcfe800 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -55,7 +55,7 @@ def test_audit( token=Token(), username="some-user", token_type=TokenType.session, - scopes=["user:token"], + scopes={"user:token"}, created=now, expires=now + timedelta(days=7), ) @@ -128,7 +128,7 @@ async def setup() -> OIDCAuthorizationCode: token_service = factory.create_token_service() user_info = TokenUserInfo(username="some-user") token = await token_service.create_session_token( - user_info, scopes=[], ip_address="127.0.0.1" + user_info, scopes=set(), ip_address="127.0.0.1" ) oidc_service = factory.create_oidc_service() return await oidc_service.issue_code( @@ -249,7 +249,7 @@ def test_maintenance( token=Token(), username="some-user", token_type=TokenType.session, - scopes=["read:all", "user:token"], + scopes={"read:all", "user:token"}, created=now - timedelta(minutes=60), expires=now - timedelta(minutes=30), ) @@ -257,7 +257,7 @@ def test_maintenance( token=Token(), username="some-user", token_type=TokenType.session, - scopes=["read:all", "user:token"], + scopes={"read:all", "user:token"}, created=now - timedelta(minutes=60), expires=now + timedelta(minutes=30), ) @@ -265,7 +265,7 @@ def test_maintenance( token=Token().key, username="other-user", token_type=TokenType.session, - scopes=[], + scopes=set(), expires=now - CHANGE_HISTORY_RETENTION + timedelta(days=10), actor="other-user", action=TokenChange.create, diff --git a/tests/handlers/api_admins_test.py b/tests/handlers/api_admins_test.py index 192dffa92..ff01a4f49 100644 --- a/tests/handlers/api_admins_test.py +++ b/tests/handlers/api_admins_test.py @@ -31,7 +31,7 @@ async def test_admins( "type": "permission_denied", } - token_data = await create_session_token(factory, scopes=["admin:token"]) + token_data = await create_session_token(factory, scopes={"admin:token"}) r = await client.get( "/auth/api/v1/admins", headers={"Authorization": f"bearer {token_data.token}"}, diff --git a/tests/handlers/api_history_test.py b/tests/handlers/api_history_test.py index 35a2f9b25..ae3591c44 100644 --- a/tests/handlers/api_history_test.py +++ b/tests/handlers/api_history_test.py @@ -38,7 +38,7 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: user_info_one = TokenUserInfo(username="one") token_one = await token_service.create_session_token( user_info_one, - scopes=["admin:token", "exec:test", "read:all", "user:token"], + scopes={"admin:token", "exec:test", "read:all", "user:token"}, ip_address="192.0.2.3", ) token_data_one = await token_service.get_data(token_one) @@ -46,11 +46,11 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: await token_service.get_internal_token( token_data_one, "foo", - scopes=["exec:test", "read:all"], + scopes={"exec:test", "read:all"}, ip_address="192.0.2.4", ) internal_token_one_bar = await token_service.get_internal_token( - token_data_one, "bar", scopes=["read:all"], ip_address="192.0.2.3" + token_data_one, "bar", scopes={"read:all"}, ip_address="192.0.2.3" ) token_data_internal_one_bar = await token_service.get_data( internal_token_one_bar @@ -59,7 +59,7 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: await token_service.get_internal_token( token_data_internal_one_bar, "baz", - scopes=[], + scopes=set(), ip_address="10.10.10.10", ) notebook_token_one = await token_service.get_notebook_token( @@ -70,14 +70,14 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: await token_service.get_internal_token( token_data_notebook_one, "foo", - scopes=["exec:test"], + scopes={"exec:test"}, ip_address="10.10.10.20", ) user_info_two = TokenUserInfo(username="two") token_two = await token_service.create_session_token( user_info_two, - scopes=["admin:token", "read:some", "user:token"], + scopes={"admin:token", "read:some", "user:token"}, ip_address="192.0.2.20", ) token_data_two = await token_service.get_data(token_two) @@ -86,7 +86,7 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: token_data_two, token_data_two.username, token_name="some token", - scopes=["admin:token", "read:some", "user:token"], + scopes={"admin:token", "read:some", "user:token"}, ip_address="192.0.2.20", ) token_data_user_two = await token_service.get_data(user_token_two) @@ -94,7 +94,7 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: await token_service.get_internal_token( token_data_user_two, "foo", - scopes=["read:some"], + scopes={"read:some"}, ip_address="10.10.10.10", ) assert await token_service.modify_token( @@ -108,7 +108,7 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: request = AdminTokenRequest( username="bot-service", token_type=TokenType.service, - scopes=["admin:token"], + scopes={"admin:token"}, ) service_token = await token_service.create_token_from_admin_request( request, @@ -121,7 +121,7 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: user_token_two.key, service_token_data, ip_address="2001:db8:034a:ea78:4278:4562:6578:9876", - scopes=["admin:token", "read:all"], + scopes={"admin:token", "read:all"}, ) assert await token_service.modify_token( user_token_two.key, @@ -129,7 +129,7 @@ async def build_history(factory: Factory) -> list[TokenChangeHistoryEntry]: ip_address="2001:db8:034a:ea78:4278:4562:6578:af42", token_name="other name", expires=current_datetime() + timedelta(days=30), - scopes=["read:all"], + scopes={"read:all"}, ) assert await token_service.delete_token( token_one.key, @@ -263,7 +263,7 @@ async def check_pagination( async def test_admin_change_history( client: AsyncClient, factory: Factory ) -> None: - token_data = await create_session_token(factory, scopes=["admin:token"]) + token_data = await create_session_token(factory, scopes={"admin:token"}) await set_session_cookie(client, token_data.token) history = await build_history(factory) @@ -507,7 +507,7 @@ async def test_no_scope( token_data, token_data.username, token_name="user", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) diff --git a/tests/handlers/api_login_test.py b/tests/handlers/api_login_test.py index 3d5798505..6c3cbff04 100644 --- a/tests/handlers/api_login_test.py +++ b/tests/handlers/api_login_test.py @@ -26,7 +26,7 @@ async def test_login( client: AsyncClient, config: Config, factory: Factory ) -> None: token_data = await create_session_token( - factory, username="example", scopes=["read:all", "exec:admin"] + factory, username="example", scopes={"read:all", "exec:admin"} ) cookie = State(token=token_data.token).to_cookie() client.cookies.set(COOKIE_NAME, cookie, domain=TEST_HOSTNAME) diff --git a/tests/handlers/api_tokens_test.py b/tests/handlers/api_tokens_test.py index 31c0bb182..7b1272695 100644 --- a/tests/handlers/api_tokens_test.py +++ b/tests/handlers/api_tokens_test.py @@ -41,7 +41,7 @@ async def test_create_delete_modify( token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["read:all", "exec:admin", "user:token"], + scopes={"read:all", "exec:admin", "user:token"}, ip_address="127.0.0.1", ) csrf = await set_session_cookie(client, session_token) @@ -157,7 +157,7 @@ async def test_create_delete_modify( # Get a token admin token, which will be allowed to modify the token. admin_token = await token_service.create_session_token( - user_info, scopes=["admin:token"], ip_address="127.0.0.1" + user_info, scopes={"admin:token"}, ip_address="127.0.0.1" ) csrf = await set_session_cookie(client, admin_token) @@ -303,7 +303,7 @@ async def test_token_info( token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["exec:admin", "user:token"], + scopes={"exec:admin", "user:token"}, ip_address="127.0.0.1", ) @@ -350,7 +350,7 @@ async def test_token_info( data, data.username, token_name="some-token", - scopes=["exec:admin"], + scopes={"exec:admin"}, expires=expires, ip_address="127.0.0.1", ) @@ -443,14 +443,14 @@ async def test_auth_required( async def test_csrf_required( client: AsyncClient, factory: Factory, mock_slack: MockSlackWebhook ) -> None: - token_data = await create_session_token(factory, scopes=["admin:token"]) + token_data = await create_session_token(factory, scopes={"admin:token"}) csrf = await set_session_cookie(client, token_data.token) token_service = factory.create_token_service() user_token = await token_service.create_user_token( token_data, token_data.username, token_name="foo", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) @@ -563,7 +563,7 @@ async def test_no_scope( token_data, token_data.username, token_name="user", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) @@ -634,7 +634,7 @@ async def test_wrong_user( username="other-person", name="Some Other Person", uid=137123 ) other_session_token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" + user_info, scopes={"user:token"}, ip_address="127.0.0.1" ) other_session_data = await token_service.get_data(other_session_token) assert other_session_data @@ -642,7 +642,7 @@ async def test_wrong_user( other_session_data, "other-person", token_name="foo", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) @@ -724,7 +724,7 @@ async def test_wrong_user( async def test_no_expires(client: AsyncClient, factory: Factory) -> None: """Test creating a user token that doesn't expire.""" token_data = await create_session_token( - factory, scopes=["admin:token", "user:token"] + factory, scopes={"admin:token", "user:token"} ) csrf = await set_session_cookie(client, token_data.token) @@ -779,7 +779,7 @@ async def test_duplicate_token_name( ) -> None: """Test duplicate token names.""" token_data = await create_session_token( - factory, scopes=["admin:token", "user:token"] + factory, scopes={"admin:token", "user:token"} ) csrf = await set_session_cookie(client, token_data.token) @@ -824,7 +824,7 @@ async def test_bad_expires( ) -> None: """Test creating or modifying a token with bogus expirations.""" token_data = await create_session_token( - factory, scopes=["user:token", "admin:token"] + factory, scopes={"user:token", "admin:token"} ) csrf = await set_session_cookie(client, token_data.token) @@ -877,7 +877,7 @@ async def test_bad_scopes( known_scopes = list(config.known_scopes.keys()) assert len(known_scopes) > 4 token_data = await create_session_token( - factory, scopes=known_scopes[:3] + ["other:scope", "user:token"] + factory, scopes=set(known_scopes[:3]) | {"other:scope", "user:token"} ) csrf = await set_session_cookie(client, token_data.token) @@ -927,7 +927,7 @@ async def test_create_admin( caplog: pytest.LogCaptureFixture, ) -> None: """Test creating a token through the admin interface.""" - token_data = await create_session_token(factory, scopes=["exec:admin"]) + token_data = await create_session_token(factory, scopes={"exec:admin"}) csrf = await set_session_cookie(client, token_data.token) r = await client.post( @@ -937,7 +937,7 @@ async def test_create_admin( ) assert r.status_code == 403 - token_data = await create_session_token(factory, scopes=["admin:token"]) + token_data = await create_session_token(factory, scopes={"admin:token"}) csrf = await set_session_cookie(client, token_data.token) # Intentionally pass in a datetime with microseconds. They should be @@ -1164,7 +1164,7 @@ async def test_create_admin_ldap( ) -> None: """Create a token through the admin interface with LDAP user data.""" await reconfigure("oidc", factory) - token_data = await create_session_token(factory, scopes=["admin:token"]) + token_data = await create_session_token(factory, scopes={"admin:token"}) csrf = await set_session_cookie(client, token_data.token) mock_ldap.add_test_user( UserInfo( @@ -1282,7 +1282,7 @@ async def test_create_admin_firestore( await reconfigure("oidc-firestore", factory) firestore_storage = factory.create_firestore_storage() await firestore_storage.initialize() - token_data = await create_session_token(factory, scopes=["admin:token"]) + token_data = await create_session_token(factory, scopes={"admin:token"}) csrf = await set_session_cookie(client, token_data.token) # Create a new service token with no user metadata. @@ -1342,7 +1342,7 @@ async def test_no_form_post( token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["read:all", "exec:admin", "user:token"], + scopes={"read:all", "exec:admin", "user:token"}, ip_address="127.0.0.1", ) csrf = await set_session_cookie(client, session_token) @@ -1384,7 +1384,7 @@ async def test_scope_modify( token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["admin:token", "read:all", "exec:admin", "user:token"], + scopes={"admin:token", "read:all", "exec:admin", "user:token"}, ip_address="127.0.0.1", ) csrf = await set_session_cookie(client, session_token) @@ -1481,7 +1481,7 @@ async def test_ldap_error( [{"uidNumber": ["bogus"]}], ) token_data = await create_session_token( - factory, username="ldap-user", scopes=["read:all"], minimal=True + factory, username="ldap-user", scopes={"read:all"}, minimal=True ) await set_session_cookie(client, token_data.token) diff --git a/tests/handlers/ingress_load_test.py b/tests/handlers/ingress_load_test.py index 5a69c11dc..242561517 100644 --- a/tests/handlers/ingress_load_test.py +++ b/tests/handlers/ingress_load_test.py @@ -22,7 +22,7 @@ @pytest.mark.asyncio async def test_notebook(client: AsyncClient, factory: Factory) -> None: data = await create_session_token( - factory, scopes=["exec:test", "read:all"] + factory, scopes={"exec:test", "read:all"} ) await set_session_cookie(client, data.token) @@ -41,7 +41,7 @@ async def test_notebook(client: AsyncClient, factory: Factory) -> None: @pytest.mark.asyncio async def test_internal(client: AsyncClient, factory: Factory) -> None: data = await create_session_token( - factory, scopes=["exec:test", "read:all"] + factory, scopes={"exec:test", "read:all"} ) await set_session_cookie(client, data.token) diff --git a/tests/handlers/ingress_logging_test.py b/tests/handlers/ingress_logging_test.py index a82d99e4e..34be76657 100644 --- a/tests/handlers/ingress_logging_test.py +++ b/tests/handlers/ingress_logging_test.py @@ -21,7 +21,7 @@ async def test_success( client: AsyncClient, factory: Factory, caplog: pytest.LogCaptureFixture ) -> None: - token_data = await create_session_token(factory, scopes=["exec:admin"]) + token_data = await create_session_token(factory, scopes={"exec:admin"}) # Successful request with X-Forwarded-For and a bearer token. caplog.clear() @@ -94,7 +94,7 @@ async def test_success( async def test_authz_failed( client: AsyncClient, factory: Factory, caplog: pytest.LogCaptureFixture ) -> None: - token_data = await create_session_token(factory, scopes=["exec:admin"]) + token_data = await create_session_token(factory, scopes={"exec:admin"}) caplog.clear() r = await client.get( @@ -264,7 +264,7 @@ async def test_notebook( client: AsyncClient, factory: Factory, caplog: pytest.LogCaptureFixture ) -> None: token_data = await create_session_token( - factory, group_names=["admin"], scopes=["exec:admin", "read:all"] + factory, group_names=["admin"], scopes={"exec:admin", "read:all"} ) assert token_data.expires @@ -330,7 +330,7 @@ async def test_internal( client: AsyncClient, factory: Factory, caplog: pytest.LogCaptureFixture ) -> None: token_data = await create_session_token( - factory, group_names=["admin"], scopes=["exec:admin", "read:all"] + factory, group_names=["admin"], scopes={"exec:admin", "read:all"} ) assert token_data.expires diff --git a/tests/handlers/ingress_test.py b/tests/handlers/ingress_test.py index 48193e5f9..fad6d86a6 100644 --- a/tests/handlers/ingress_test.py +++ b/tests/handlers/ingress_test.py @@ -179,7 +179,7 @@ async def test_satisfy_all( factory: Factory, mock_slack: MockSlackWebhook, ) -> None: - token_data = await create_session_token(factory, scopes=["exec:test"]) + token_data = await create_session_token(factory, scopes={"exec:test"}) r = await client.get( "/ingress/auth", @@ -203,7 +203,7 @@ async def test_satisfy_all( @pytest.mark.asyncio async def test_success(client: AsyncClient, factory: Factory) -> None: token_data = await create_session_token( - factory, group_names=["admin"], scopes=["exec:admin", "read:all"] + factory, group_names=["admin"], scopes={"exec:admin", "read:all"} ) r = await client.get( @@ -230,7 +230,7 @@ async def test_success_minimal(client: AsyncClient, factory: Factory) -> None: user_info = TokenUserInfo(username="user", uid=1234) token_service = factory.create_token_service() token = await token_service.create_session_token( - user_info, scopes=["read:all"], ip_address="127.0.0.1" + user_info, scopes={"read:all"}, ip_address="127.0.0.1" ) r = await client.get( @@ -246,7 +246,7 @@ async def test_success_minimal(client: AsyncClient, factory: Factory) -> None: @pytest.mark.asyncio async def test_notebook(client: AsyncClient, factory: Factory) -> None: token_data = await create_session_token( - factory, group_names=["admin"], scopes=["exec:admin", "read:all"] + factory, group_names=["admin"], scopes={"exec:admin", "read:all"} ) assert token_data.expires assert token_data.groups @@ -304,7 +304,7 @@ async def test_internal(client: AsyncClient, factory: Factory) -> None: token_data = await create_session_token( factory, group_names=["admin"], - scopes=["exec:admin", "read:all", "read:some"], + scopes={"exec:admin", "read:all", "read:some"}, ) assert token_data.expires assert token_data.groups @@ -379,7 +379,7 @@ async def test_internal(client: AsyncClient, factory: Factory) -> None: @pytest.mark.asyncio async def test_internal_scopes(client: AsyncClient, factory: Factory) -> None: """Delegated scopes are optional and dropped if not available.""" - token_data = await create_session_token(factory, scopes=["read:some"]) + token_data = await create_session_token(factory, scopes={"read:some"}) assert token_data.expires r = await client.get( @@ -445,7 +445,7 @@ async def test_internal_scopes(client: AsyncClient, factory: Factory) -> None: async def test_internal_errors( client: AsyncClient, factory: Factory, mock_slack: MockSlackWebhook ) -> None: - token_data = await create_session_token(factory, scopes=["read:some"]) + token_data = await create_session_token(factory, scopes={"read:some"}) # Cannot request a notebook token and an internal token at the same time. r = await client.get( @@ -485,7 +485,7 @@ async def test_success_any(client: AsyncClient, factory: Factory) -> None: with only ``exec:test``. Ensure they are accepted. """ token_data = await create_session_token( - factory, group_names=["test"], scopes=["exec:test"] + factory, group_names=["test"], scopes={"exec:test"} ) r = await client.get( @@ -506,7 +506,7 @@ async def test_basic( client: AsyncClient, config: Config, factory: Factory ) -> None: token_data = await create_session_token( - factory, group_names=["test"], scopes=["exec:admin"] + factory, group_names=["test"], scopes={"exec:admin"} ) basic = f"{token_data.token}:blahblahblah".encode() @@ -632,7 +632,7 @@ async def test_success_unicode_name( user_info = TokenUserInfo(username="user", uid=1234, name="名字") token_service = factory.create_token_service() token = await token_service.create_session_token( - user_info, scopes=["read:all"], ip_address="127.0.0.1" + user_info, scopes={"read:all"}, ip_address="127.0.0.1" ) r = await client.get( @@ -652,7 +652,7 @@ async def test_minimum_lifetime( token_service = factory.create_token_service() token = await token_service.create_session_token( user_info, - scopes=["read:all", "user:token"], + scopes={"read:all", "user:token"}, ip_address="127.0.0.1", ) token_data = await token_service.get_data(token) @@ -680,7 +680,7 @@ async def test_minimum_lifetime( token_data, "user", token_name="token", - scopes=["read:all"], + scopes={"read:all"}, expires=expires, ip_address="127.0.0.1", ) @@ -746,7 +746,7 @@ async def test_default_minimum_lifetime( # change Redis, which is canonical; no need to change the database as # well. token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" + user_info, scopes={"user:token"}, ip_address="127.0.0.1" ) token_data = await token_service.get_data(token) assert token_data @@ -790,7 +790,7 @@ async def test_default_minimum_lifetime( async def test_authorization_filtering( client: AsyncClient, factory: Factory ) -> None: - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) r = await client.get( "/ingress/auth", @@ -865,7 +865,7 @@ async def test_authorization_filtering( @pytest.mark.asyncio async def test_cookie_filtering(client: AsyncClient, factory: Factory) -> None: - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) await set_session_cookie(client, token_data.token) r = await client.get("/ingress/auth", params={"scope": "read:all"}) @@ -912,7 +912,7 @@ async def test_cookie_filtering(client: AsyncClient, factory: Factory) -> None: async def test_delegate_authorization( client: AsyncClient, factory: Factory ) -> None: - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) r = await client.get( "/ingress/auth", @@ -963,7 +963,7 @@ async def test_delegate_authorization( @pytest.mark.asyncio async def test_anonymous(client: AsyncClient, factory: Factory) -> None: - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) await set_session_cookie(client, token_data.token) r = await client.get("/ingress/anonymous") @@ -1038,7 +1038,7 @@ async def test_ldap_error( [{"uidNumber": ["bogus"]}], ) token_data = await create_session_token( - factory, username="ldap-user", scopes=["read:all"], minimal=True + factory, username="ldap-user", scopes={"read:all"}, minimal=True ) await set_session_cookie(client, token_data.token) @@ -1054,7 +1054,7 @@ async def test_ldap_error( @pytest.mark.asyncio async def test_user(client: AsyncClient, factory: Factory) -> None: token_data = await create_session_token( - factory, group_names=["admin"], scopes=["read:all"] + factory, group_names=["admin"], scopes={"read:all"} ) r = await client.get( @@ -1080,7 +1080,7 @@ async def test_user(client: AsyncClient, factory: Factory) -> None: @pytest.mark.asyncio async def test_only_service(client: AsyncClient, factory: Factory) -> None: token_data = await create_session_token( - factory, group_names=["admin"], scopes=["read:all"] + factory, group_names=["admin"], scopes={"read:all"} ) # Directly authenticating to an ingress restricted to specific services diff --git a/tests/handlers/logout_test.py b/tests/handlers/logout_test.py index eb351915c..2db14645e 100644 --- a/tests/handlers/logout_test.py +++ b/tests/handlers/logout_test.py @@ -26,7 +26,7 @@ async def test_logout( factory: Factory, caplog: pytest.LogCaptureFixture, ) -> None: - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) await set_session_cookie(client, token_data.token) # Confirm that we're logged in. @@ -59,7 +59,7 @@ async def test_logout( @pytest.mark.asyncio async def test_logout_with_url(client: AsyncClient, factory: Factory) -> None: - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) await set_session_cookie(client, token_data.token) # Confirm that we're logged in. diff --git a/tests/handlers/oidc_test.py b/tests/handlers/oidc_test.py index 201fa9e75..241d4a562 100644 --- a/tests/handlers/oidc_test.py +++ b/tests/handlers/oidc_test.py @@ -1066,10 +1066,10 @@ async def test_userinfo_internal( await reconfigure( "github-oidc-server", factory, monkeypatch, oidc_clients=clients ) - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) token_service = factory.create_token_service() internal_token = await token_service.get_internal_token( - token_data, "some-service", ["read:all"], ip_address="127.0.0.1" + token_data, "some-service", {"read:all"}, ip_address="127.0.0.1" ) r = await client.get( diff --git a/tests/handlers/quota_test.py b/tests/handlers/quota_test.py index bf083e5a7..ded310231 100644 --- a/tests/handlers/quota_test.py +++ b/tests/handlers/quota_test.py @@ -20,7 +20,7 @@ async def test_info(client: AsyncClient, factory: Factory) -> None: ) token_service = factory.create_token_service() token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" + user_info, scopes={"user:token"}, ip_address="127.0.0.1" ) r = await client.get( @@ -38,7 +38,7 @@ async def test_info(client: AsyncClient, factory: Factory) -> None: user_info.groups = [Group(name="foo", id=12313)] token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" + user_info, scopes={"user:token"}, ip_address="127.0.0.1" ) r = await client.get( diff --git a/tests/operator/tokens_test.py b/tests/operator/tokens_test.py index 49f0adbc0..92bef3203 100644 --- a/tests/operator/tokens_test.py +++ b/tests/operator/tokens_test.py @@ -72,7 +72,7 @@ async def assert_secret_token_matches_spec( token=data.token, username=spec.service, token_type=TokenType.service, - scopes=spec.scopes, + scopes=set(spec.scopes), created=data.created, expires=None, name=None, @@ -171,7 +171,7 @@ async def test_secret_verification( AdminTokenRequest( username="bot-some-other-service", token_type=TokenType.service, - scopes=["admin:token"], + scopes={"admin:token"}, ), TokenData.internal_token(), ip_address=None, @@ -194,7 +194,7 @@ async def test_secret_verification( AdminTokenRequest( username=tokens[1]["spec"]["service"], token_type=TokenType.service, - scopes=["read:all"], + scopes={"read:all"}, ), TokenData.internal_token(), ip_address=None, diff --git a/tests/services/oidc_test.py b/tests/services/oidc_test.py index 4d513f1bd..e75badc87 100644 --- a/tests/services/oidc_test.py +++ b/tests/services/oidc_test.py @@ -137,10 +137,10 @@ async def test_redeem_code( access_token = Token.from_str(reply.access_token) access_data = await token_service.get_data(access_token) assert access_data - assert access_data.model_dump() == { + assert access_data.model_dump(mode="json") == { "token": access_token.model_dump(), "username": token_data.username, - "token_type": TokenType.oidc, + "token_type": TokenType.oidc.value, "service": None, "scopes": [], "created": ANY, diff --git a/tests/services/token_cache_test.py b/tests/services/token_cache_test.py index 0b8ec5be4..389733bdc 100644 --- a/tests/services/token_cache_test.py +++ b/tests/services/token_cache_test.py @@ -20,18 +20,18 @@ @pytest.mark.asyncio async def test_basic(factory: Factory) -> None: - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) token_service = factory.create_token_service() token_cache = factory.create_token_cache_service() internal_token = await token_service.get_internal_token( - token_data, "some-service", ["read:all"], ip_address="127.0.0.1" + token_data, "some-service", {"read:all"}, ip_address="127.0.0.1" ) notebook_token = await token_service.get_notebook_token( token_data, ip_address="127.0.0.1" ) assert internal_token == await token_cache.get_internal_token( - token_data, "some-service", ["read:all"], "127.0.0.1" + token_data, "some-service", {"read:all"}, "127.0.0.1" ) assert notebook_token == await token_cache.get_notebook_token( token_data, "127.0.0.1" @@ -39,17 +39,17 @@ async def test_basic(factory: Factory) -> None: # Requesting different internal tokens doesn't work. assert internal_token != await token_cache.get_internal_token( - token_data, "other-service", ["read:all"], "127.0.0.1" + token_data, "other-service", {"read:all"}, "127.0.0.1" ) assert notebook_token != await token_cache.get_internal_token( - token_data, "some-service", [], "127.0.0.1" + token_data, "some-service", set(), "127.0.0.1" ) # A different service token for the same user requesting the same # information creates a different internal token. - new_token_data = await create_session_token(factory, scopes=["read:all"]) + new_token_data = await create_session_token(factory, scopes={"read:all"}) assert internal_token != await token_cache.get_internal_token( - new_token_data, "some-service", ["read:all"], "127.0.0.1" + new_token_data, "some-service", {"read:all"}, "127.0.0.1" ) assert notebook_token != await token_cache.get_notebook_token( new_token_data, "127.0.0.1" @@ -59,30 +59,30 @@ async def test_basic(factory: Factory) -> None: # internal token is requested with the same scope. Cases where the parent # token no longer has that scope are caught one level up by the token # service and thus aren't tested here. - token_data.scopes = ["read:all", "admin:token"] + token_data.scopes = {"read:all", "admin:token"} assert internal_token == await token_cache.get_internal_token( - token_data, "some-service", ["read:all"], "127.0.0.1" + token_data, "some-service", {"read:all"}, "127.0.0.1" ) assert internal_token != await token_cache.get_internal_token( - token_data, "some-service", ["admin:token"], "127.0.0.1" + token_data, "some-service", {"admin:token"}, "127.0.0.1" ) @pytest.mark.asyncio async def test_invalid(factory: Factory) -> None: """Invalid tokens should not be returned even if cached.""" - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) token_cache = factory.create_token_cache_service() internal_token = Token() notebook_token = Token() token_cache._internal_cache.store( - token_data, "some-service", ["read:all"], internal_token + token_data, "some-service", {"read:all"}, internal_token ) token_cache._notebook_cache.store(token_data, notebook_token) assert internal_token != await token_cache.get_internal_token( - token_data, "some-service", ["read:all"], "127.0.0.1" + token_data, "some-service", {"read:all"}, "127.0.0.1" ) assert notebook_token != await token_cache.get_notebook_token( token_data, "127.0.0.1" @@ -92,7 +92,7 @@ async def test_invalid(factory: Factory) -> None: @pytest.mark.asyncio async def test_expiration(config: Config, factory: Factory) -> None: """The cache is valid until half the lifetime of the child token.""" - token_data = await create_session_token(factory, scopes=["read:all"]) + token_data = await create_session_token(factory, scopes={"read:all"}) lifetime = config.token_lifetime now = current_datetime() logger = structlog.get_logger("gafaelfawr") @@ -115,18 +115,18 @@ async def test_expiration(config: Config, factory: Factory) -> None: token=Token(), username=token_data.username, token_type=TokenType.internal, - scopes=["read:all"], + scopes={"read:all"}, created=created, expires=expires, ) await token_store.store_data(internal_token_data) token_cache._internal_cache.store( - token_data, "some-service", ["read:all"], internal_token_data.token + token_data, "some-service", {"read:all"}, internal_token_data.token ) # The cache should return this token. assert internal_token_data.token == await token_cache.get_internal_token( - token_data, "some-service", ["read:all"], "127.0.0.1" + token_data, "some-service", {"read:all"}, "127.0.0.1" ) # Now change the expiration to be ten seconds earlier, which should make @@ -138,7 +138,7 @@ async def test_expiration(config: Config, factory: Factory) -> None: # The cache should now decline to return the token and generate a new one. old_token = internal_token_data.token assert old_token != await token_cache.get_internal_token( - token_data, "some-service", ["read:all"], "127.0.0.1" + token_data, "some-service", {"read:all"}, "127.0.0.1" ) # Do the same test with a notebook token. @@ -146,7 +146,7 @@ async def test_expiration(config: Config, factory: Factory) -> None: token=Token(), username=token_data.username, token_type=TokenType.notebook, - scopes=["read:all"], + scopes={"read:all"}, created=created, expires=expires, ) diff --git a/tests/services/token_test.py b/tests/services/token_test.py index 05475d912..3de2f2520 100644 --- a/tests/services/token_test.py +++ b/tests/services/token_test.py @@ -53,7 +53,7 @@ async def test_session_token(config: Config, factory: Factory) -> None: ) token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" + user_info, scopes={"user:token"}, ip_address="127.0.0.1" ) data = await token_service.get_data(token) assert data @@ -61,7 +61,7 @@ async def test_session_token(config: Config, factory: Factory) -> None: token=token, username="example", token_type=TokenType.session, - scopes=["user:token"], + scopes={"user:token"}, created=data.created, expires=data.expires, name="Example Person", @@ -95,7 +95,7 @@ async def test_session_token(config: Config, factory: Factory) -> None: token=token.key, username=data.username, token_type=TokenType.session, - scopes=["user:token"], + scopes={"user:token"}, expires=data.expires, actor=data.username, action=TokenChange.create, @@ -107,15 +107,15 @@ async def test_session_token(config: Config, factory: Factory) -> None: # Test a session token with scopes. token = await token_service.create_session_token( user_info, - scopes=["read:all", "exec:admin"], + scopes={"read:all", "exec:admin"}, ip_address="127.0.0.1", ) data = await token_service.get_data(token) assert data - assert data.scopes == ["exec:admin", "read:all"] + assert data.scopes == {"exec:admin", "read:all"} info = await token_service.get_token_info_unchecked(token.key) assert info - assert info.scopes == ["exec:admin", "read:all"] + assert info.scopes == {"exec:admin", "read:all"} @pytest.mark.asyncio @@ -126,20 +126,18 @@ async def test_user_token(factory: Factory) -> None: token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["read:all", "exec:admin", "user:token"], + scopes={"read:all", "exec:admin", "user:token"}, ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) assert data expires = current_datetime() + timedelta(days=2) - # Scopes are provided not in sorted order to ensure they're sorted when - # creating the token. user_token = await token_service.create_user_token( data, "example", token_name="some-token", - scopes=["read:all", "exec:admin"], + scopes={"read:all", "exec:admin"}, expires=expires, ip_address="192.168.0.1", ) @@ -150,7 +148,7 @@ async def test_user_token(factory: Factory) -> None: username=user_info.username, token_name="some-token", token_type=TokenType.user, - scopes=["exec:admin", "read:all"], + scopes={"exec:admin", "read:all"}, created=info.created, last_used=None, expires=expires, @@ -161,7 +159,7 @@ async def test_user_token(factory: Factory) -> None: token=user_token, username=user_info.username, token_type=TokenType.user, - scopes=["exec:admin", "read:all"], + scopes={"exec:admin", "read:all"}, created=info.created, expires=info.expires, name=user_info.name, @@ -178,7 +176,7 @@ async def test_user_token(factory: Factory) -> None: username=data.username, token_type=TokenType.user, token_name="some-token", - scopes=["exec:admin", "read:all"], + scopes={"exec:admin", "read:all"}, expires=info.expires, actor=data.username, action=TokenChange.create, @@ -199,7 +197,7 @@ async def test_notebook_token(config: Config, factory: Factory) -> None: token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["read:all", "exec:admin", "user:token"], + scopes={"read:all", "exec:admin", "user:token"}, ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) @@ -212,7 +210,7 @@ async def test_notebook_token(config: Config, factory: Factory) -> None: token=token.key, username=user_info.username, token_type=TokenType.notebook, - scopes=["exec:admin", "read:all", "user:token"], + scopes={"exec:admin", "read:all", "user:token"}, created=info.created, last_used=None, expires=data.expires, @@ -223,7 +221,7 @@ async def test_notebook_token(config: Config, factory: Factory) -> None: token=token, username=user_info.username, token_type=TokenType.notebook, - scopes=["exec:admin", "read:all", "user:token"], + scopes={"exec:admin", "read:all", "user:token"}, created=info.created, expires=data.expires, name=user_info.name, @@ -255,7 +253,7 @@ async def test_notebook_token(config: Config, factory: Factory) -> None: username=data.username, token_type=TokenType.notebook, parent=data.token.key, - scopes=["exec:admin", "read:all", "user:token"], + scopes={"exec:admin", "read:all", "user:token"}, expires=data.expires, actor=data.username, action=TokenChange.create, @@ -275,7 +273,7 @@ async def test_notebook_token(config: Config, factory: Factory) -> None: token=second_token, username=data.username, token_type=TokenType.notebook, - scopes=["exec:admin", "read:all", "user:token"], + scopes={"exec:admin", "read:all", "user:token"}, created=info.created, expires=data.expires, name=data.name, @@ -300,7 +298,7 @@ async def test_notebook_token(config: Config, factory: Factory) -> None: data, data.username, token_name="some token", - scopes=[], + scopes=set(), expires=None, ip_address="127.0.0.1", ) @@ -327,7 +325,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["read:all", "exec:admin", "user:token"], + scopes={"read:all", "exec:admin", "user:token"}, ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) @@ -336,7 +334,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: internal_token = await token_service.get_internal_token( data, service="some-service", - scopes=["read:all"], + scopes={"read:all"}, ip_address="2001:db8::45", ) info = await token_service.get_token_info_unchecked(internal_token.key) @@ -346,7 +344,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: username=user_info.username, token_type=TokenType.internal, service="some-service", - scopes=["read:all"], + scopes={"read:all"}, created=info.created, last_used=None, expires=data.expires, @@ -358,7 +356,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: username=user_info.username, token_type=TokenType.internal, service="some-service", - scopes=["read:all"], + scopes={"read:all"}, created=info.created, expires=data.expires, name=user_info.name, @@ -371,7 +369,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: await token_service.get_internal_token( data, service="some-service", - scopes=["read:some"], + scopes={"read:some"}, ip_address="127.0.0.1", ) @@ -380,7 +378,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: new_internal_token = await token_service.get_internal_token( data, service="some-service", - scopes=["read:all"], + scopes={"read:all"}, ip_address="127.0.0.1", ) assert internal_token == new_internal_token @@ -390,7 +388,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: new_internal_token = await token_service.get_internal_token( data, service="some-service", - scopes=["read:all"], + scopes={"read:all"}, ip_address="127.0.0.1", ) assert internal_token == new_internal_token @@ -406,7 +404,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: token_type=TokenType.internal, parent=data.token.key, service="some-service", - scopes=["read:all"], + scopes={"read:all"}, expires=data.expires, actor=data.username, action=TokenChange.create, @@ -429,7 +427,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: username=data.username, token_type=TokenType.internal, service="some-service", - scopes=["read:all"], + scopes={"read:all"}, created=created, expires=expires, name=data.name, @@ -446,7 +444,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: dup_internal_token = await token_service.get_internal_token( data, service="some-service", - scopes=["read:all"], + scopes={"read:all"}, ip_address="127.0.0.1", ) assert dup_internal_token in (internal_token, second_internal_token) @@ -455,14 +453,14 @@ async def test_internal_token(config: Config, factory: Factory) -> None: new_internal_token = await token_service.get_internal_token( data, service="some-service", - scopes=["exec:admin"], + scopes={"exec:admin"}, ip_address="127.0.0.1", ) assert internal_token != new_internal_token new_internal_token = await token_service.get_internal_token( data, service="another-service", - scopes=["read:all"], + scopes={"read:all"}, ip_address="127.0.0.1", ) assert internal_token != new_internal_token @@ -474,19 +472,19 @@ async def test_internal_token(config: Config, factory: Factory) -> None: data, data.username, token_name="some token", - scopes=["exec:admin"], + scopes={"exec:admin"}, expires=None, ip_address="127.0.0.1", ) data = await token_service.get_data(user_token) assert data new_internal_token = await token_service.get_internal_token( - data, service="some-service", scopes=[], ip_address="127.0.0.1" + data, service="some-service", scopes=set(), ip_address="127.0.0.1" ) assert new_internal_token != internal_token info = await token_service.get_token_info_unchecked(new_internal_token.key) assert info - assert info.scopes == [] + assert info.scopes == set() expires = info.created + config.token_lifetime assert info.expires == expires @@ -495,7 +493,7 @@ async def test_internal_token(config: Config, factory: Factory) -> None: async def test_child_token_lifetime(config: Config, factory: Factory) -> None: """Test that a new internal token is generated at half its lifetime.""" session_token_data = await create_session_token( - factory, scopes=["admin:token", "user:token"] + factory, scopes={"admin:token", "user:token"} ) token_service = factory.create_token_service() @@ -511,7 +509,7 @@ async def test_child_token_lifetime(config: Config, factory: Factory) -> None: session_token_data.username, token_name="n", expires=expires, - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) user_token_data = await token_service.get_data(user_token) @@ -519,13 +517,13 @@ async def test_child_token_lifetime(config: Config, factory: Factory) -> None: # Get an internal token and ensure we get the same one when we ask again. internal_token = await token_service.get_internal_token( - user_token_data, service="a", scopes=[], ip_address="127.0.0.1" + user_token_data, service="a", scopes=set(), ip_address="127.0.0.1" ) internal_token_data = await token_service.get_data(internal_token) assert internal_token_data assert internal_token_data.expires == user_token_data.expires new_internal_token = await token_service.get_internal_token( - user_token_data, service="a", scopes=[], ip_address="127.0.0.1" + user_token_data, service="a", scopes=set(), ip_address="127.0.0.1" ) assert new_internal_token == internal_token @@ -558,7 +556,7 @@ async def test_child_token_lifetime(config: Config, factory: Factory) -> None: # Now, request an internal and notebook token. We should get different # ones with a longer expiration. new_internal_token = await token_service.get_internal_token( - user_token_data, service="a", scopes=[], ip_address="127.0.0.1" + user_token_data, service="a", scopes=set(), ip_address="127.0.0.1" ) assert new_internal_token != internal_token internal_token = new_internal_token @@ -590,7 +588,7 @@ async def test_child_token_lifetime(config: Config, factory: Factory) -> None: # Get an internal and notebook token again. We should get the same ones # as last time. new_internal_token = await token_service.get_internal_token( - user_token_data, service="a", scopes=[], ip_address="127.0.0.1" + user_token_data, service="a", scopes=set(), ip_address="127.0.0.1" ) assert new_internal_token == internal_token new_notebook_token = await token_service.get_notebook_token( @@ -606,7 +604,7 @@ async def test_token_from_admin_request(factory: Factory) -> None: ) token_service = factory.create_token_service() token = await token_service.create_session_token( - user_info, scopes=[], ip_address="127.0.0.1" + user_info, scopes=set(), ip_address="127.0.0.1" ) data = await token_service.get_data(token) assert data @@ -615,7 +613,7 @@ async def test_token_from_admin_request(factory: Factory) -> None: username="otheruser", token_type=TokenType.user, token_name="some token", - scopes=["read:all"], + scopes={"read:all"}, expires=expires, name="Other User", uid=1345, @@ -631,18 +629,18 @@ async def test_token_from_admin_request(factory: Factory) -> None: # Get a token with an appropriate scope. session_token = await token_service.create_session_token( - user_info, scopes=["admin:token"], ip_address="127.0.0.1" + user_info, scopes={"admin:token"}, ip_address="127.0.0.1" ) admin_data = await token_service.get_data(session_token) assert admin_data # Test a few more errors. - request.scopes = ["bogus:scope"] + request.scopes = {"bogus:scope"} with pytest.raises(InvalidScopesError): await token_service.create_token_from_admin_request( request, admin_data, ip_address="127.0.0.1" ) - request.scopes = ["read:all"] + request.scopes = {"read:all"} request.expires = current_datetime() with pytest.raises(InvalidExpiresError): await token_service.create_token_from_admin_request( @@ -671,7 +669,7 @@ async def test_token_from_admin_request(factory: Factory) -> None: username=request.username, token_type=TokenType.user, token_name=request.token_name, - scopes=["read:all"], + scopes={"read:all"}, expires=request.expires, actor=admin_data.username, action=TokenChange.create, @@ -709,7 +707,7 @@ async def test_token_from_admin_request(factory: Factory) -> None: token=token.key, username=request.username, token_type=TokenType.service, - scopes=[], + scopes=set(), expires=None, actor=admin_data.username, action=TokenChange.create, @@ -726,7 +724,7 @@ async def test_list(factory: Factory) -> None: ) token_service = factory.create_token_service() session_token = await token_service.create_session_token( - user_info, scopes=["user:token"], ip_address="127.0.0.1" + user_info, scopes={"user:token"}, ip_address="127.0.0.1" ) data = await token_service.get_data(session_token) assert data @@ -734,14 +732,14 @@ async def test_list(factory: Factory) -> None: data, data.username, token_name="some-token", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) other_user_info = TokenUserInfo( username="other", name="Other Person", uid=1313 ) other_session_token = await token_service.create_session_token( - other_user_info, scopes=["admin:token"], ip_address="1.1.1.1" + other_user_info, scopes={"admin:token"}, ip_address="1.1.1.1" ) admin_data = await token_service.get_data(other_session_token) assert admin_data @@ -785,7 +783,7 @@ async def test_modify(factory: Factory) -> None: token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["admin:token", "read:all", "user:token"], + scopes={"admin:token", "read:all", "user:token"}, ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) @@ -794,7 +792,7 @@ async def test_modify(factory: Factory) -> None: data, data.username, token_name="some-token", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) @@ -808,7 +806,7 @@ async def test_modify(factory: Factory) -> None: await token_service.modify_token( user_token.key, data, - scopes=["read:all"], + scopes={"read:all"}, expires=expires, ip_address="192.168.0.4", ) @@ -819,7 +817,7 @@ async def test_modify(factory: Factory) -> None: username="example", token_type=TokenType.user, token_name="happy token", - scopes=["read:all"], + scopes={"read:all"}, created=info.created, expires=expires, last_used=None, @@ -843,7 +841,7 @@ async def test_modify(factory: Factory) -> None: username=data.username, token_type=TokenType.user, token_name="happy token", - scopes=["read:all"], + scopes={"read:all"}, expires=None, actor=data.username, action=TokenChange.edit, @@ -857,11 +855,11 @@ async def test_modify(factory: Factory) -> None: username=data.username, token_type=TokenType.user, token_name="happy token", - scopes=["read:all"], + scopes={"read:all"}, expires=expires, actor=data.username, action=TokenChange.edit, - old_scopes=[], + old_scopes=set(), old_expires=None, ip_address="192.168.0.4", event_time=history.entries[1].event_time, @@ -872,7 +870,7 @@ async def test_modify(factory: Factory) -> None: username=data.username, token_type=TokenType.user, token_name="happy token", - scopes=[], + scopes=set(), expires=None, actor=data.username, action=TokenChange.edit, @@ -886,7 +884,7 @@ async def test_modify(factory: Factory) -> None: username=data.username, token_type=TokenType.user, token_name="some-token", - scopes=[], + scopes=set(), expires=None, actor=data.username, action=TokenChange.create, @@ -904,7 +902,7 @@ async def test_delete(factory: Factory) -> None: data, data.username, token_name="some token", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) @@ -930,7 +928,7 @@ async def test_delete(factory: Factory) -> None: username=data.username, token_type=TokenType.user, token_name="some token", - scopes=[], + scopes=set(), expires=None, actor=data.username, action=TokenChange.revoke, @@ -943,7 +941,7 @@ async def test_delete(factory: Factory) -> None: username=data.username, token_type=TokenType.user, token_name="some token", - scopes=[], + scopes=set(), expires=None, actor=data.username, action=TokenChange.create, @@ -957,7 +955,7 @@ async def test_delete(factory: Factory) -> None: data, data.username, token_name="some token", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) other_data = await create_session_token(factory, username="other") @@ -968,7 +966,7 @@ async def test_delete(factory: Factory) -> None: # Admins can delete soemone else's token. admin_data = await create_session_token( - factory, username="admin", scopes=["admin:token"] + factory, username="admin", scopes={"admin:token"} ) assert await token_service.get_data(token) assert await token_service.delete_token( @@ -982,13 +980,13 @@ async def test_delete_cascade(factory: Factory) -> None: """Test that deleting a token cascades to child tokens.""" token_service = factory.create_token_service() session_token_data = await create_session_token( - factory, scopes=["admin:token", "read:all", "user:token"] + factory, scopes={"admin:token", "read:all", "user:token"} ) user_token = await token_service.create_user_token( session_token_data, session_token_data.username, token_name="user-token", - scopes=["user:token"], + scopes={"user:token"}, ip_address="127.0.0.1", ) user_token_data = await token_service.get_data(user_token) @@ -996,7 +994,7 @@ async def test_delete_cascade(factory: Factory) -> None: admin_request = AdminTokenRequest( username="bot-service", token_type=TokenType.service, - scopes=["read:all", "user:token"], + scopes={"read:all", "user:token"}, name="Some Service", ) service_token = await token_service.create_token_from_admin_request( @@ -1014,15 +1012,21 @@ async def test_delete_cascade(factory: Factory) -> None: session_children = [ notebook_token, await token_service.get_internal_token( - session_token_data, "service-a", scopes=[], ip_address="127.0.0.1" + session_token_data, + "service-a", + scopes=set(), + ip_address="127.0.0.1", ), await token_service.get_internal_token( - notebook_token_data, "service-b", scopes=[], ip_address="127.0.0.1" + notebook_token_data, + "service-b", + scopes=set(), + ip_address="127.0.0.1", ), await token_service.get_internal_token( notebook_token_data, "service-a", - scopes=["read:all"], + scopes={"read:all"}, ip_address="127.0.0.1", ), ] @@ -1032,7 +1036,7 @@ async def test_delete_cascade(factory: Factory) -> None: await token_service.get_internal_token( internal_token_data, "service-b", - scopes=["read:all"], + scopes={"read:all"}, ip_address="127.0.0.1", ) ) @@ -1040,7 +1044,7 @@ async def test_delete_cascade(factory: Factory) -> None: # Shorter trees of tokens from the user and service tokens. user_children = [ await token_service.get_internal_token( - user_token_data, "service-c", scopes=[], ip_address="127.0.0.1" + user_token_data, "service-c", scopes=set(), ip_address="127.0.0.1" ), await token_service.get_notebook_token( user_token_data, ip_address="127.0.0.1" @@ -1048,7 +1052,10 @@ async def test_delete_cascade(factory: Factory) -> None: ] service_children = [ await token_service.get_internal_token( - service_token_data, "service-a", scopes=[], ip_address="127.0.0.1" + service_token_data, + "service-a", + scopes=set(), + ip_address="127.0.0.1", ) ] @@ -1091,7 +1098,7 @@ async def test_modify_expires(config: Config, factory: Factory) -> None: """Test that expiration changes cascade to subtokens.""" token_service = factory.create_token_service() session_token_data = await create_session_token( - factory, scopes=["admin:token", "user:token"] + factory, scopes={"admin:token", "user:token"} ) # Create a user token with no expiration and some additional tokens @@ -1100,7 +1107,7 @@ async def test_modify_expires(config: Config, factory: Factory) -> None: session_token_data, session_token_data.username, token_name="user-token", - scopes=["user:token"], + scopes={"user:token"}, ip_address="127.0.0.1", ) user_token_data = await token_service.get_data(user_token) @@ -1111,12 +1118,12 @@ async def test_modify_expires(config: Config, factory: Factory) -> None: notebook_token_data = await token_service.get_data(notebook_token) assert notebook_token_data internal_token = await token_service.get_internal_token( - user_token_data, "service-a", scopes=[], ip_address="127.0.0.1" + user_token_data, "service-a", scopes=set(), ip_address="127.0.0.1" ) internal_token_data = await token_service.get_data(internal_token) assert internal_token_data nested_token = await token_service.get_internal_token( - notebook_token_data, "service-b", scopes=[], ip_address="127.0.0.1" + notebook_token_data, "service-b", scopes=set(), ip_address="127.0.0.1" ) nested_token_data = await token_service.get_data(nested_token) assert nested_token_data @@ -1186,7 +1193,7 @@ async def test_invalid( token=Token(), username="example", token_type=TokenType.session, - scopes=[], + scopes=set(), created=current_datetime(), name="Some User", uid=12345, @@ -1349,7 +1356,7 @@ async def test_invalid_username(factory: Factory) -> None: token_service = factory.create_token_service() session_token = await token_service.create_session_token( user_info, - scopes=["read:all", "admin:token"], + scopes={"read:all", "admin:token"}, ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) @@ -1372,12 +1379,16 @@ async def test_invalid_username(factory: Factory) -> None: user_info.username = user with pytest.raises(PermissionDeniedError): await token_service.create_session_token( - user_info, scopes=[], ip_address="127.0.0.1" + user_info, scopes=set(), ip_address="127.0.0.1" ) data.username = user with pytest.raises(PermissionDeniedError): await token_service.create_user_token( - data, user, token_name="n", scopes=[], ip_address="127.0.0.1" + data, + user, + token_name="n", + scopes=set(), + ip_address="127.0.0.1", ) with pytest.raises(PermissionDeniedError): await token_service.get_notebook_token( @@ -1385,7 +1396,7 @@ async def test_invalid_username(factory: Factory) -> None: ) with pytest.raises(PermissionDeniedError): await token_service.get_internal_token( - data, service="s", scopes=[], ip_address="127.0.0.1" + data, service="s", scopes=set(), ip_address="127.0.0.1" ) with pytest.raises(ValidationError): AdminTokenRequest(username=user, token_type=TokenType.service) @@ -1415,7 +1426,7 @@ async def test_expire_tokens(factory: Factory) -> None: token=Token(), username="some-user", token_type=TokenType.session, - scopes=["read:all", "user:token"], + scopes={"read:all", "user:token"}, created=now - timedelta(minutes=60), expires=now - timedelta(minutes=30), ) @@ -1423,7 +1434,7 @@ async def test_expire_tokens(factory: Factory) -> None: token=Token(), username=session_token_data.username, token_type=TokenType.user, - scopes=["read:all"], + scopes={"read:all"}, created=now - timedelta(minutes=50), expires=now - timedelta(minutes=30), ) @@ -1431,7 +1442,7 @@ async def test_expire_tokens(factory: Factory) -> None: token=Token(), username=session_token_data.username, token_type=TokenType.user, - scopes=["admin:token", "read:all"], + scopes={"admin:token", "read:all"}, created=now - timedelta(minutes=50), expires=now + timedelta(minutes=30), ) @@ -1439,7 +1450,7 @@ async def test_expire_tokens(factory: Factory) -> None: token=Token(), username=session_token_data.username, token_type=TokenType.notebook, - scopes=["read:all"], + scopes={"read:all"}, created=now - timedelta(minutes=59), expires=now - timedelta(minutes=30), ) @@ -1448,7 +1459,7 @@ async def test_expire_tokens(factory: Factory) -> None: username=session_token_data.username, token_type=TokenType.internal, service="tap", - scopes=[], + scopes=set(), created=now - timedelta(minutes=58), expires=now - timedelta(minutes=30), ) @@ -1457,7 +1468,7 @@ async def test_expire_tokens(factory: Factory) -> None: username=session_token_data.username, token_type=TokenType.internal, service="tap", - scopes=["read:all"], + scopes={"read:all"}, created=now - timedelta(minutes=58), expires=now - timedelta(minutes=30), ) @@ -1465,7 +1476,7 @@ async def test_expire_tokens(factory: Factory) -> None: token=Token(), username="bot-service", token_type=TokenType.service, - scopes=["read:all"], + scopes={"read:all"}, created=now - timedelta(minutes=45), expires=now - timedelta(minutes=30), ) @@ -1548,14 +1559,14 @@ async def test_truncate_history(factory: Factory) -> None: now = current_datetime() token_service = factory.create_token_service() session_token_data = await create_session_token( - factory, scopes=["admin:token"] + factory, scopes={"admin:token"} ) history_store = TokenChangeHistoryStore(factory.session) old_entry = TokenChangeHistoryEntry( token=Token().key, username="other-user", token_type=TokenType.session, - scopes=[], + scopes=set(), expires=now - CHANGE_HISTORY_RETENTION + timedelta(days=10), actor="other-user", action=TokenChange.create, @@ -1566,7 +1577,7 @@ async def test_truncate_history(factory: Factory) -> None: token=Token().key, username="other-user", token_type=TokenType.session, - scopes=[], + scopes=set(), expires=now - CHANGE_HISTORY_RETENTION + timedelta(days=5), actor="other-user", action=TokenChange.create, @@ -1599,7 +1610,7 @@ async def test_audit(factory: Factory) -> None: token=Token(), username="some-user", token_type=TokenType.session, - scopes=["user:token"], + scopes={"user:token"}, created=now, expires=now + timedelta(days=7), ) @@ -1611,7 +1622,7 @@ async def test_audit(factory: Factory) -> None: token=Token(), username="other-user", token_type=TokenType.session, - scopes=["user:token"], + scopes={"user:token"}, created=now, expires=now + timedelta(days=7), ) @@ -1622,7 +1633,7 @@ async def test_audit(factory: Factory) -> None: token=Token(), username="some-user", token_type=TokenType.user, - scopes=[], + scopes=set(), created=now - timedelta(days=1), expires=now + timedelta(days=7), ) @@ -1630,7 +1641,7 @@ async def test_audit(factory: Factory) -> None: token=db_user_token_data.token, username=db_user_token_data.username, token_type=db_user_token_data.token_type, - scopes=["read:all"], + scopes={"read:all"}, created=now, expires=db_user_token_data.expires, ) @@ -1644,7 +1655,7 @@ async def test_audit(factory: Factory) -> None: username="some-user", token_type=TokenType.internal, service="some-service", - scopes=[], + scopes=set(), created=now, expires=now + timedelta(days=14), ) @@ -1661,7 +1672,7 @@ async def test_audit(factory: Factory) -> None: username="some-user", token_type=TokenType.internal, service="some-service", - scopes=[], + scopes=set(), created=now, expires=now + timedelta(days=7), ) @@ -1676,7 +1687,7 @@ async def test_audit(factory: Factory) -> None: token=Token(), username="some-user", token_type=TokenType.session, - scopes=["bogus:scope"], + scopes={"bogus:scope"}, created=now, expires=now + timedelta(days=7), ) @@ -1690,7 +1701,7 @@ async def test_audit(factory: Factory) -> None: token=Token(), username="some-user", token_type=TokenType.session, - scopes=["user:token"], + scopes={"user:token"}, created=now - timedelta(days=7), expires=now - timedelta(seconds=5), ) @@ -1750,7 +1761,7 @@ async def test_state_metrics(config: Config, factory: Factory) -> None: token_data, token_data.username, token_name="something", - scopes=[], + scopes=set(), ip_address="127.0.0.1", ) diff --git a/tests/storage/token_test.py b/tests/storage/token_test.py index 185ccb158..7906ab3d5 100644 --- a/tests/storage/token_test.py +++ b/tests/storage/token_test.py @@ -27,7 +27,7 @@ async def test_metrics(factory: Factory) -> None: token_data, "someuser", token_name="some-token", - scopes=[], + scopes=set(), ip_address="192.168.0.1", ) async with factory.session.begin(): diff --git a/tests/support/selenium.py b/tests/support/selenium.py index 266b9890d..c7af57e6c 100644 --- a/tests/support/selenium.py +++ b/tests/support/selenium.py @@ -91,7 +91,7 @@ async def _selenium_startup(token_path: Path) -> None: """Startup hook for the app run in Selenium testing mode.""" config = await config_dependency() user_info = TokenUserInfo(username="testuser", name="Test User", uid=1000) - scopes = list(config.known_scopes.keys()) + scopes = set(config.known_scopes.keys()) engine = create_database_engine( config.database_url, config.database_password diff --git a/tests/support/tokens.py b/tests/support/tokens.py index 2c3b1ce07..efa2e6a89 100644 --- a/tests/support/tokens.py +++ b/tests/support/tokens.py @@ -24,7 +24,7 @@ async def add_expired_session_token( user_info: TokenUserInfo, *, - scopes: list[str], + scopes: set[str], ip_address: str, session: async_scoped_session, ) -> None: @@ -41,13 +41,13 @@ async def add_expired_session_token( Parameters ---------- user_info - The user information to associate with the token. + User information to associate with the token. scopes - The scopes of the token. + Scopes of the token. ip_address - The IP address from which the request came. + IP address from which the request came. session - The database session. + Database session. """ token_db_store = TokenDatabaseStore(session) token_change_store = TokenChangeHistoryStore(session) @@ -84,7 +84,7 @@ async def create_session_token( *, username: str | None = None, group_names: list[str] | None = None, - scopes: list[str] | None = None, + scopes: set[str] | None = None, minimal: bool = False, ) -> TokenData: """Create a session token. @@ -127,8 +127,8 @@ async def create_session_token( gid=2000, groups=groups, ) - if not scopes: - scopes = ["user:token"] + if scopes is None: + scopes = {"user:token"} token_service = factory.create_token_service() token = await token_service.create_session_token( user_info, scopes=scopes, ip_address="127.0.0.1"