async def create_session_token(self, user_info: TokenUserInfo, *, scopes: List[str], ip_address: str) -> Token: """Create a new session token. Parameters ---------- user_info : `gafaelfawr.models.token.TokenUserInfo` The user information to associate with the token. scopes : List[`str`] The scopes of the token. ip_address : `str` The IP address from which the request came. Returns ------- token : `gafaelfawr.models.token.Token` The newly-created token. Raises ------ gafaelfawr.exceptions.PermissionDeniedError If the provided username is invalid. """ self._validate_username(user_info.username) scopes = sorted(scopes) token = Token() created = current_datetime() expires = created + self._config.token_lifetime data = TokenData( token=token, token_type=TokenType.session, scopes=scopes, created=created, expires=expires, **user_info.dict(), ) history_entry = TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=TokenType.session, scopes=scopes, expires=expires, actor=data.username, action=TokenChange.create, ip_address=ip_address, event_time=created, ) await self._token_redis_store.store_data(data) with self._transaction_manager.transaction(): self._token_db_store.add(data) self._token_change_store.add(history_entry) return token
async def add_expired_session_token( user_info: TokenUserInfo, *, scopes: List[str], ip_address: str, session: AsyncSession, ) -> None: """Add an expired session token to the database. This requires going beneath the service layer, since the service layer rejects creation of expired tokens (since apart from testing this isn't a sensible thing to want to do). This does not add the token to Redis, since Redis will refuse to add it with a negative expiration time, so can only be used for tests that exclusively use the database. Parameters ---------- user_info : `gafaelfawr.models.token.TokenUserInfo` The user information to associate with the token. scopes : List[`str`] The scopes of the token. ip_address : `str` The IP address from which the request came. session : `sqlalchemy.ext.asyncio.AsyncSession` The database session. """ token_db_store = TokenDatabaseStore(session) token_change_store = TokenChangeHistoryStore(session) token = Token() created = current_datetime() expires = created - timedelta(minutes=10) data = TokenData( token=token, token_type=TokenType.session, scopes=scopes, created=created, expires=expires, **user_info.dict(), ) history_entry = TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=TokenType.session, scopes=scopes, expires=expires, actor=data.username, action=TokenChange.create, ip_address=ip_address, event_time=created, ) await token_db_store.add(data) await token_change_store.add(history_entry)
def add(self, entry: TokenChangeHistoryEntry) -> None: """Record a change to a token.""" entry_dict = entry.dict() # Convert the lists of scopes to the empty string for an empty list # and a comma-separated string otherwise. entry_dict["scopes"] = ",".join(sorted(entry.scopes)) if entry.old_scopes is not None: entry_dict["old_scopes"] = ",".join(sorted(entry.old_scopes)) new = TokenChangeHistory(**entry_dict) self._session.add(new)
async def _modify_expires( self, key: str, auth_data: TokenData, expires: datetime, ip_address: str, ) -> None: """Change the expiration of a token if necessary. Used to update the expiration of subtokens when the parent token expiration has changed. Parameters ---------- key : `str` The key of the token to update. auth_data : `gafaelfawr.models.token.TokenData` The token data for the authentication token of the user changing the expiration. expires : `datetime.datetime` The new expiration of the parent token. The expiration of the child token will be changed if it's later than this value. ip_address : `str` The IP address from which the request came. """ info = self.get_token_info_unchecked(key) if not info: return if info.expires and info.expires <= expires: return history_entry = TokenChangeHistoryEntry( token=key, username=info.username, token_type=info.token_type, token_name=info.token_name, parent=info.parent, scopes=info.scopes, service=info.service, expires=expires, old_expires=info.expires, actor=auth_data.username, action=TokenChange.edit, ip_address=ip_address, ) self._token_db_store.modify(key, expires=expires) self._token_change_store.add(history_entry) data = await self._token_redis_store.get_data_by_key(key) if data: data.expires = expires await self._token_redis_store.store_data(data)
async def _delete_one_token( self, key: str, auth_data: TokenData, ip_address: str, ) -> bool: """Helper function to delete a single token. This does not do cascading delete and assumes authorization has already been checked. Must be called inside a transaction. Parameters ---------- key : `str` The key of the token to delete. auth_data : `gafaelfawr.models.token.TokenData` The token data for the authentication token of the user deleting the token. ip_address : `str` The IP address from which the request came. Returns ------- success : `bool` Whether the token was found and deleted. """ info = self.get_token_info_unchecked(key) if not info: return False history_entry = TokenChangeHistoryEntry( token=key, username=info.username, token_type=info.token_type, token_name=info.token_name, parent=info.parent, scopes=info.scopes, service=info.service, expires=info.expires, actor=auth_data.username, action=TokenChange.revoke, ip_address=ip_address, ) await self._token_redis_store.delete(key) success = self._token_db_store.delete(key) if success: self._token_change_store.add(history_entry) self._logger.info("Deleted token", key=key, username=info.username) return success
def list( self, *, cursor: Optional[HistoryCursor] = None, limit: Optional[int] = None, since: Optional[datetime] = None, until: Optional[datetime] = None, username: Optional[str] = None, actor: Optional[str] = None, key: Optional[str] = None, token: Optional[str] = None, token_type: Optional[TokenType] = None, ip_or_cidr: Optional[str] = None, ) -> PaginatedHistory[TokenChangeHistoryEntry]: """Return all changes to a specific token. Parameters ---------- cursor : `gafaelfawr.models.history.HistoryCursor`, optional A pagination cursor specifying where to start in the results. limit : `int`, optional Limit the number of returned results. since : `datetime.datetime`, optional Limit the results to events at or after this time. until : `datetime.datetime`, optional Limit the results to events before or at this time. username : `str`, optional Limit the results to tokens owned by this user. actor : `str`, optional Limit the results to actions performed by this user. key : `str`, optional Limit the results to this token and any subtokens of this token. Note that this will currently pick up direct subtokens but not subtokens of subtokens. token : `str`, optional Limit the results to only this token. token_type : `gafaelfawr.models.token.TokenType`, optional Limit the results to tokens of this type. ip_or_cidr : `str`, optional Limit the results to changes made from this IPv4 or IPv6 address or CIDR block. Unless the underlying database is PostgreSQL, the CIDR block must be on an octet boundary. Returns ------- entries : List[`gafaelfawr.models.history.TokenChangeHistoryEntry`] List of change history entries, which may be empty. """ query = self._session.query(TokenChangeHistory) if since: query = query.filter(TokenChangeHistory.event_time >= since) if until: query = query.filter(TokenChangeHistory.event_time <= until) if username: query = query.filter_by(username=username) if actor: query = query.filter_by(actor=actor) if key: query = query.filter( or_( TokenChangeHistory.token == key, TokenChangeHistory.parent == key, ) ) if token: query = query.filter_by(token=token) if token_type: query = query.filter_by(token_type=token_type) if ip_or_cidr: query = self._apply_ip_or_cidr_filter(query, ip_or_cidr) # Shunt the complicated case of a paginated query to a separate # function to keep the logic more transparent. if cursor or limit: return self._paginated_query(query, cursor, limit) # Perform the query and return the results. query = query.order_by( TokenChangeHistory.event_time.desc(), TokenChangeHistory.id.desc() ) entries = query.all() return PaginatedHistory[TokenChangeHistoryEntry]( entries=[TokenChangeHistoryEntry.from_orm(e) for e in entries], count=len(entries), prev_cursor=None, next_cursor=None, )
async def test_token_from_admin_request(factory: ComponentFactory) -> None: user_info = TokenUserInfo(username="******", name="Example Person", uid=4137) token_service = factory.create_token_service() async with factory.session.begin(): token = await token_service.create_session_token( user_info, scopes=[], ip_address="127.0.0.1") data = await token_service.get_data(token) assert data expires = current_datetime() + timedelta(days=2) request = AdminTokenRequest( username="******", token_type=TokenType.user, token_name="some token", scopes=["read:all"], expires=expires, name="Other User", uid=1345, groups=[TokenGroup(name="some-group", id=4133)], ) # Cannot create a token via admin request because the authentication # information is missing the admin:token scope. with pytest.raises(PermissionDeniedError): await token_service.create_token_from_admin_request( request, data, ip_address="127.0.0.1") # Get a token with an appropriate scope. async with factory.session.begin(): session_token = await token_service.create_session_token( 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"] 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.expires = current_datetime() with pytest.raises(InvalidExpiresError): await token_service.create_token_from_admin_request( request, admin_data, ip_address="127.0.0.1") request.expires = expires # Try a successful request. async with factory.session.begin(): token = await token_service.create_token_from_admin_request( request, admin_data, ip_address="127.0.0.1") user_data = await token_service.get_data(token) assert user_data and user_data == TokenData( token=token, created=user_data.created, **request.dict()) assert_is_now(user_data.created) async with factory.session.begin(): history = await token_service.get_change_history( admin_data, token=token.key, username=request.username) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, username=request.username, token_type=TokenType.user, token_name=request.token_name, scopes=["read:all"], expires=request.expires, actor=admin_data.username, action=TokenChange.create, ip_address="127.0.0.1", event_time=user_data.created, ) ] # Non-admins can't see other people's tokens. with pytest.raises(PermissionDeniedError): await token_service.get_change_history(data, token=token.key, username=request.username) # Now request a service token with minimal data instead. request = AdminTokenRequest(username="******", token_type=TokenType.service) async with factory.session.begin(): token = await token_service.create_token_from_admin_request( request, admin_data, ip_address="127.0.0.1") service_data = await token_service.get_data(token) assert service_data and service_data == TokenData( token=token, created=service_data.created, **request.dict()) assert_is_now(service_data.created) async with factory.session.begin(): history = await token_service.get_change_history( admin_data, token=token.key, username=request.username) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, username=request.username, token_type=TokenType.service, scopes=[], expires=None, actor=admin_data.username, action=TokenChange.create, ip_address="127.0.0.1", event_time=service_data.created, ) ]
async def get_notebook_token(self, token_data: TokenData, ip_address: str) -> Token: """Get or create a new notebook token. The new token will have the same expiration time as the existing token on which it's based unless that expiration time is longer than the expiration time of normal interactive tokens, in which case it will be capped at the interactive token expiration time. Parameters ---------- token_data : `gafaelfawr.models.token.TokenData` The authentication data on which to base the new token. ip_address : `str` The IP address from which the request came. Returns ------- token : `gafaelfawr.models.token.Token` The newly-created token. Raises ------ gafaelfawr.exceptions.PermissionDeniedError If the username is invalid. """ self._validate_username(token_data.username) # See if there is a cached token. token = await self._token_cache.get_notebook_token(token_data) if token: return token # See if there's already a matching notebook token. key = self._token_db_store.get_notebook_token_key( token_data, self._minimum_expiration(token_data)) if key: data = await self._token_redis_store.get_data_by_key(key) if data: self._token_cache.store_notebook_token(data.token, token_data) return data.token # There is not, so we need to create a new one. token = Token() created = current_datetime() expires = created + self._config.token_lifetime if token_data.expires and token_data.expires < expires: expires = token_data.expires data = TokenData( token=token, username=token_data.username, token_type=TokenType.notebook, scopes=token_data.scopes, created=created, expires=expires, name=token_data.name, email=token_data.email, uid=token_data.uid, groups=token_data.groups, ) history_entry = TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=TokenType.notebook, parent=token_data.token.key, scopes=data.scopes, expires=expires, actor=token_data.username, action=TokenChange.create, ip_address=ip_address, event_time=created, ) await self._token_redis_store.store_data(data) with self._transaction_manager.transaction(): self._token_db_store.add(data, parent=token_data.token.key) self._token_change_store.add(history_entry) # Cache the token and return it. self._logger.info("Created new notebook token", key=token.key) self._token_cache.store_notebook_token(token, token_data) return token
async def modify_token( self, key: str, auth_data: TokenData, username: Optional[str] = None, *, ip_address: str, token_name: Optional[str] = None, scopes: Optional[List[str]] = None, expires: Optional[datetime] = None, no_expire: bool = False, ) -> Optional[TokenInfo]: """Modify a token. Parameters ---------- key : `str` The key of the token to modify. auth_data : `gafaelfawr.models.token.TokenData` The token data for the authentication token of the user making this modification. username : `str`, optional If given, constrain modifications to tokens owned by the given user. ip_address : `str` The IP address from which the request came. token_name : `str`, optional The new name for the token. scopes : List[`str`], optional The new scopes for the token. expires : `datetime`, optional The new expiration time for the token. no_expire : `bool` If set, the token should not expire. This is a separate parameter because passing `None` to ``expires`` is ambiguous. Returns ------- info : `gafaelfawr.models.token.TokenInfo` or `None` Information for the updated token or `None` if it was not found. Raises ------ gafaelfawr.exceptions.InvalidExpiresError The provided expiration time was invalid. gafaelfawr.exceptions.DuplicateTokenNameError A token with this name for this user already exists. gafaelfawr.exceptions.PermissionDeniedError The token being modified is not owned by the user identified with ``auth_data`` or the user attempted to modify a token type other than user. """ info = self.get_token_info_unchecked(key, username) if not info: return None self._check_authorization(info.username, auth_data) if info.token_type != TokenType.user: msg = "Only user tokens can be modified" self._logger.warning("Permission denied", error=msg) raise PermissionDeniedError(msg) if scopes: self._validate_scopes(scopes, auth_data) self._validate_expires(expires) # Determine if the lifetime has decreased, in which case we may have # to update subtokens. update_subtoken_expires = expires and (not info.expires or expires <= info.expires) history_entry = TokenChangeHistoryEntry( token=key, 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, expires=info.expires if not (expires or no_expire) else expires, actor=auth_data.username, action=TokenChange.edit, old_token_name=info.token_name if token_name else None, old_scopes=info.scopes if scopes is not None else None, old_expires=info.expires if (expires or no_expire) else None, ip_address=ip_address, ) with self._transaction_manager.transaction(): info = self._token_db_store.modify( key, token_name=token_name, scopes=sorted(scopes) if scopes else scopes, expires=expires, no_expire=no_expire, ) self._token_change_store.add(history_entry) # Update the expiration in Redis if needed. if info and (no_expire or expires): data = await self._token_redis_store.get_data_by_key(key) if data: data.expires = None if no_expire else expires await self._token_redis_store.store_data(data) else: info = None # Update subtokens if needed. if update_subtoken_expires and info: assert expires for child in self._token_db_store.get_children(key): await self._modify_expires(child, auth_data, expires, ip_address) if info: timestamp = int(info.expires.timestamp()) if info.expires else None self._logger.info( "Modified token", key=key, token_name=info.token_name, token_scope=",".join(info.scopes), expires=timestamp, ) return info
async def create_user_token( self, auth_data: TokenData, username: str, *, token_name: str, scopes: List[str], expires: Optional[datetime] = None, ip_address: str, ) -> Token: """Add a new user token. Parameters ---------- auth_data : `gafaelfawr.models.token.TokenData` The token data for the authentication token of the user creating a user token. username : `str` The username for which to create a token. token_name : `str` The name of the token. scopes : List[`str`] The scopes of the token. expires : `datetime` or `None` When the token should expire. If not given, defaults to the expiration of the authentication token taken from ``data``. ip_address : `str` The IP address from which the request came. Returns ------- token : `gafaelfawr.models.token.Token` The newly-created token. Raises ------ gafaelfawr.exceptions.DuplicateTokenNameError A token with this name for this user already exists. gafaelfawr.exceptions.InvalidExpiresError The provided expiration time was invalid. gafaelfawr.exceptions.PermissionDeniedError If the given username didn't match the user information in the authentication token, or if the specified username is invalid. Notes ----- This can only be used by the user themselves, not by a token administrator, because this API does not provide a way to set the additional user information for the token. Once the user information no longer needs to be tracked by the token system, it can be unified with ``create_token_from_admin_request``. """ self._check_authorization(username, auth_data, require_same_user=True) self._validate_username(username) self._validate_expires(expires) self._validate_scopes(scopes, auth_data) scopes = sorted(scopes) token = Token() created = current_datetime() data = TokenData( token=token, username=username, token_type=TokenType.user, scopes=scopes, created=created, expires=expires, name=auth_data.name, email=auth_data.email, uid=auth_data.uid, groups=auth_data.groups, ) history_entry = TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=TokenType.user, token_name=token_name, scopes=scopes, expires=expires, actor=auth_data.username, action=TokenChange.create, ip_address=ip_address, event_time=created, ) await self._token_redis_store.store_data(data) with self._transaction_manager.transaction(): self._token_db_store.add(data, token_name=token_name) self._token_change_store.add(history_entry) self._logger.info( "Created new user token", key=token.key, token_name=token_name, token_scope=",".join(data.scopes), ) return token
async def create_token_from_admin_request( self, request: AdminTokenRequest, auth_data: TokenData, *, ip_address: Optional[str], ) -> Token: """Create a new service or user token from an admin request. Parameters ---------- request : `gafaelfawr.models.token.AdminTokenRequest` The incoming request. auth_data : `gafaelfawr.models.token.TokenData` The data for the authenticated user making the request. ip_address : `str` or `None` The IP address from which the request came, or `None` for internal requests by Gafaelfawr. Returns ------- token : `gafaelfawr.models.token.Token` The newly-created token. Raises ------ gafaelfawr.exceptions.PermissionDeniedError If the provided username is invalid. """ self._check_authorization(request.username, auth_data, require_admin=True) self._validate_username(request.username) self._validate_scopes(request.scopes) self._validate_expires(request.expires) token = Token() created = current_datetime() data = TokenData( token=token, username=request.username, token_type=request.token_type, scopes=request.scopes, created=created, expires=request.expires, name=request.name, email=request.email, uid=request.uid, groups=request.groups, ) history_entry = TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=data.token_type, token_name=request.token_name, scopes=data.scopes, expires=request.expires, actor=auth_data.username, action=TokenChange.create, ip_address=ip_address, event_time=created, ) await self._token_redis_store.store_data(data) with self._transaction_manager.transaction(): self._token_db_store.add(data, token_name=request.token_name) self._token_change_store.add(history_entry) if data.token_type == TokenType.user: self._logger.info( "Created new user token", key=token.key, token_name=request.token_name, token_scope=",".join(data.scopes), token_username=data.username, ) else: self._logger.info( "Created new service token", key=token.key, token_scope=",".join(data.scopes), token_username=data.username, ) return token
def _paginated_query( self, query: Query, cursor: Optional[HistoryCursor], limit: Optional[int], ) -> PaginatedHistory[TokenChangeHistoryEntry]: """Run a paginated query (one with a limit or a cursor).""" limited_query = query # Apply the cursor, if there is one. if cursor: limited_query = self._apply_cursor(limited_query, cursor) # When retrieving a previous set of results using a previous # cursor, we have to reverse the sort algorithm so that the cursor # boundary can be applied correctly. We'll then later reverse the # result set to return it in proper forward-sorted order. if cursor and cursor.previous: limited_query = limited_query.order_by( TokenChangeHistory.event_time, TokenChangeHistory.id ) else: limited_query = limited_query.order_by( TokenChangeHistory.event_time.desc(), TokenChangeHistory.id.desc(), ) # Grab one more element than the query limit so that we know whether # to create a cursor (because there are more elements) and what the # cursor value should be (for forward cursors). if limit: limited_query = limited_query.limit(limit + 1) # Execute the query twice, once to get the next bach of results and # once to get the count of all entries without pagination. entries = limited_query.all() count = query.count() # Calculate the cursors, remove the extra element we asked for, and # reverse the results again if we did a reverse sort because we were # using a previous cursor. prev_cursor = None next_cursor = None if cursor and cursor.previous: if limit: next_cursor = HistoryCursor.invert(cursor) if len(entries) > limit: prev_cursor = self._build_prev_cursor(entries[limit - 1]) entries = entries[:limit] entries.reverse() elif limit: if cursor: prev_cursor = HistoryCursor.invert(cursor) if len(entries) > limit: next_cursor = self._build_next_cursor(entries[limit]) entries = entries[:limit] # Return the results. return PaginatedHistory[TokenChangeHistoryEntry]( entries=[TokenChangeHistoryEntry.from_orm(e) for e in entries], count=count, prev_cursor=prev_cursor, next_cursor=next_cursor, )
async def test_delete(factory: ComponentFactory) -> None: data = await create_session_token(factory) token_service = factory.create_token_service() async with factory.session.begin(): token = await token_service.create_user_token( data, data.username, token_name="some token", scopes=[], ip_address="127.0.0.1", ) async with factory.session.begin(): assert await token_service.delete_token(token.key, data, data.username, ip_address="127.0.0.1") assert await token_service.get_data(token) is None async with factory.session.begin(): assert await token_service.get_token_info_unchecked(token.key) is None assert await token_service.get_user_info(token) is None async with factory.session.begin(): assert not await token_service.delete_token( token.key, data, data.username, ip_address="127.0.0.1") async with factory.session.begin(): history = await token_service.get_change_history( data, token=token.key, username=data.username) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=TokenType.user, token_name="some token", scopes=[], expires=None, actor=data.username, action=TokenChange.revoke, ip_address="127.0.0.1", event_time=history.entries[0].event_time, ), TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=TokenType.user, token_name="some token", scopes=[], expires=None, actor=data.username, action=TokenChange.create, ip_address="127.0.0.1", event_time=history.entries[1].event_time, ), ] # Cannot delete someone else's token. async with factory.session.begin(): token = await token_service.create_user_token( data, data.username, token_name="some token", scopes=[], ip_address="127.0.0.1", ) other_data = await create_session_token(factory, username="******") async with factory.session.begin(): with pytest.raises(PermissionDeniedError): await token_service.delete_token(token.key, other_data, data.username, ip_address="127.0.0.1") # Admins can delete soemone else's token. admin_data = await create_session_token(factory, username="******", scopes=["admin:token"]) assert await token_service.get_data(token) async with factory.session.begin(): assert await token_service.delete_token(token.key, admin_data, data.username, ip_address="127.0.0.1") assert await token_service.get_data(token) is None
async def test_notebook_token(setup: SetupTest) -> None: user_info = TokenUserInfo( username="******", name="Example Person", uid=4137, groups=[TokenGroup(name="foo", id=1000)], ) token_service = setup.factory.create_token_service() session_token = await token_service.create_session_token( user_info, scopes=["read:all", "exec:admin", "user:token"], ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) assert data token = await token_service.get_notebook_token(data, ip_address="1.0.0.1") assert await token_service.get_user_info(token) == user_info info = token_service.get_token_info_unchecked(token.key) assert info and info == TokenInfo( token=token.key, username=user_info.username, token_type=TokenType.notebook, scopes=["exec:admin", "read:all", "user:token"], created=info.created, last_used=None, expires=data.expires, parent=session_token.key, ) assert_is_now(info.created) assert await token_service.get_data(token) == TokenData( token=token, username=user_info.username, token_type=TokenType.notebook, scopes=["exec:admin", "read:all", "user:token"], created=info.created, expires=data.expires, name=user_info.name, uid=user_info.uid, groups=user_info.groups, ) # Creating another notebook token from the same parent token just returns # the same notebook token as before. new_token = await token_service.get_notebook_token( data, ip_address="127.0.0.1" ) assert token == new_token history = token_service.get_change_history( data, token=token.key, username=data.username ) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=TokenType.notebook, parent=data.token.key, scopes=["exec:admin", "read:all", "user:token"], expires=data.expires, actor=data.username, action=TokenChange.create, ip_address="1.0.0.1", event_time=info.created, ) ] # Check that the expiration time is capped by creating a user token that # doesn't expire and then creating a notebook token from it. user_token = await token_service.create_user_token( data, data.username, token_name="some token", scopes=[], expires=None, ip_address="127.0.0.1", ) data = await token_service.get_data(user_token) assert data new_token = await token_service.get_notebook_token( data, ip_address="127.0.0.1" ) assert new_token != token info = token_service.get_token_info_unchecked(new_token.key) assert info expires = info.created + timedelta(minutes=setup.config.issuer.exp_minutes) assert info.expires == expires
async def test_session_token(config: Config, factory: ComponentFactory) -> None: token_service = factory.create_token_service() user_info = TokenUserInfo( username="******", name="Example Person", uid=4137, groups=[ TokenGroup(name="group", id=1000), TokenGroup(name="another", id=3134), ], ) async with factory.session.begin(): token = await token_service.create_session_token( user_info, scopes=["user:token"], ip_address="127.0.0.1") data = await token_service.get_data(token) assert data and data == TokenData( token=token, username="******", token_type=TokenType.session, scopes=["user:token"], created=data.created, expires=data.expires, name="Example Person", uid=4137, groups=[ TokenGroup(name="group", id=1000), TokenGroup(name="another", id=3134), ], ) assert_is_now(data.created) expires = data.created + timedelta(minutes=config.issuer.exp_minutes) assert data.expires == expires async with factory.session.begin(): info = await token_service.get_token_info_unchecked(token.key) assert info and info == TokenInfo( token=token.key, username=user_info.username, token_name=None, token_type=TokenType.session, scopes=data.scopes, created=int(data.created.timestamp()), last_used=None, expires=int(data.expires.timestamp()), parent=None, ) assert await token_service.get_user_info(token) == user_info async with factory.session.begin(): history = await token_service.get_change_history( data, token=token.key, username=data.username) assert history.entries == [ TokenChangeHistoryEntry( token=token.key, username=data.username, token_type=TokenType.session, scopes=["user:token"], expires=data.expires, actor=data.username, action=TokenChange.create, ip_address="127.0.0.1", event_time=data.created, ) ] # Test a session token with scopes. async with factory.session.begin(): token = await token_service.create_session_token( user_info, scopes=["read:all", "exec:admin"], ip_address="127.0.0.1", ) data = await token_service.get_data(token) assert data and data.scopes == ["exec:admin", "read:all"] info = await token_service.get_token_info_unchecked(token.key) assert info and info.scopes == ["exec:admin", "read:all"]
async def test_internal_token(config: Config, factory: ComponentFactory) -> None: user_info = TokenUserInfo( username="******", name="Example Person", uid=4137, groups=[TokenGroup(name="foo", id=1000)], ) token_service = factory.create_token_service() async with factory.session.begin(): session_token = await token_service.create_session_token( user_info, scopes=["read:all", "exec:admin", "user:token"], ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) assert data async with factory.session.begin(): internal_token = await token_service.get_internal_token( data, service="some-service", scopes=["read:all"], ip_address="2001:db8::45", ) assert await token_service.get_user_info(internal_token) == user_info info = await token_service.get_token_info_unchecked(internal_token.key) assert info and info == TokenInfo( token=internal_token.key, username=user_info.username, token_type=TokenType.internal, service="some-service", scopes=["read:all"], created=info.created, last_used=None, expires=data.expires, parent=session_token.key, ) assert_is_now(info.created) # Cannot request a scope that the parent token doesn't have. with pytest.raises(InvalidScopesError): await token_service.get_internal_token( data, service="some-service", scopes=["read:some"], ip_address="127.0.0.1", ) # Creating another internal token from the same parent token with the same # parameters just returns the same internal token as before. new_internal_token = await token_service.get_internal_token( data, service="some-service", scopes=["read:all"], ip_address="127.0.0.1", ) assert internal_token == new_internal_token # Try again with the cache cleared to force a database lookup. await token_service._token_cache.clear() async with factory.session.begin(): new_internal_token = await token_service.get_internal_token( data, service="some-service", scopes=["read:all"], ip_address="127.0.0.1", ) assert internal_token == new_internal_token async with factory.session.begin(): history = await token_service.get_change_history( data, token=internal_token.key, username=data.username) assert history.entries == [ TokenChangeHistoryEntry( token=internal_token.key, username=data.username, token_type=TokenType.internal, parent=data.token.key, service="some-service", scopes=["read:all"], expires=data.expires, actor=data.username, action=TokenChange.create, ip_address="2001:db8::45", event_time=info.created, ) ] # It's possible we'll have a race condition where two workers both create # an internal token at the same time with the same parameters. Gafaelfawr # 3.0.2 had a regression where, once that had happened, it could not # retrieve the internal token because it didn't expect multiple results # from the query. Simulate this and make sure it's handled properly. The # easiest way to do this is to use the internals of the token service. second_internal_token = Token() created = current_datetime() expires = created + config.token_lifetime internal_token_data = TokenData( token=second_internal_token, username=data.username, token_type=TokenType.internal, scopes=["read:all"], created=created, expires=expires, name=data.name, email=data.email, uid=data.uid, groups=data.groups, ) await token_service._token_redis_store.store_data(internal_token_data) async with factory.session.begin(): await token_service._token_db_store.add(internal_token_data, service="some-service", parent=data.token.key) await token_service._token_cache.clear() async with factory.session.begin(): dup_internal_token = await token_service.get_internal_token( data, service="some-service", scopes=["read:all"], ip_address="127.0.0.1", ) assert dup_internal_token in (internal_token, second_internal_token) # A different scope or a different service results in a new token. async with factory.session.begin(): new_internal_token = await token_service.get_internal_token( data, service="some-service", scopes=["exec:admin"], ip_address="127.0.0.1", ) assert internal_token != new_internal_token async with factory.session.begin(): new_internal_token = await token_service.get_internal_token( data, service="another-service", scopes=["read:all"], ip_address="127.0.0.1", ) assert internal_token != new_internal_token # Check that the expiration time is capped by creating a user token that # doesn't expire and then creating a notebook token from it. Use this to # test a token with empty scopes. async with factory.session.begin(): user_token = await token_service.create_user_token( data, data.username, token_name="some token", scopes=["exec:admin"], expires=None, ip_address="127.0.0.1", ) data = await token_service.get_data(user_token) assert data async with factory.session.begin(): new_internal_token = await token_service.get_internal_token( data, service="some-service", scopes=[], 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 and info.scopes == [] expires = info.created + timedelta(minutes=config.issuer.exp_minutes) assert info.expires == expires
async def test_user_token(factory: ComponentFactory) -> None: user_info = TokenUserInfo(username="******", name="Example Person", uid=4137) token_service = factory.create_token_service() async with factory.session.begin(): session_token = await token_service.create_session_token( user_info, 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. async with factory.session.begin(): user_token = await token_service.create_user_token( data, "example", token_name="some-token", scopes=["read:all", "exec:admin"], expires=expires, ip_address="192.168.0.1", ) assert await token_service.get_user_info(user_token) == user_info info = await token_service.get_token_info_unchecked(user_token.key) assert info and info == TokenInfo( token=user_token.key, username=user_info.username, token_name="some-token", token_type=TokenType.user, scopes=["exec:admin", "read:all"], created=info.created, last_used=None, expires=int(expires.timestamp()), parent=None, ) assert_is_now(info.created) assert await token_service.get_data(user_token) == TokenData( token=user_token, username=user_info.username, token_type=TokenType.user, scopes=["exec:admin", "read:all"], created=info.created, expires=info.expires, name=user_info.name, uid=user_info.uid, ) async with factory.session.begin(): history = await token_service.get_change_history( data, token=user_token.key, username=data.username) assert history.entries == [ TokenChangeHistoryEntry( token=user_token.key, username=data.username, token_type=TokenType.user, token_name="some-token", scopes=["exec:admin", "read:all"], expires=info.expires, actor=data.username, action=TokenChange.create, ip_address="192.168.0.1", event_time=info.created, ) ]
async def test_internal_token(setup: SetupTest) -> None: user_info = TokenUserInfo( username="******", name="Example Person", uid=4137, groups=[TokenGroup(name="foo", id=1000)], ) token_service = setup.factory.create_token_service() session_token = await token_service.create_session_token( user_info, scopes=["read:all", "exec:admin", "user:token"], ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) assert data internal_token = await token_service.get_internal_token( data, service="some-service", scopes=["read:all"], ip_address="2001:db8::45", ) assert await token_service.get_user_info(internal_token) == user_info info = token_service.get_token_info_unchecked(internal_token.key) assert info and info == TokenInfo( token=internal_token.key, username=user_info.username, token_type=TokenType.internal, service="some-service", scopes=["read:all"], created=info.created, last_used=None, expires=data.expires, parent=session_token.key, ) assert_is_now(info.created) # Cannot request a scope that the parent token doesn't have. with pytest.raises(InvalidScopesError): await token_service.get_internal_token( data, service="some-service", scopes=["read:some"], ip_address="127.0.0.1", ) # Creating another internal token from the same parent token with the same # parameters just returns the same internal token as before. new_internal_token = await token_service.get_internal_token( data, service="some-service", scopes=["read:all"], ip_address="127.0.0.1", ) assert internal_token == new_internal_token history = token_service.get_change_history( data, token=internal_token.key, username=data.username ) assert history.entries == [ TokenChangeHistoryEntry( token=internal_token.key, username=data.username, token_type=TokenType.internal, parent=data.token.key, service="some-service", scopes=["read:all"], expires=data.expires, actor=data.username, action=TokenChange.create, ip_address="2001:db8::45", event_time=info.created, ) ] # A different scope or a different service results in a new token. new_internal_token = await token_service.get_internal_token( data, service="some-service", 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"], ip_address="127.0.0.1", ) assert internal_token != new_internal_token # Check that the expiration time is capped by creating a user token that # doesn't expire and then creating a notebook token from it. Use this to # test a token with empty scopes. user_token = await token_service.create_user_token( data, data.username, token_name="some token", 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" ) assert new_internal_token != internal_token info = token_service.get_token_info_unchecked(new_internal_token.key) assert info and info.scopes == [] expires = info.created + timedelta(minutes=setup.config.issuer.exp_minutes) assert info.expires == expires
def entry_to_dict(entry: TokenChangeHistoryEntry) -> Dict[str, Any]: """Convert a history entry to the expected API output.""" reduced_entry = TokenChangeHistoryEntry(**entry.reduced_dict()) return json.loads(reduced_entry.json(exclude_unset=True))
async def test_modify(factory: ComponentFactory) -> None: user_info = TokenUserInfo(username="******", name="Example Person", uid=4137) token_service = factory.create_token_service() async with factory.session.begin(): session_token = await token_service.create_session_token( user_info, scopes=["read:all", "user:token"], ip_address="127.0.0.1", ) data = await token_service.get_data(session_token) assert data async with factory.session.begin(): user_token = await token_service.create_user_token( data, data.username, token_name="some-token", scopes=[], ip_address="127.0.0.1", ) expires = current_datetime() + timedelta(days=50) async with factory.session.begin(): await token_service.modify_token( user_token.key, data, token_name="happy token", ip_address="127.0.0.1", ) await token_service.modify_token( user_token.key, data, scopes=["read:all"], expires=expires, ip_address="192.168.0.4", ) info = await token_service.get_token_info_unchecked(user_token.key) assert info and info == TokenInfo( token=user_token.key, username="******", token_type=TokenType.user, token_name="happy token", scopes=["read:all"], created=info.created, expires=expires, last_used=None, parent=None, ) async with factory.session.begin(): await token_service.modify_token( user_token.key, data, expires=None, no_expire=True, ip_address="127.0.4.5", ) async with factory.session.begin(): history = await token_service.get_change_history( data, token=user_token.key, username=data.username) assert history.entries == [ TokenChangeHistoryEntry( token=user_token.key, username=data.username, token_type=TokenType.user, token_name="happy token", scopes=["read:all"], expires=None, actor=data.username, action=TokenChange.edit, old_expires=expires, ip_address="127.0.4.5", event_time=history.entries[0].event_time, ), TokenChangeHistoryEntry( token=user_token.key, username=data.username, token_type=TokenType.user, token_name="happy token", scopes=["read:all"], expires=expires, actor=data.username, action=TokenChange.edit, old_scopes=[], old_expires=None, ip_address="192.168.0.4", event_time=history.entries[1].event_time, ), TokenChangeHistoryEntry( token=user_token.key, username=data.username, token_type=TokenType.user, token_name="happy token", scopes=[], expires=None, actor=data.username, action=TokenChange.edit, old_token_name="some-token", ip_address="127.0.0.1", event_time=history.entries[2].event_time, ), TokenChangeHistoryEntry( token=user_token.key, username=data.username, token_type=TokenType.user, token_name="some-token", scopes=[], expires=None, actor=data.username, action=TokenChange.create, ip_address="127.0.0.1", event_time=info.created, ), ]