From 101be3d1190b1a093a1f6fd63823c74bd09780ea Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Thu, 29 Jan 2026 09:17:37 +0100 Subject: [PATCH 1/4] pick up token permission changes This commit moves a test for cached stores behind the token authentication. This allows to pick up token permission changes that happened during the runtime of the server --- dump_things_service/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dump_things_service/utils.py b/dump_things_service/utils.py index fe06f1b..cb331eb 100644 --- a/dump_things_service/utils.py +++ b/dump_things_service/utils.py @@ -281,12 +281,6 @@ def get_token_store( ) -> tuple[ModelStore, str, TokenPermission, str] | tuple[None, None, None, None]: check_collection(instance_config, collection_name) - # Check whether a store for this collection and token does already exist. - # If the token is a hashed token, we have to - store_info = instance_config.token_stores[collection_name].get(plain_token) - if store_info: - return store_info - # Try to authenticate the token with the authentication providers that # are associated with the collection. auth_info = authenticate_token(instance_config, collection_name, plain_token) @@ -319,6 +313,11 @@ def get_token_store( detail='No incoming area for collection ' + collection_name ) + # Check whether a store for this collection and token does already exist. + store_info = instance_config.token_stores[collection_name].get(plain_token) + if store_info: + return store_info + store_dir = instance_config.store_path / incoming / auth_info.incoming_label token_store = create_token_store( instance_config=instance_config, -- 2.52.0 From c6ed54cb38c13f0dd733e6bad1981ac5706b2084 Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Thu, 29 Jan 2026 09:44:09 +0100 Subject: [PATCH 2/4] add maintenance mode endpoint and logic --- dump_things_service/__init__.py | 2 ++ dump_things_service/config.py | 2 +- dump_things_service/main.py | 46 ++++++++++++++++++++++++++++++--- dump_things_service/utils.py | 14 ++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/dump_things_service/__init__.py b/dump_things_service/__init__.py index fe02332..5fe7ad2 100644 --- a/dump_things_service/__init__.py +++ b/dump_things_service/__init__.py @@ -12,6 +12,7 @@ from starlette.status import ( HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_500_INTERNAL_SERVER_ERROR, + HTTP_503_SERVICE_UNAVAILABLE, ) from starlette.status import ( HTTP_413_REQUEST_ENTITY_TOO_LARGE as HTTP_413_CONTENT_TOO_LARGE, @@ -31,6 +32,7 @@ __all__ = [ 'HTTP_413_CONTENT_TOO_LARGE', 'HTTP_422_UNPROCESSABLE_CONTENT', 'HTTP_500_INTERNAL_SERVER_ERROR', + 'HTTP_503_SERVICE_UNAVAILABLE', 'JSON', 'YAML', 'config_file_name', diff --git a/dump_things_service/config.py b/dump_things_service/config.py index 3c390dc..0c69306 100644 --- a/dump_things_service/config.py +++ b/dump_things_service/config.py @@ -172,7 +172,7 @@ class InstanceConfig: hashed_tokens: dict = dataclasses.field(default_factory=dict) validators: dict = dataclasses.field(default_factory=dict) use_classes: dict = dataclasses.field(default_factory=dict) - + maintenance_mode: set = dataclasses.field(default_factory=set) mode_mapping = { TokenModes.READ_CURATED: TokenPermission(curated_read=True), diff --git a/dump_things_service/main.py b/dump_things_service/main.py index 8de67f0..5fb7049 100644 --- a/dump_things_service/main.py +++ b/dump_things_service/main.py @@ -80,6 +80,7 @@ from dump_things_service.model import ( get_subclasses, ) from dump_things_service.utils import ( + authenticate_token, check_bounds, check_collection, combine_ttl, @@ -166,8 +167,8 @@ of the project. tag_info = [ { - 'name': 'Server info', - 'description': 'Get general information about the server', + 'name': 'Server management', + 'description': 'General server operations', }, { 'name': 'Read records', @@ -407,7 +408,7 @@ async def root() -> RedirectResponse: @app.get( '/server', - tags=['Server info'], + tags=['Server management'], name='get server information' ) async def server() -> ServerResponse: @@ -424,6 +425,45 @@ async def server() -> ServerResponse: ) +@app.get( + '/maintenance', + tags=['Server management'], + name='put a collection in maintenance mode' +) +async def maintenance( + collection: str, + active: bool, + api_key: str | None = Depends(api_key_header_scheme), +): + + if api_key is None: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=f'Token required for this operation', + ) + + # Try to authenticate the token with the authentication providers that + # are associated with the collection. + auth_info = authenticate_token(g_instance_config, collection, api_key) + permissions = auth_info.token_permission + + if not ( + permissions.curated_write + and permissions.curated_read + and permissions.zones_access + ): + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=f'Curator permissions required for this operation', + ) + + if active: + g_instance_config.maintenance_mode.add(collection) + else: + g_instance_config.maintenance_mode.remove(collection) + return + + @app.get( '/{collection}/record', tags=['Read records'], diff --git a/dump_things_service/utils.py b/dump_things_service/utils.py index cb331eb..064dcf3 100644 --- a/dump_things_service/utils.py +++ b/dump_things_service/utils.py @@ -20,6 +20,7 @@ from dump_things_service import ( HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_413_CONTENT_TOO_LARGE, + HTTP_503_SERVICE_UNAVAILABLE, ) from dump_things_service.auth import ( AuthenticationError, @@ -213,6 +214,19 @@ async def process_token( final_permissions = join_default_token_permissions( instance_config, token_permissions, collection ) + + # Check for maintenance mode + if collection in instance_config.maintenance_mode: + if not ( + final_permissions.curated_read + and final_permissions.curated_write + and final_permissions.zones_access + ): + raise HTTPException( + status_code=HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Collection '{collection}' is in maintenance mode", + ) + if not final_permissions.incoming_read and not final_permissions.curated_read: raise HTTPException( status_code=HTTP_403_FORBIDDEN, -- 2.52.0 From 7bc8861b4dd298654bb56a6550c35ef96a52c975 Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Thu, 29 Jan 2026 15:03:07 +0100 Subject: [PATCH 3/4] convert maintenance to POST, add maintenance test --- dump_things_service/main.py | 16 ++++--- dump_things_service/tests/test_basic.py | 56 +++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/dump_things_service/main.py b/dump_things_service/main.py index 5fb7049..b8d9408 100644 --- a/dump_things_service/main.py +++ b/dump_things_service/main.py @@ -95,8 +95,9 @@ if TYPE_CHECKING: from dump_things_service.lazy_list import LazyList -class TokenCapabilityRequest(BaseModel): - token: str | None +class MaintenanceRequest(BaseModel): + collection: str + active: bool class ServerCollectionResponse(BaseModel): @@ -425,15 +426,14 @@ async def server() -> ServerResponse: ) -@app.get( +@app.post( '/maintenance', tags=['Server management'], name='put a collection in maintenance mode' ) async def maintenance( - collection: str, - active: bool, - api_key: str | None = Depends(api_key_header_scheme), + body: MaintenanceRequest, + api_key: str | None = Depends(api_key_header_scheme), ): if api_key is None: @@ -442,8 +442,12 @@ async def maintenance( detail=f'Token required for this operation', ) + collection = body.collection + active = body.active + # Try to authenticate the token with the authentication providers that # are associated with the collection. + check_collection(g_instance_config, collection) auth_info = authenticate_token(g_instance_config, collection, api_key) permissions = auth_info.token_permission diff --git a/dump_things_service/tests/test_basic.py b/dump_things_service/tests/test_basic.py index fbf87cb..31068b1 100644 --- a/dump_things_service/tests/test_basic.py +++ b/dump_things_service/tests/test_basic.py @@ -4,9 +4,11 @@ import pytest # F401 from .. import ( HTTP_200_OK, + HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, + HTTP_503_SERVICE_UNAVAILABLE, ) from ..__about__ import __version__ from ..utils import cleaned_json @@ -446,3 +448,57 @@ def test_ignore_classes(fastapi_client_simple): json={'pid': f'dlflatsocial:c_{class_name}'}, ) assert response.status_code == HTTP_404_NOT_FOUND + + +def test_maintenance(fastapi_client_simple): + test_client, _ = fastapi_client_simple + + # Ensure that only curators can put a collection in maintenance mode + response = test_client.post( + '/maintenance', + headers={'x-dumpthings-token': 'token-1'}, + json={'collection': 'collection_1', 'active': True}, + ) + assert response.status_code == HTTP_400_BAD_REQUEST + + # Ensure unknown collections are caught in maintenance mode + response = test_client.post( + '/maintenance', + headers={'x-dumpthings-token': 'token_admin'}, + json={'collection': 'collection_x', 'active': True}, + ) + assert response.status_code == HTTP_404_NOT_FOUND + + response = test_client.post( + '/maintenance', + headers={'x-dumpthings-token': 'token_admin'}, + json={'collection': 'collection_1', 'active': True}, + ) + + response = test_client.get( + '/collection_1/record?pid=abc:something/', + headers={'x-dumpthings-token': 'token-1'}, + ) + assert response.status_code == HTTP_503_SERVICE_UNAVAILABLE + + # Ensure unknown collections are caught in maintenance mode + response = test_client.get( + '/collection_x/record?pid=abc:something/', + headers={'x-dumpthings-token': 'token-1'}, + ) + assert response.status_code == HTTP_404_NOT_FOUND + + # Deactivate maintenance mode + response = test_client.post( + '/maintenance', + headers={'x-dumpthings-token': 'token_admin'}, + json={'collection': 'collection_1', 'active': False}, + ) + assert response.status_code == HTTP_200_OK + + # Ensure that + response = test_client.get( + '/collection_1/record?pid=abc:something/', + headers={'x-dumpthings-token': 'token-1'}, + ) + assert response.status_code == HTTP_200_OK -- 2.52.0 From 35bcf8b9de0aaf235213c49663f996935eb8686f Mon Sep 17 00:00:00 2001 From: Christian Monch Date: Mon, 2 Feb 2026 08:35:46 +0100 Subject: [PATCH 4/4] update CHANGELOG.md, bump version to 5.4.0 --- CHANGELOG.md | 5 ++++- dump_things_service/__about__.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f49d771..06666ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 5.x.x (2026-01-28) +# 5.4.0 (2026-01-28) ## New features @@ -6,6 +6,9 @@ supported by the collections, i.e., classes for which storage- and validation-endpoints exist. +- Add `/maintenance`-endpoint to temporarilly lock collections for non-curator + access. + # 5.3.6 (2026-01-13) diff --git a/dump_things_service/__about__.py b/dump_things_service/__about__.py index bc9d0f4..271e4c0 100644 --- a/dump_things_service/__about__.py +++ b/dump_things_service/__about__.py @@ -1 +1 @@ -__version__ = '5.3.6' +__version__ = '5.4.0' -- 2.52.0