clue-api 1.7.0.dev464__tar.gz → 1.7.0.dev465__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/PKG-INFO +1 -1
  2. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/auth.py +6 -5
  3. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/config.py +73 -0
  4. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/pyproject.toml +1 -1
  5. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/LICENSE +0 -0
  6. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/README.md +0 -0
  7. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/.gitignore +0 -0
  8. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/__init__.py +0 -0
  9. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/__init__.py +0 -0
  10. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/base.py +0 -0
  11. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/documentation.py +0 -0
  12. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/__init__.py +0 -0
  13. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/actions.py +0 -0
  14. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/configs.py +0 -0
  15. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/fetchers.py +0 -0
  16. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/lookup.py +0 -0
  17. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/registration.py +0 -0
  18. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/static.py +0 -0
  19. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v1/sync.py +0 -0
  20. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v2/__init__.py +0 -0
  21. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/api/v2/lookup.py +0 -0
  22. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/app.py +0 -0
  23. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/cache/__init__.py +0 -0
  24. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/__init__.py +0 -0
  25. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/bytes_utils.py +0 -0
  26. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/classification.py +0 -0
  27. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/classification.yml +0 -0
  28. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/dict_utils.py +0 -0
  29. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/exceptions.py +0 -0
  30. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/forge.py +0 -0
  31. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/json_utils.py +0 -0
  32. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/list_utils.py +0 -0
  33. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/logging/__init__.py +0 -0
  34. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/logging/audit.py +0 -0
  35. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/logging/format.py +0 -0
  36. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/regex.py +0 -0
  37. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/str_utils.py +0 -0
  38. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/swagger.py +0 -0
  39. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/common/uid.py +0 -0
  40. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/config.py +0 -0
  41. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/constants/__init__.py +0 -0
  42. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/constants/env.py +0 -0
  43. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/constants/supported_types.py +0 -0
  44. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/cronjobs/__init__.py +0 -0
  45. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/cronjobs/plugins.py +0 -0
  46. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/error.py +0 -0
  47. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/extensions/__init__.py +0 -0
  48. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/extensions/config.py +0 -0
  49. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/gunicorn_config.py +0 -0
  50. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/healthz.py +0 -0
  51. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/discover.py +0 -0
  52. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/headers.py +0 -0
  53. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/oauth.py +0 -0
  54. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/obo.py +0 -0
  55. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/helper/plugin_requests.py +0 -0
  56. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/__init__.py +0 -0
  57. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/actions.py +0 -0
  58. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/auth_user.py +0 -0
  59. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/fetchers.py +0 -0
  60. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/graph.py +0 -0
  61. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/jwks.py +0 -0
  62. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/model_list.py +0 -0
  63. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/network.py +0 -0
  64. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/__init__.py +0 -0
  65. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/base.py +0 -0
  66. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/file.py +0 -0
  67. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/graph.py +0 -0
  68. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/image.py +0 -0
  69. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/status.py +0 -0
  70. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/results/validation.py +0 -0
  71. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/schema.py +0 -0
  72. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/selector.py +0 -0
  73. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/sync.py +0 -0
  74. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/models/validators.py +0 -0
  75. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/patched.py +0 -0
  76. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/__init__.py +0 -0
  77. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/celery_app.py +0 -0
  78. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/__init__.py +0 -0
  79. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/central_server.py +0 -0
  80. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/email_render.py +0 -0
  81. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/token.py +0 -0
  82. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/helpers/trino.py +0 -0
  83. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/models.py +0 -0
  84. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/plugin/utils.py +0 -0
  85. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/py.typed +0 -0
  86. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/__init__.py +0 -0
  87. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/__init__.py +0 -0
  88. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/cache.py +0 -0
  89. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/events.py +0 -0
  90. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/hash.py +0 -0
  91. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/queues/__init__.py +0 -0
  92. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/queues/comms.py +0 -0
  93. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/set.py +0 -0
  94. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  95. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/security/__init__.py +0 -0
  96. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/security/obo.py +0 -0
  97. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/security/utils.py +0 -0
  98. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/action_service.py +0 -0
  99. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/auth_service.py +0 -0
  100. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/config_service.py +0 -0
  101. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/fetcher_service.py +0 -0
  102. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/jwt_service.py +0 -0
  103. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/lookup_service.py +0 -0
  104. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/mongo_service.py +0 -0
  105. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/type_service.py +0 -0
  106. {clue_api-1.7.0.dev464 → clue_api-1.7.0.dev465}/clue/services/user_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clue-api
3
- Version: 1.7.0.dev464
3
+ Version: 1.7.0.dev465
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,5 +1,5 @@
1
1
  from typing import Any, Optional
2
- from urllib.parse import urlparse
2
+ from urllib.parse import urlencode
3
3
 
4
4
  from authlib.integrations.base_client import OAuthError
5
5
  from flask import current_app, request
@@ -121,10 +121,11 @@ def login(**_) -> dict[str, Any]: # noqa: C901
121
121
 
122
122
  # This means that they want to start the oauth process, so we'll redirect them to their chosen provider
123
123
  if "code" not in request.args and not refresh_token:
124
- referer = request.headers.get("Referer", None)
125
- uri = urlparse(referer if referer else request.host_url)
126
- port_portion = ":" + str(uri.port) if uri.port else ""
127
- redirect_uri = f"{uri.scheme}://{uri.hostname}{port_portion}/login?provider={oauth_provider}"
124
+ # The redirect URI must only come from trusted configuration, never from request headers
125
+ if not config.api.frontend_url:
126
+ raise ClueValueError("api.frontend_url not set! Cannot start OAuth")
127
+
128
+ redirect_uri = f"{config.api.frontend_url.rstrip('/')}/login?{urlencode({'provider': oauth_provider})}"
128
129
  return provider.authorize_redirect(redirect_uri=redirect_uri, nonce=request.args.get("nonce", None))
129
130
 
130
131
  # At this point we know the code exists, so we're good to use that to exchange for an JSON Web Token with
@@ -543,6 +543,10 @@ class API(BaseModel):
543
543
  audit: bool = Field(description="Should API calls be audited and saved to a separate log file?", default=True)
544
544
  debug: bool = Field(description="Enable debugging?", default=False)
545
545
  discover_url: str | None = Field(description="Discover URL", default=None)
546
+ frontend_url: str | None = Field(
547
+ description="Trusted Clue UI origin used to build the OAuth redirect URI. Required when OAuth is enabled.",
548
+ default=None,
549
+ )
546
550
  external_sources: list[ExternalSource] = Field(description="List of external sources to query", default=[])
547
551
  registration_allowed_origins: list[str] = Field(
548
552
  description="Exact URL origins permitted for runtime external source registration", default=[]
@@ -562,6 +566,67 @@ class API(BaseModel):
562
566
  description="Validate if the XSRF token matches the randomly generated token for the session", default=True
563
567
  )
564
568
 
569
+ @field_validator("frontend_url")
570
+ @classmethod
571
+ def validate_frontend_url(cls, frontend_url: str | None) -> str | None: # noqa: ANN102
572
+ """Validates that frontend_url is a plain http(s) origin, and normalizes it to ``scheme://host[:port]``.
573
+
574
+ The UI is served from the root of its origin (no router basename, absolute /api and /login paths), so a
575
+ path other than an optional trailing slash is rejected.
576
+
577
+ Args:
578
+ frontend_url (str | None): The configured UI origin.
579
+
580
+ Raises:
581
+ ValueError: Raised whenever the URL is not a safe, unambiguous http(s) origin.
582
+
583
+ Returns:
584
+ str | None: The validated origin, without a trailing slash.
585
+ """
586
+ if frontend_url is None:
587
+ return None
588
+
589
+ error = (
590
+ "frontend_url must be an http(s) origin such as https://clue.example[:port] (an optional trailing slash "
591
+ "is allowed) with a valid ASCII host, and no credentials, path, query, fragment or percent-encoding"
592
+ )
593
+
594
+ # urlsplit silently drops tabs/newlines, so check the raw string first
595
+ if any(not char.isprintable() or char.isspace() or ord(char) > 126 or char in "\\%" for char in frontend_url):
596
+ raise ValueError(error)
597
+
598
+ try:
599
+ parsed = urlsplit(frontend_url)
600
+ port = parsed.port
601
+ host = parsed.hostname
602
+ except ValueError as err:
603
+ raise ValueError(error) from err
604
+
605
+ if (
606
+ parsed.scheme not in {"http", "https"}
607
+ or not host
608
+ or "@" in parsed.netloc
609
+ or parsed.netloc.endswith(":")
610
+ or (port is not None and port < 1)
611
+ or parsed.path not in {"", "/"}
612
+ or parsed.query
613
+ or parsed.fragment
614
+ or "?" in frontend_url
615
+ or "#" in frontend_url
616
+ ):
617
+ raise ValueError(error)
618
+
619
+ TypeAdapter(HttpUrl).validate_python(frontend_url)
620
+
621
+ # Prevent Pydantic from accepting invalid domains such as clue..com
622
+ if ":" not in host:
623
+ if any(char not in "abcdefghijklmnopqrstuvwxyz0123456789_-." for char in host) or any(
624
+ not label or label.startswith("-") or label.endswith("-") for label in host.split(".")
625
+ ):
626
+ raise ValueError(error)
627
+
628
+ return f"{parsed.scheme}://{parsed.netloc}"
629
+
565
630
 
566
631
  class Retention(BaseModel):
567
632
  enabled: Annotated[bool, Field(description="Should records be cached for users?")] = True
@@ -622,6 +687,14 @@ class Config(BaseSettings):
622
687
  env_nested_delimiter="__",
623
688
  )
624
689
 
690
+ @model_validator(mode="after")
691
+ def validate_oauth_frontend_url(self: Self) -> Self:
692
+ """The OAuth redirect URI is built from api.frontend_url, so it must be set when OAuth is enabled."""
693
+ if self.auth.oauth.enabled and not self.api.frontend_url:
694
+ raise ValueError("api.frontend_url must be set when auth.oauth.enabled is true")
695
+
696
+ return self
697
+
625
698
  @classmethod
626
699
  def settings_customise_sources(
627
700
  cls, # noqa: ANN102
@@ -141,7 +141,7 @@ log_cli_level = "WARN"
141
141
  [tool.poetry]
142
142
  package-mode = true
143
143
  name = "clue-api"
144
- version = "1.7.0.dev464"
144
+ version = "1.7.0.dev465"
145
145
  description = "Clue distributed enrichment service"
146
146
  authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
147
147
  license = "MIT"
File without changes