From dea905240a8f16fd62e5f1d935e7500a3108e83c Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Fri, 26 Jun 2026 11:20:36 +0200 Subject: [PATCH 1/2] fix authentication errors This commit fixes handling of default tokens, specifically hashed default tokens. This fixes an authentication error in non-curated access with non-local tokens. --- dump_things_service/authenticate.py | 81 +++++++++++++++++++++++++ dump_things_service/utils.py | 94 ++++++++++++++++++++--------- 2 files changed, 146 insertions(+), 29 deletions(-) create mode 100644 dump_things_service/authenticate.py diff --git a/dump_things_service/authenticate.py b/dump_things_service/authenticate.py new file mode 100644 index 0000000..cf4793b --- /dev/null +++ b/dump_things_service/authenticate.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import logging +from itertools import count +from typing import TYPE_CHECKING + +from fastapi import ( + APIRouter, + Depends, + FastAPI, + HTTPException, +) +from fastapi_pagination import ( + Page, + add_pagination, + paginate, +) + +from dump_things_service import ( + HTTP_401_UNAUTHORIZED, + HTTP_404_NOT_FOUND, + HTTP_422_UNPROCESSABLE_CONTENT, abstract_config, +) +from dump_things_service.abstract_config import ( + check_collection, + read_config, +) +from dump_things_service.api_key import api_key_header_scheme +from dump_things_service.auth import AuthenticationInfo +from dump_things_service.backends.schema_type_layer import _SchemaTypeLayer +from dump_things_service.exceptions import CurieResolutionError +from dump_things_service.instance_state import get_instance_state +from dump_things_service.lazy_list import ModifierList +from dump_things_service.utils import ( + authenticate_token, + check_bounds, + cleaned_json, + wrap_http_exception, +) + +if TYPE_CHECKING: + from pydantic import BaseModel + + from dump_things_service.backends import StorageBackend + from dump_things_service.lazy_list import LazyList + from dump_things_service.store.model_store import _ModelStore + + +def get_store_and_backend( + collection: str, + plain_token: str | None, +) -> tuple[_ModelStore, StorageBackend, AuthenticationInfo]: + + # A token is required + if plain_token is None: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail='token required', + ) + + instance_state = get_instance_state() + abstract_config = read_config(instance_state.store_path) + + # Check that the collection exists + check_collection(abstract_config=abstract_config, collection=collection) + + # Get token permissions + auth_info = authenticate_token(instance_state, collection, plain_token) + permissions = auth_info.token_permission + if permissions.curated_write is False: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail=f'no write access to curated area of collection `{collection}`', + ) + + # Get the curated model store + model_store = instance_state.curated_stores[collection] + backend = model_store.backend + if isinstance(backend, _SchemaTypeLayer): + return model_store, backend.backend, auth_info + return model_store, backend, auth_info diff --git a/dump_things_service/utils.py b/dump_things_service/utils.py index 328e3e9..ebb7d69 100644 --- a/dump_things_service/utils.py +++ b/dump_things_service/utils.py @@ -14,7 +14,6 @@ from contextlib import contextmanager from functools import reduce from typing import ( TYPE_CHECKING, - Callable, ) import fsspec @@ -30,14 +29,14 @@ from dump_things_service import ( ) from dump_things_service.abstract_config import ( Configuration, - RecordDirBackendConfig, TokenModes, TokenPermission, mode_mapping, check_collection, + get_collection_config_by_name, get_default_token_config, - get_token_config_for_representation_and_collection, get_mapping_function_by_name, + get_token_config_for_representation_and_collection, ) from dump_things_service.auth import ( AuthenticationError, @@ -206,20 +205,63 @@ def authenticate_token( return auth_info +def get_default_token_auth_info( + abstract_config: Configuration, + collection_name: str, + token_name: str, +) -> AuthenticationInfo: + token_config = abstract_config.tokens[token_name] + collection_info = token_config.collections.get(collection_name) + if collection_info is None: + detail = f"Default token not valid for collection '{collection_name}'" + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail=detail, + ) + return AuthenticationInfo( + token_permission=mode_mapping[TokenModes(collection_info.mode)], + user_id = token_config.user_id, + incoming_label = collection_info.incoming_label, + ) + + def get_token_store( abstract_config: Configuration, instance_state: InstanceState, collection_name: str, - token_representation: str, + token_representation: str | None, + *, + is_token_name: bool = False, ) -> tuple[_ModelStore, TokenPermission, str] | tuple[None, None, None, None]: - # Try to authenticate the token with the authentication providers that - # are associated with the collection. - auth_info = authenticate_token( - instance_state, - collection_name, - token_representation, - ) + # If a token representation is provided, try to authenticate the token + # with the authentication providers that are associated with the collection. + if not is_token_name: + if token_representation is None: + msg = 'get_token_store: token_representation is None and is_token_name is False. This is a calling error!' + logger.error(msg) + raise ValueError(msg) + + auth_info = authenticate_token( + instance_state, + collection_name, + token_representation, + ) + + else: + auth_info = get_default_token_auth_info( + abstract_config=abstract_config, + collection_name=collection_name, + token_name=token_representation, + ) + token_representation = None + + if not auth_info: + detail = f"invalid token for collection '{collection_name}'" + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail=detail, + ) permissions = auth_info.token_permission # If the token has no incoming-read or incoming-write permissions, we do not @@ -429,28 +471,22 @@ async def process_token( ) -> tuple[TokenPermission, _ModelStore]: if api_key is None: - token_config = get_default_token_config(abstract_config, collection) - else: - token_elements = get_token_config_for_representation_and_collection( + collection_config = get_collection_config_by_name(abstract_config, collection) + token_store, token_permissions, user_id = get_token_store( abstract_config, - collection_name=collection, - token_representation=api_key, + instance_state, + collection, + token_representation=collection_config.default_token, + is_token_name=True, ) - token_config = token_elements[1] if token_elements else None - - if not token_config: - detail = f"invalid token for collection '{collection}'" - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail=detail, + else: + token_store, token_permissions, user_id = get_token_store( + abstract_config, + instance_state, + collection, + api_key, ) - token_store, token_permissions, user_id = get_token_store( - abstract_config, - instance_state, - collection, - token_config.representation, - ) final_permissions = join_default_token_permissions( abstract_config, instance_state, token_permissions, collection ) -- 2.52.0 From 3bb6745eb3107e254a3bec752a7a22f67b08b315 Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Fri, 26 Jun 2026 11:30:40 +0200 Subject: [PATCH 2/2] updated changelog, bump version to 6.2.2 --- CHANGELOG.md | 7 +++++++ dump_things_service/__about__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 907a744..9f25ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 6.2.2 (2026-06-26) + +# Bugfixes + +- Fix a bug in authentication when non-config tokens are used. + + # 6.2.1 (2026-06-26) # Bugfixes diff --git a/dump_things_service/__about__.py b/dump_things_service/__about__.py index b133e0c..ac1df1f 100644 --- a/dump_things_service/__about__.py +++ b/dump_things_service/__about__.py @@ -1 +1 @@ -__version__ = '6.2.1' +__version__ = '6.2.2' -- 2.52.0