diff --git a/src/gafaelfawr/cache.py b/src/gafaelfawr/cache.py index d685b5d7..056bc275 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 38bdc46f..61938c9e 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 45cbc7f8..f526ffcf 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 246f39fd..c34f07bf 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 82f4d211..97f1b5d6 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 fcd03fb7..a794776d 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 dea9d649..37e19a8f 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 3ceb379c..6543709f 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 19db6269..760aed11 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 889448ef..cfe1303a 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 aabedeef..16dcfe80 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 192dffa9..ff01a4f4 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 35a2f9b2..ae3591c4 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 3d579850..6c3cbff0 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 31c0bb18..7b127269 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 5a69c11d..24256151 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 a82d99e4..34be7665 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 48193e5f..fad6d86a 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 eb351915..2db14645 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 201fa9e7..241d4a56 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 bf083e5a..ded31023 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 49f0adbc..92bef320 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 4d513f1b..e75badc8 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 0b8ec5be..389733bd 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 05475d91..3de2f252 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 185ccb15..7906ab3d 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 266b9890..c7af57e6 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 2c3b1ce0..efa2e6a8 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"