fix authentication errors #238
4 changed files with 154 additions and 30 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = '6.2.1'
|
||||
__version__ = '6.2.2'
|
||||
|
|
|
|||
81
dump_things_service/authenticate.py
Normal file
81
dump_things_service/authenticate.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue