Ensure instance-state updates when collections are PUT or DELETE-ed #252
3 changed files with 108 additions and 13 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -1,3 +1,29 @@
|
||||||
|
# 6.3.1 (2026-07-03)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Use canonical storage representation of records. This reduces "noise" in
|
||||||
|
audit-backends by minimizing the differences between record versions. This
|
||||||
|
also reduces the noise in change set in the `dtc`-subcommand `auto-curate`.
|
||||||
|
|
||||||
|
- `GET /tokens`-endpoint now returns full token information, including the
|
||||||
|
token representation. This allows to fully reproduce a configuration on
|
||||||
|
a different server via API-calls only.
|
||||||
|
|
||||||
|
- `gitaudit`-audit backends now use a fixed user-id. This prevents start up
|
||||||
|
errors in environments where no git-user id is configured.
|
||||||
|
|
||||||
|
|
||||||
|
# 6.3.0 (2026-06-29)
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Automated annotation adding in write-record-endpoints has been removed.
|
||||||
|
The new query parameter `add_submission_tag` is added to write-record-endpoints.
|
||||||
|
If set to True, the server will add an automated annotation with the annotation
|
||||||
|
tags that are defined in the server-configuration (the default is False).
|
||||||
|
|
||||||
|
|
||||||
# 6.3.0 (2026-06-29)
|
# 6.3.0 (2026-06-29)
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from fastapi_pagination import add_pagination
|
from fastapi_pagination import add_pagination
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ tag_groups = [
|
||||||
'incoming_write',
|
'incoming_write',
|
||||||
]
|
]
|
||||||
|
|
||||||
openapi_tags_template = [
|
openapi_tags_template: list[dict | str] = [
|
||||||
{
|
{
|
||||||
'name': 'Server management',
|
'name': 'Server management',
|
||||||
'description': 'General server operations',
|
'description': 'General server operations',
|
||||||
|
|
@ -115,7 +116,7 @@ def manifest_configuration(
|
||||||
# because token-objects are all re-created below).
|
# because token-objects are all re-created below).
|
||||||
for collection_name in deleted_collection_names:
|
for collection_name in deleted_collection_names:
|
||||||
delete_endpoints_for_collection(instance_state, collection_name)
|
delete_endpoints_for_collection(instance_state, collection_name)
|
||||||
delete_collection(instance_state, collection_name)
|
delete_collection_from_instance_state(instance_state, collection_name)
|
||||||
|
|
||||||
# Create the internal representation objects for collections that have been
|
# Create the internal representation objects for collections that have been
|
||||||
# added to the configuration.
|
# added to the configuration.
|
||||||
|
|
@ -172,21 +173,28 @@ def delete_token(
|
||||||
instance_state.tokens.pop(token_name)
|
instance_state.tokens.pop(token_name)
|
||||||
|
|
||||||
|
|
||||||
def delete_collection(
|
def delete_collection_from_instance_state(
|
||||||
instance_state: InstanceState,
|
instance_state: InstanceState,
|
||||||
collection_name: str,
|
collection_name: str,
|
||||||
):
|
):
|
||||||
instance_state.collections.pop(collection_name)
|
# TODO: Maybe all collection-specific information
|
||||||
|
|
||||||
# TODO: remove further collection-related information from
|
|
||||||
# instance_state. Maybe all collection-specific information
|
|
||||||
# should go into the instance_state.collection[x]-object!?
|
# should go into the instance_state.collection[x]-object!?
|
||||||
# That would allow to remove it easily.
|
# That would allow to remove it easily.
|
||||||
|
|
||||||
|
# Remove objects that exist for every collection
|
||||||
|
instance_state.collections.pop(collection_name)
|
||||||
|
instance_state.curated_stores.pop(collection_name)
|
||||||
|
instance_state.validators.pop(collection_name)
|
||||||
|
instance_state.auth_sources.pop(collection_name)
|
||||||
|
|
||||||
|
# Remove optional objects
|
||||||
|
instance_state.audit_backends.pop(collection_name, None)
|
||||||
|
instance_state.incoming_stores.pop(collection_name, None)
|
||||||
|
|
||||||
|
|
||||||
def create_openapi_tags(
|
def create_openapi_tags(
|
||||||
instance_state: InstanceState,
|
instance_state: InstanceState,
|
||||||
openapi_tags_template: list[dict | str],
|
openapi_tags_template_: list[dict | str],
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
# Collect tag name lists for all tag groups that we have defined.
|
# Collect tag name lists for all tag groups that we have defined.
|
||||||
tag_group_info = {
|
tag_group_info = {
|
||||||
|
|
@ -199,8 +207,8 @@ def create_openapi_tags(
|
||||||
)
|
)
|
||||||
for tag_group in tag_groups
|
for tag_group in tag_groups
|
||||||
}
|
}
|
||||||
result = openapi_tags_template.copy()
|
result = openapi_tags_template_.copy()
|
||||||
for tag_group, tag_list in tag_group_info.items():
|
for tag_group, tag_list in tag_group_info.items():
|
||||||
index = result.index(tag_group)
|
index = result.index(tag_group)
|
||||||
result[index : index + 1] = tag_list
|
result[index : index + 1] = tag_list
|
||||||
return result
|
return cast('list[dict]', result)
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,14 @@ from dump_things_service import (
|
||||||
HTTP_404_NOT_FOUND,
|
HTTP_404_NOT_FOUND,
|
||||||
)
|
)
|
||||||
from dump_things_service.abstract_config import (
|
from dump_things_service.abstract_config import (
|
||||||
|
ForgejoAuthSpec,
|
||||||
GitAuditBackendConfig,
|
GitAuditBackendConfig,
|
||||||
TokenCollectionConfig,
|
TokenCollectionConfig,
|
||||||
TokenModes,
|
TokenModes,
|
||||||
hash_token_representation,
|
hash_token_representation,
|
||||||
)
|
)
|
||||||
from dump_things_service.collection_endpoints import CollectionRequest
|
from dump_things_service.collection_endpoints import CollectionRequest
|
||||||
|
from dump_things_service.instance_state import get_instance_state
|
||||||
from dump_things_service.token_endpoints import (
|
from dump_things_service.token_endpoints import (
|
||||||
AdminTokenRequest,
|
AdminTokenRequest,
|
||||||
TokenRequest,
|
TokenRequest,
|
||||||
|
|
@ -82,15 +84,18 @@ def test_collection_adding(fastapi_client_simple):
|
||||||
assert response.status_code == HTTP_404_NOT_FOUND
|
assert response.status_code == HTTP_404_NOT_FOUND
|
||||||
assert not _name_in_openapi_paths(test_client, new_collection_name)
|
assert not _name_in_openapi_paths(test_client, new_collection_name)
|
||||||
|
|
||||||
# Add a new collection
|
# Add a new collection to the configuration
|
||||||
response = test_client.post(
|
response = test_client.post(
|
||||||
'/collections',
|
'/collections',
|
||||||
headers={'x-dumpthings-token': admin_token},
|
headers={'x-dumpthings-token': admin_token},
|
||||||
json=new_collection_request.model_dump(mode='json', by_alias=True),
|
json=new_collection_request.model_dump(mode='json', by_alias=True),
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_201_CREATED
|
assert response.status_code == HTTP_201_CREATED
|
||||||
|
|
||||||
|
# Check that the collection endpoints exist in the open API document
|
||||||
assert _name_in_openapi_paths(test_client, new_collection_name)
|
assert _name_in_openapi_paths(test_client, new_collection_name)
|
||||||
|
|
||||||
|
# Read back the new collection and compare it
|
||||||
response = test_client.get(
|
response = test_client.get(
|
||||||
f'/collections/{new_collection_name}',
|
f'/collections/{new_collection_name}',
|
||||||
headers={'x-dumpthings-token': admin_token},
|
headers={'x-dumpthings-token': admin_token},
|
||||||
|
|
@ -124,13 +129,12 @@ def test_collection_adding(fastapi_client_simple):
|
||||||
'representation': new_token_request.representation,
|
'representation': new_token_request.representation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add a new record to the collection to verify that the token works
|
||||||
new_record = {
|
new_record = {
|
||||||
'pid': 'http://example.com/admin-test-1',
|
'pid': 'http://example.com/admin-test-1',
|
||||||
'given_name': 'Admin Test 1',
|
'given_name': 'Admin Test 1',
|
||||||
'schema_type': 'abc:Person',
|
'schema_type': 'abc:Person',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add a record to the collection
|
|
||||||
response = test_client.post(
|
response = test_client.post(
|
||||||
f'/{new_collection_name}/record/Person',
|
f'/{new_collection_name}/record/Person',
|
||||||
headers={'x-dumpthings-token': new_token_representation},
|
headers={'x-dumpthings-token': new_token_representation},
|
||||||
|
|
@ -313,3 +317,60 @@ def test_admin_token_management(fastapi_client_simple):
|
||||||
)
|
)
|
||||||
assert response.status_code == HTTP_200_OK
|
assert response.status_code == HTTP_200_OK
|
||||||
assert new_admin_token_name not in response.json()
|
assert new_admin_token_name not in response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def test_instance_state_updates(fastapi_client_simple):
|
||||||
|
test_client, _, admin_token = fastapi_client_simple
|
||||||
|
|
||||||
|
instance_state = get_instance_state()
|
||||||
|
|
||||||
|
forgejo_auth_spec = ForgejoAuthSpec(
|
||||||
|
type='forgejo',
|
||||||
|
url='http://localhost',
|
||||||
|
organization='orga-1',
|
||||||
|
team='team-1',
|
||||||
|
label_type='user',
|
||||||
|
)
|
||||||
|
|
||||||
|
forgejo_audit_collection_name = 'test_forgejo_audit_collection'
|
||||||
|
forgejo_audit_collection_request = CollectionRequest(
|
||||||
|
name=forgejo_audit_collection_name,
|
||||||
|
default_token='test_default_token',
|
||||||
|
curated=PurePosixPath(f'{curated}/{forgejo_audit_collection_name}'),
|
||||||
|
schema=test_schema_location,
|
||||||
|
incoming=PurePosixPath(f'{incoming}/{forgejo_audit_collection_name}'),
|
||||||
|
auth_sources=[forgejo_auth_spec],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Post a collection with forgejo audit-backend without an instance_id
|
||||||
|
response = test_client.post(
|
||||||
|
'/collections',
|
||||||
|
headers={'x-dumpthings-token': admin_token},
|
||||||
|
json=forgejo_audit_collection_request.model_dump(mode='json', by_alias=True),
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_201_CREATED
|
||||||
|
forgejo_instance = instance_state.auth_sources[forgejo_audit_collection_name][0]
|
||||||
|
assert forgejo_instance.instance_id is None
|
||||||
|
|
||||||
|
# Put a collection with updated forgejo audit-backend, containing an instance_id
|
||||||
|
forgejo_auth_spec.instance_id = 'instance-id'
|
||||||
|
response = test_client.put(
|
||||||
|
'/collections',
|
||||||
|
headers={'x-dumpthings-token': admin_token},
|
||||||
|
json=forgejo_audit_collection_request.model_dump(mode='json', by_alias=True),
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_201_CREATED
|
||||||
|
auth_sources = instance_state.auth_sources[forgejo_audit_collection_name]
|
||||||
|
assert len(auth_sources) == 1
|
||||||
|
new_forgejo_instance = auth_sources[0]
|
||||||
|
|
||||||
|
assert new_forgejo_instance is not forgejo_instance
|
||||||
|
assert new_forgejo_instance.instance_id == 'instance-id'
|
||||||
|
|
||||||
|
# Delete the collection again because we check for a known number of
|
||||||
|
# collections in other tests.
|
||||||
|
response = test_client.delete(
|
||||||
|
f'/collections/{forgejo_audit_collection_name}',
|
||||||
|
headers={'x-dumpthings-token': admin_token},
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue