-
Notifications
You must be signed in to change notification settings - Fork 0
Scaffolding: Federation gateway setup #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
klpoland
wants to merge
7
commits into
master
Choose a base branch
from
feature-kpoland-federation-gateway-scaffolding
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
78b03aa
gateway: federation export API, Redis events, sync API key
klpoland 7b6f1e0
federation config hardening
klpoland 9e3fc32
pre-commit fixes
klpoland 3b3ea8f
arg to kwarg
klpoland 4bbcbed
fix test errors
klpoland 2c09fae
clean up env variable settings, ip parsing, auth classes
klpoland 3393c0f
gateway mirror for health check
klpoland File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
168 changes: 168 additions & 0 deletions
168
gateway/sds_gateway/api_methods/federation/availability.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| """Federation operational status: config, sync health, Redis, sync API key.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import ipaddress | ||
| import json | ||
| import time | ||
| import urllib.error | ||
| import urllib.request | ||
| from typing import Any | ||
|
|
||
| from django.conf import settings | ||
| from loguru import logger as log | ||
|
|
||
| from sds_gateway.api_methods.models import KeySources | ||
| from sds_gateway.api_methods.tasks import get_redis_client | ||
| from sds_gateway.users.models import UserAPIKey | ||
|
|
||
| _HTTP_OK = 200 | ||
| _RECHECK_INTERVAL_SECONDS = 60.0 | ||
| _last_evaluated_at: float = 0.0 | ||
| _cached_operational: bool = False | ||
| _cached_reason: str = "not evaluated" | ||
|
|
||
|
|
||
| def _setting(name: str, *, default: Any = None) -> Any: | ||
| return getattr(settings, name, default) | ||
|
|
||
|
|
||
| def _export_allowed_networks() -> list[ipaddress.IPv4Network | ipaddress.IPv6Network]: | ||
| """Networks from settings (parsed at startup); tests may override with strings.""" | ||
| raw = _setting("FEDERATION_EXPORT_ALLOWED_CIDRS", default=[]) | ||
| networks: list[ipaddress.IPv4Network | ipaddress.IPv6Network] = [] | ||
| for item in raw: | ||
| if isinstance(item, (ipaddress.IPv4Network, ipaddress.IPv6Network)): | ||
| networks.append(item) | ||
| else: | ||
| networks.append(ipaddress.ip_network(str(item).strip(), strict=False)) | ||
| return networks | ||
|
|
||
|
|
||
| def federation_client_ip(request) -> str | None: | ||
| """Client IP for export access control (direct internal connections only).""" | ||
| remote = request.META.get("REMOTE_ADDR") | ||
| if remote: | ||
| return str(remote).strip() | ||
| return None | ||
|
|
||
|
|
||
| def is_client_ip_allowed_for_federation_export(request) -> bool: | ||
| cidrs = _export_allowed_networks() | ||
| if not cidrs: | ||
| return False | ||
| client_ip = federation_client_ip(request) | ||
| if not client_ip: | ||
| return False | ||
| try: | ||
| addr = ipaddress.ip_address(client_ip) | ||
| except ValueError: | ||
| return False | ||
| return any(addr in network for network in cidrs) | ||
|
|
||
|
|
||
| def _sync_health_ok() -> tuple[bool, str]: # noqa: PLR0911 | ||
| if _setting("FEDERATION_SKIP_SYNC_HEALTH_PROBE", default=False): | ||
| return True, "health probe skipped" | ||
| url = (_setting("FEDERATION_SYNC_HEALTH_URL") or "").strip() | ||
| if not url: | ||
| return False, "FEDERATION_SYNC_HEALTH_URL is not set" | ||
| if not url.startswith(("http://", "https://")): | ||
| return False, "FEDERATION_SYNC_HEALTH_URL must be http(s)" | ||
| timeout = float( | ||
| _setting("FEDERATION_SYNC_HEALTH_PROBE_TIMEOUT", default=2.0), | ||
| ) | ||
| request = urllib.request.Request(url, method="GET") # noqa: S310 | ||
| try: | ||
| with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 | ||
| if response.status != _HTTP_OK: | ||
| return False, f"sync health returned HTTP {response.status}" | ||
| body = response.read().decode("utf-8", errors="replace") | ||
| except urllib.error.URLError as exc: | ||
| return False, f"sync health probe failed: {exc.reason}" | ||
| except TimeoutError: | ||
| return False, "sync health probe timed out" | ||
| if body: | ||
| try: | ||
| payload = json.loads(body) | ||
| if isinstance(payload, dict) and payload.get("status") == "ok": | ||
| return True, "sync health ok" | ||
| except json.JSONDecodeError: | ||
| pass | ||
| return True, "sync health returned 200" | ||
|
klpoland marked this conversation as resolved.
|
||
| return True, "sync health returned 200" | ||
|
|
||
|
|
||
| def _sync_api_key_present() -> tuple[bool, str]: | ||
| if _setting("FEDERATION_SKIP_SYNC_API_KEY_CHECK", default=False): | ||
| return True, "sync API key check skipped" | ||
| exists = UserAPIKey.objects.filter(source=KeySources.FederationSync).exists() | ||
| if not exists: | ||
| return False, "no FederationSync API key in database" | ||
| return True, "FederationSync API key present" | ||
|
|
||
|
|
||
| def _redis_ok() -> tuple[bool, str]: | ||
| if not _setting("FEDERATION_ENABLED", default=False): | ||
| return True, "redis not required (federation disabled)" | ||
| if _setting("FEDERATION_SKIP_REDIS_PROBE", default=False): | ||
| return True, "redis probe skipped" | ||
|
|
||
| try: | ||
| client = get_redis_client() | ||
| client.ping() | ||
| except Exception as exc: # noqa: BLE001 | ||
| return False, f"redis ping failed: {exc}" | ||
| return True, "redis ok" | ||
|
|
||
|
|
||
| def evaluate_federation_operational() -> tuple[bool, str]: | ||
| if not _setting("FEDERATION_ENABLED", default=False): | ||
| return False, "FEDERATION_ENABLED is False" | ||
|
|
||
| site_name = (_setting("FEDERATION_SITE_NAME", default="") or "").strip() | ||
| if not site_name: | ||
| return False, "FEDERATION_SITE_NAME must be set when federation is enabled" | ||
|
|
||
| for check in (_sync_api_key_present, _sync_health_ok, _redis_ok): | ||
| ok, reason = check() | ||
| if not ok: | ||
| return False, reason | ||
| return True, "federation operational" | ||
|
|
||
|
|
||
| def refresh_federation_operational_state(*, force: bool = False) -> tuple[bool, str]: | ||
| global _cached_operational, _cached_reason, _last_evaluated_at # noqa: PLW0603 | ||
|
|
||
| now = time.monotonic() | ||
| if ( | ||
| not force | ||
| and _last_evaluated_at | ||
| and (now - _last_evaluated_at) < _RECHECK_INTERVAL_SECONDS | ||
| ): | ||
| return _cached_operational, _cached_reason | ||
|
|
||
| operational, reason = evaluate_federation_operational() | ||
| _cached_operational = operational | ||
| _cached_reason = reason | ||
| _last_evaluated_at = now | ||
| settings.FEDERATION_OPERATIONAL = operational | ||
| settings.FEDERATION_OPERATIONAL_REASON = reason | ||
| return operational, reason | ||
|
|
||
|
|
||
| def initialize_federation_operational_state() -> None: | ||
| operational, reason = refresh_federation_operational_state(force=True) | ||
| if operational: | ||
| log.info("Federation is operational: {}", reason) | ||
| else: | ||
| log.warning("Federation disabled: {}", reason) | ||
|
|
||
|
|
||
| def is_federation_operational() -> bool: | ||
| if _setting("FEDERATION_OPERATIONAL_OVERRIDE", default=None) is not None: | ||
| return bool(_setting("FEDERATION_OPERATIONAL_OVERRIDE")) | ||
| if not _setting("FEDERATION_ENABLED", default=False): | ||
| return False | ||
| operational, _reason = refresh_federation_operational_state() | ||
| return operational | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.