Skip to content

Scaffolding: Federation gateway setup#299

Open
klpoland wants to merge 17 commits into
masterfrom
feature-kpoland-federation-gateway-scaffolding
Open

Scaffolding: Federation gateway setup#299
klpoland wants to merge 17 commits into
masterfrom
feature-kpoland-federation-gateway-scaffolding

Conversation

@klpoland

@klpoland klpoland commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Note

High Risk
Touches authentication/API key scoping, new internal data-export endpoints with network ACLs, and changes dataset publish/detach behavior that existing clients may rely on.

Overview
Introduces federation on the gateway behind FEDERATION_ENABLED, with repo-root federation-shared.env (generated by generate-secrets.sh) loaded in local compose alongside new Django federation settings (site name, Redis channel, sync health probes, export CIDR allowlist).

When operational, dataset/capture saves trigger post-commit reindex into fed-datasets / fed-captures and Redis pub/sub notifications for the sync service. A new /api/.../federation/export/... surface is restricted to FederationSync Api-Key clients on allowed internal IPs; those keys are blocked elsewhere via a global default permission. Startup/prepare_gateway can sync the federation-sync DRF token and mint export keys through get-federation-sync-api-key (SVI minting refactored similarly).

Published datasets can no longer revert from final or public; detach-from-datasets only clears draft/private links. Federation export serializers and contract tests align with the sibling sds_federation Pydantic models.

Reviewed by Cursor Bugbot for commit 94ef4d5. Bugbot is set up for automated code reviews on this repo. Configure here.

@klpoland klpoland self-assigned this Jun 19, 2026
@klpoland klpoland added feature New feature or request federation Federation related work (sync service, document storage, peer configuration, etc.) labels Jun 19, 2026
@semanticdiff-com

semanticdiff-com Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

Changed Files
File Status
  gateway/sds_gateway/api_methods/views/capture_endpoints.py  63% smaller
  gateway/sds_gateway/api_methods/views/file_endpoints.py  48% smaller
  gateway/sds_gateway/api_methods/views/dataset_endpoints.py  42% smaller
  gateway/sds_gateway/api_methods/tests/test_authenticate.py  41% smaller
  gateway/sds_gateway/users/tests/test_quick_add_views.py  39% smaller
  gateway/sds_gateway/users/urls.py  27% smaller
  gateway/sds_gateway/users/api/views.py  25% smaller
  gateway/sds_gateway/api_methods/helpers/extract_drf_metadata.py  15% smaller
  gateway/sds_gateway/api_methods/utils/relationship_utils.py  13% smaller
  gateway/config/settings/base.py  5% smaller
  gateway/sds_gateway/api_methods/tests/factories.py  5% smaller
  .gitignore Unsupported file format
  federation-shared.example.env Unsupported file format
  gateway/.envs/example/django.env Unsupported file format
  gateway/compose.local.yaml  0% smaller
  gateway/config/api_router.py  0% smaller
  gateway/pyproject.toml Unsupported file format
  gateway/scripts/fallow-cross-file-dupes.sh Unsupported file format
  gateway/scripts/generate-secrets.sh Unsupported file format
  gateway/sds_gateway/api_methods/apps.py  0% smaller
  gateway/sds_gateway/api_methods/authentication.py  0% smaller
  gateway/sds_gateway/api_methods/federation/__init__.py  0% smaller
  gateway/sds_gateway/api_methods/federation/availability.py  0% smaller
  gateway/sds_gateway/api_methods/federation/compile_federated_data.py  0% smaller
  gateway/sds_gateway/api_methods/federation/events.py  0% smaller
  gateway/sds_gateway/api_methods/federation/export_contract.py  0% smaller
  gateway/sds_gateway/api_methods/federation/fed_index.py  0% smaller
  gateway/sds_gateway/api_methods/federation/permissions.py  0% smaller
  gateway/sds_gateway/api_methods/federation/redis_channel.py  0% smaller
  gateway/sds_gateway/api_methods/federation/reindex.py  0% smaller
  gateway/sds_gateway/api_methods/federation/signals.py  0% smaller
  gateway/sds_gateway/api_methods/management/commands/prepare_gateway.py  0% smaller
  gateway/sds_gateway/api_methods/models.py  0% smaller
  gateway/sds_gateway/api_methods/permissions.py  0% smaller
  gateway/sds_gateway/api_methods/serializers/capture_serializers.py  0% smaller
  gateway/sds_gateway/api_methods/serializers/dataset_serializers.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_dataset_endpoints.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_dataset_publish_immutability.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_federation_events.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_federation_export.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_federation_export_contract.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_federation_hardening.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_federation_signals.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_federation_sync_api_key.py  0% smaller
  gateway/sds_gateway/api_methods/tests/test_file_endpoints.py  0% smaller
  gateway/sds_gateway/api_methods/utils/asset_access_control.py  0% smaller
  gateway/sds_gateway/api_methods/views/federation_endpoints.py  0% smaller
  gateway/sds_gateway/users/backend_service_key_utils.py  0% smaller
  gateway/sds_gateway/users/management/commands/create_test_files.py  0% smaller
  gateway/sds_gateway/users/management/commands/init_federation_sync_token.py  0% smaller
  gateway/sds_gateway/users/management/commands/init_svi_token.py  0% smaller
  gateway/sds_gateway/users/migrations/0012_alter_userapikey_source_federation_sync.py  0% smaller
  gateway/sds_gateway/users/migrations/max_migration.txt Unsupported file format
  gateway/sds_gateway/users/models.py  0% smaller
  gateway/sds_gateway/users/svi_utils.py  0% smaller
  gateway/sds_gateway/users/tests/test_federation_sync_drf_init.py  0% smaller
  gateway/sds_gateway/users/tests/test_svi_utils.py  0% smaller

@klpoland klpoland changed the title gateway: federation export API, Redis events, sync API key Scaffolding: Federation gateway setup Jun 19, 2026
@klpoland klpoland added the gateway Gateway component label Jun 19, 2026
@klpoland

Copy link
Copy Markdown
Collaborator Author

Added some dedicated API endpoints, serializers, and signals for federation and configuration that defaults to disabling federation sync.

@klpoland klpoland force-pushed the feature-kpoland-federation-gateway-scaffolding branch 2 times, most recently from 9f94dfd to 83d51d0 Compare June 25, 2026 19:02
@klpoland klpoland marked this pull request as ready for review June 25, 2026 19:03
@klpoland klpoland requested a review from lucaspar June 25, 2026 19:03
Comment thread gateway/sds_gateway/api_methods/views/dataset_endpoints.py Outdated
Comment thread gateway/sds_gateway/api_methods/federation/availability.py
@klpoland klpoland force-pushed the feature-kpoland-federation-gateway-scaffolding branch from 83d51d0 to 4bbcbed Compare June 25, 2026 19:53
Comment thread gateway/.envs/example/django.env
Comment thread gateway/config/settings/base.py
Comment thread gateway/sds_gateway/api_methods/federation/events.py Outdated
Comment thread gateway/sds_gateway/api_methods/federation/export_contract.py Outdated
Comment thread gateway/sds_gateway/api_methods/federation/availability.py Outdated
@lucaspar lucaspar added the migrations Code changes that require data or schema migrations in the database. label Jun 25, 2026
Comment thread gateway/sds_gateway/users/management/commands/create_test_files.py Outdated
Comment thread gateway/sds_gateway/users/management/commands/create_federation_sync_api_key.py Outdated
Comment thread gateway/.envs/example/django.env
Comment thread gateway/sds_gateway/api_methods/apps.py Outdated
Comment thread gateway/config/settings/base.py Outdated
Comment thread gateway/config/settings/base.py
Comment thread gateway/sds_gateway/api_methods/federation/signals.py Outdated
@klpoland

klpoland commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Refactor:

  • Index into local federated index FIRST then send Redis signal.
  • Redis triggers export API call on sync service which queries Opensearch, not Postgres, for doc body.

Hardening:

  • Added some cleaning logic on the Dataset model to enforce publishing (final status and/or public) immutability on save, preventing reversion (we may revisit this, I was just noticing as I was building out the logic for federation signals that unpublishing a public dataset would have strange effects on the federation flow. This should prevent/warn if an admin tries to manually unpublish on a peer site for example.)

Logic Updates:

  • When dataset is published, the signal checks that it is not deleted, if deleted (before signal) AND an active doc doesn't currently exist, skip indexing/notify if so to prevent stale/deleted pre-bootstrap data from being indexed.
  • If the signal is for soft deletion, it checks that the federated doc hasn't been marked as deleted previously and if not, updates and notifies. (this is for BOTH captures and datasets)
  • When datasets are indexed/updated, so are their attached (not deleted) captures.
  • When a capture is updated (say, when metadata is re-indexed) the signal will also check if it is in a public dataset and index/update/notify accordingly.

…(TODO: implement on federation app side), federation init/checks refactor, federation queryset reusability
Comment thread gateway/sds_gateway/api_methods/federation/signals.py Outdated
Comment thread gateway/sds_gateway/api_methods/federation/signals.py Outdated
Comment thread gateway/sds_gateway/api_methods/federation/signals.py Outdated
Comment thread gateway/config/settings/base.py Outdated
Comment thread gateway/sds_gateway/api_methods/federation/signals.py Outdated
Comment thread gateway/sds_gateway/api_methods/federation/signals.py Outdated


def capture_in_published_dataset(capture: Capture) -> bool:
return capture.datasets.federation_exportable().exists()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FK-linked captures skip federation

Medium Severity

Federation eligibility for captures uses only the datasets M2M (capture.datasets.federation_exportable() and datasets__in on list queries), while the gateway still supports legacy Capture.dataset FK via get_capture_datasets. Captures tied to a published dataset only through the FK are omitted from indexing, signals, and export lists.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ee4b2ca. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check if this is still relevant, but I can use the bridge function.

…, make sure serializers have fields to pre-filter fed searches on gateway
)
if body is None:
raise NotFound()
return Response(body)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bootstrap list detail mismatch

Medium Severity

Federation export list routes build each row from live Postgres via compile_federated_*_doc, while detail routes read only fed-* OpenSearch documents. Existing public datasets and captures are indexed only on post_save when federation is operational, so enabling federation or recovering without a backfill leaves list responses populated while per-UUID detail calls return 404.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08158b5. Configure here.

item_type=item_type,
uuid=instance.uuid,
timestamp=timestamp,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redis notify skipped after index

Medium Severity

reindex_federated_asset writes to OpenSearch first, then calls publish_federation_event. If Redis publish raises, events.py logs a warning and swallows the error, so the local fed-* document can be current while the sync service never receives a notification and remote peers stay stale until another save.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08158b5. Configure here.

)
user = get_object_or_404(User, email=email, is_approved=True)
raw_key = self.mint_user_api_key(user)
return Response({"api_key": raw_key, "email": user.email})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Federation mint allows arbitrary users

Medium Severity

The federation sync mint endpoint reuses the SVI pattern: any approved email query parameter is passed to mint_federation_sync_api_key, which always creates FederationSync-scoped API keys. A holder of the sync DRF token can mint export keys for arbitrary users instead of only the federation sync service account.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08158b5. Configure here.

source = response.get("_source")
if not isinstance(source, dict):
return None
return source

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenSearch errors skip tombstone reindex

Medium Severity

The get_federated_export_doc_by_uuid function returns None for all OpenSearch exceptions, not just NotFoundError. This causes downstream logic, like fed_doc_exists and reindex functions, to incorrectly assume a document is missing. As a result, critical cleanup, such as tombstone reindexing for soft-deleted assets, is skipped, leaving stale documents in fed-* indices.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4609c03. Configure here.

client = get_redis_client()
client.publish(channel, json.dumps(payload))
except Exception as err: # noqa: BLE001
log.warning("Failed to publish federation event: {}", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redis publish errors swallowed

Medium Severity

After a successful fed index, publish_federation_event catches all Redis errors, logs a warning, and returns. The local index can be up to date while the sync service never receives the change notification.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4609c03. Configure here.

exc,
)
return None
source = response.get("_source")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to use f-strings with loguru

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 8 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 94ef4d5. Configure here.

_last_evaluated_at = now
settings.FEDERATION_OPERATIONAL = operational
settings.FEDERATION_OPERATIONAL_REASON = reason
return operational, reason

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operational state cached sixty seconds

High Severity

The refresh_federation_operational_state function caches federation status for 60 seconds. This prevents immediate recognition of federation becoming operational (e.g., after the FederationSync API key is created or health checks pass). As a result, post_save handlers and export endpoints may incorrectly treat federation as down for up to a minute, leading to skipped indexing, missed Redis events, and 503 errors.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 94ef4d5. Configure here.

return
for capture in fresh.captures.filter(is_deleted=False):
if capture_needs_federation_reindex(capture):
reindex_federated_asset(capture, ItemType.CAPTURE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FK-linked captures skipped federation

Medium Severity

The federation logic for captures currently only considers the Capture.datasets M2M relationship. During the expand-contract migration, captures linked solely by the deprecated Capture.dataset FK are missed. This prevents them from being federated, indexed, or reindexed, impacting functions like public_captures_queryset and capture_in_published_dataset.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 94ef4d5. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request federation Federation related work (sync service, document storage, peer configuration, etc.) gateway Gateway component migrations Code changes that require data or schema migrations in the database.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants