clue-api 1.7.0.dev465__tar.gz → 1.7.0.dev468__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 (107) hide show
  1. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/PKG-INFO +1 -1
  2. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/actions.py +9 -3
  3. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/fetchers.py +4 -0
  4. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/helper/plugin_requests.py +10 -1
  5. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/actions.py +6 -7
  6. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/fetchers.py +6 -7
  7. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/network.py +6 -0
  8. clue_api-1.7.0.dev468/clue/models/validators.py +50 -0
  9. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/services/action_service.py +20 -6
  10. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/services/fetcher_service.py +18 -4
  11. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/pyproject.toml +1 -1
  12. clue_api-1.7.0.dev465/clue/models/validators.py +0 -19
  13. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/LICENSE +0 -0
  14. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/README.md +0 -0
  15. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/.gitignore +0 -0
  16. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/__init__.py +0 -0
  17. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/__init__.py +0 -0
  18. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/base.py +0 -0
  19. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/documentation.py +0 -0
  20. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/__init__.py +0 -0
  21. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/auth.py +0 -0
  22. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/configs.py +0 -0
  23. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/lookup.py +0 -0
  24. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/registration.py +0 -0
  25. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/static.py +0 -0
  26. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v1/sync.py +0 -0
  27. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v2/__init__.py +0 -0
  28. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/api/v2/lookup.py +0 -0
  29. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/app.py +0 -0
  30. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/cache/__init__.py +0 -0
  31. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/__init__.py +0 -0
  32. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/bytes_utils.py +0 -0
  33. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/classification.py +0 -0
  34. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/classification.yml +0 -0
  35. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/dict_utils.py +0 -0
  36. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/exceptions.py +0 -0
  37. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/forge.py +0 -0
  38. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/json_utils.py +0 -0
  39. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/list_utils.py +0 -0
  40. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/logging/__init__.py +0 -0
  41. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/logging/audit.py +0 -0
  42. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/logging/format.py +0 -0
  43. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/regex.py +0 -0
  44. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/str_utils.py +0 -0
  45. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/swagger.py +0 -0
  46. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/common/uid.py +0 -0
  47. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/config.py +0 -0
  48. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/constants/__init__.py +0 -0
  49. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/constants/env.py +0 -0
  50. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/constants/supported_types.py +0 -0
  51. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/cronjobs/__init__.py +0 -0
  52. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/cronjobs/plugins.py +0 -0
  53. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/error.py +0 -0
  54. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/extensions/__init__.py +0 -0
  55. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/extensions/config.py +0 -0
  56. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/gunicorn_config.py +0 -0
  57. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/healthz.py +0 -0
  58. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/helper/discover.py +0 -0
  59. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/helper/headers.py +0 -0
  60. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/helper/oauth.py +0 -0
  61. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/helper/obo.py +0 -0
  62. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/__init__.py +0 -0
  63. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/auth_user.py +0 -0
  64. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/config.py +0 -0
  65. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/graph.py +0 -0
  66. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/jwks.py +0 -0
  67. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/model_list.py +0 -0
  68. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/results/__init__.py +0 -0
  69. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/results/base.py +0 -0
  70. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/results/file.py +0 -0
  71. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/results/graph.py +0 -0
  72. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/results/image.py +0 -0
  73. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/results/status.py +0 -0
  74. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/results/validation.py +0 -0
  75. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/schema.py +0 -0
  76. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/selector.py +0 -0
  77. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/models/sync.py +0 -0
  78. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/patched.py +0 -0
  79. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/__init__.py +0 -0
  80. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/celery_app.py +0 -0
  81. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/helpers/__init__.py +0 -0
  82. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/helpers/central_server.py +0 -0
  83. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/helpers/email_render.py +0 -0
  84. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/helpers/token.py +0 -0
  85. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/helpers/trino.py +0 -0
  86. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/models.py +0 -0
  87. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/plugin/utils.py +0 -0
  88. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/py.typed +0 -0
  89. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/__init__.py +0 -0
  90. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/datatypes/__init__.py +0 -0
  91. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/datatypes/cache.py +0 -0
  92. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/datatypes/events.py +0 -0
  93. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/datatypes/hash.py +0 -0
  94. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/datatypes/queues/__init__.py +0 -0
  95. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/datatypes/queues/comms.py +0 -0
  96. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/datatypes/set.py +0 -0
  97. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  98. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/security/__init__.py +0 -0
  99. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/security/obo.py +0 -0
  100. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/security/utils.py +0 -0
  101. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/services/auth_service.py +0 -0
  102. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/services/config_service.py +0 -0
  103. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/services/jwt_service.py +0 -0
  104. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/services/lookup_service.py +0 -0
  105. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/services/mongo_service.py +0 -0
  106. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/clue/services/type_service.py +0 -0
  107. {clue_api-1.7.0.dev465 → clue_api-1.7.0.dev468}/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.dev465
3
+ Version: 1.7.0.dev468
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -8,12 +8,13 @@ List and execute actions
8
8
 
9
9
  from flask_cors import CORS
10
10
 
11
- from clue.api import internal_error, make_subapi_blueprint, not_found, ok, service_unavailable
11
+ from clue.api import bad_request, internal_error, make_subapi_blueprint, not_found, ok, service_unavailable
12
12
  from clue.common.exceptions import ClueException, NotFoundException
13
13
  from clue.common.logging import get_logger
14
14
  from clue.common.swagger import generate_swagger_docs
15
15
  from clue.config import config
16
16
  from clue.models.actions import Action, ActionResult
17
+ from clue.models.validators import validate_plugin_identifier, validate_task_id
17
18
  from clue.security import api_login
18
19
  from clue.services import action_service
19
20
 
@@ -85,10 +86,13 @@ def execute_action(plugin_id: str, action_id: str, **kwargs) -> ActionResult:
85
86
  }
86
87
  """
87
88
  try:
89
+ validate_plugin_identifier(action_id)
88
90
  return ok(action_service.execute_action(plugin_id, action_id, kwargs["user"]))
89
91
  except NotFoundException as err:
90
92
  return not_found(err=err.message)
91
93
  except ClueException as err:
94
+ if err.status_code == 400:
95
+ return bad_request(err=err.message)
92
96
  return internal_error(err=err.message)
93
97
 
94
98
 
@@ -116,12 +120,14 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, **kwargs) ->
116
120
  }
117
121
  """
118
122
  try:
119
- if not task_id:
120
- return internal_error(err="no task_id found in url. task_id is required for this request.")
123
+ validate_plugin_identifier(action_id)
124
+ validate_task_id(task_id)
121
125
  return ok(action_service.get_action_status(plugin_id, action_id, task_id, kwargs["user"]))
122
126
  except NotFoundException as err:
123
127
  return not_found(err=err.message)
124
128
  except ClueException as err:
129
+ if err.status_code == 400:
130
+ return bad_request(err=err.message)
125
131
  if err.status_code == 503:
126
132
  return service_unavailable(err=err.message)
127
133
  return internal_error(err=err.message)
@@ -14,6 +14,7 @@ from clue.common.logging import get_logger
14
14
  from clue.common.swagger import generate_swagger_docs
15
15
  from clue.config import config
16
16
  from clue.models.fetchers import FetcherDefinition
17
+ from clue.models.validators import validate_plugin_identifier, validate_task_id
17
18
  from clue.security import api_login
18
19
  from clue.services import fetcher_service
19
20
 
@@ -82,6 +83,7 @@ def run_fetcher(plugin_id: str, fetcher_id: str, **kwargs):
82
83
  }
83
84
  """
84
85
  try:
86
+ validate_plugin_identifier(fetcher_id)
85
87
  return ok(fetcher_service.run_fetcher(plugin_id, fetcher_id, kwargs["user"]))
86
88
  except NotFoundException as err:
87
89
  return not_found(err=err.message)
@@ -116,6 +118,8 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, **kwargs):
116
118
  }
117
119
  """
118
120
  try:
121
+ validate_plugin_identifier(fetcher_id)
122
+ validate_task_id(task_id)
119
123
  return ok(fetcher_service.get_fetcher_status(plugin_id, fetcher_id, task_id, kwargs["user"]))
120
124
  except NotFoundException as err:
121
125
  return not_found(err=err.message)
@@ -1,14 +1,23 @@
1
1
  from collections.abc import Callable
2
2
  from time import monotonic
3
3
  from typing import Any, TypeAlias
4
- from urllib.parse import urljoin, urlsplit
4
+ from urllib.parse import quote, urljoin, urlsplit
5
5
 
6
6
  from requests import Response
7
7
  from requests.exceptions import ConnectionError, Timeout
8
8
 
9
+ from clue.common.exceptions import NotFoundException
10
+
9
11
  _Timeout: TypeAlias = int | float | tuple[int | float | None, int | float | None] | None
10
12
 
11
13
 
14
+ def quote_plugin_path_segment(value: str) -> str:
15
+ """Encode an opaque plugin identifier, rejecting client-normalized dot segments."""
16
+ if value in {"", ".", ".."}:
17
+ raise NotFoundException("Invalid plugin identifier.", status_code=404)
18
+ return quote(value, safe="").replace(".", "%2E")
19
+
20
+
12
21
  def _timeout_budget(timeout: _Timeout) -> float | None:
13
22
  """Calculate the total duration represented by a requests timeout.
14
23
 
@@ -1,5 +1,4 @@
1
1
  # ruff: noqa: D101
2
- import re
3
2
  from inspect import isclass
4
3
  from typing import (
5
4
  Any,
@@ -29,7 +28,7 @@ from clue.constants.supported_types import SUPPORTED_TYPES
29
28
  from clue.models.results import DATA
30
29
  from clue.models.results.validation import validate_result
31
30
  from clue.models.selector import Selector
32
- from clue.models.validators import validate_classification
31
+ from clue.models.validators import validate_classification, validate_plugin_identifier
33
32
 
34
33
  logger = get_logger(__file__)
35
34
 
@@ -123,7 +122,10 @@ ER = TypeVar("ER", bound=ExecuteRequest)
123
122
 
124
123
 
125
124
  class ActionBase(BaseModel):
126
- id: str = Field(description="Unique identifier for the action.")
125
+ id: str = Field(
126
+ description="Unique action identifier with optional dot-separated components using ASCII letters, digits, "
127
+ "underscores, or hyphens."
128
+ )
127
129
  name: str = Field(description="Name of the action.")
128
130
  classification: str = Field(
129
131
  description="Classification of the action. Denotes the maximum classification of data sent to the action.",
@@ -162,10 +164,7 @@ class ActionBase(BaseModel):
162
164
  Returns:
163
165
  str: The validated ID.
164
166
  """
165
- if re.match(r"[^a-z_]", action_id):
166
- raise ClueValueError("Invalid action id - can only contain lowercase letters and underscores.")
167
-
168
- return action_id
167
+ return validate_plugin_identifier(action_id)
169
168
 
170
169
  @field_validator("classification")
171
170
  @classmethod
@@ -1,5 +1,4 @@
1
1
  # ruff: noqa: D101
2
- import re
3
2
  from typing import Dict, Generic, Literal, Optional, Self
4
3
 
5
4
  from pydantic import (
@@ -17,13 +16,16 @@ from clue.common.logging import get_logger
17
16
  from clue.constants.supported_types import SUPPORTED_TYPES
18
17
  from clue.models.results import DATA, FORMAT_MAPPINGS_REVERSE
19
18
  from clue.models.results.validation import validate_result
20
- from clue.models.validators import validate_classification
19
+ from clue.models.validators import validate_classification, validate_plugin_identifier
21
20
 
22
21
  logger = get_logger(__file__)
23
22
 
24
23
 
25
24
  class FetcherDefinition(BaseModel):
26
- id: str = Field(description="An ID for the given fetcher. Structured as <plugin_id>.<fetcher_id>.")
25
+ id: str = Field(
26
+ description="Fetcher identifier, optionally plugin_id.fetcher_id; components use ASCII letters, digits, "
27
+ "underscores, or hyphens."
28
+ )
27
29
  classification: str = Field(
28
30
  description="Classification of the fetcher. Denotes the maximum classification of data sent to the fetcher.",
29
31
  )
@@ -49,10 +51,7 @@ class FetcherDefinition(BaseModel):
49
51
  Returns:
50
52
  str: The validated ID.
51
53
  """
52
- if re.match(r"[^a-z_]", fetcher_id):
53
- raise ClueValueError("Invalid fetcher id - can only contain lowercase letters and underscores.")
54
-
55
- return fetcher_id
54
+ return validate_plugin_identifier(fetcher_id)
56
55
 
57
56
  @field_validator("classification")
58
57
  @classmethod
@@ -529,6 +529,9 @@ class QueryResult(ResultMetadata):
529
529
  Returns:
530
530
  list[QueryEntry]: The validated items field.
531
531
  """
532
+ if not CLASSIFICATION.enforce:
533
+ return items
534
+
532
535
  if not isinstance(info.context, dict) or "user" not in info.context:
533
536
  logger.warning("No user context given, dropping all query items")
534
537
  return []
@@ -538,6 +541,9 @@ class QueryResult(ResultMetadata):
538
541
  @classmethod
539
542
  def filter_items(cls, items: list[QueryEntry], user: Any) -> list[QueryEntry]: # noqa: ANN102
540
543
  """Apply classification authorization independently of response schema validation."""
544
+ if not CLASSIFICATION.enforce:
545
+ return items
546
+
541
547
  user_classification = user.get("classification") if isinstance(user, dict) else None
542
548
  if (
543
549
  not isinstance(user_classification, str)
@@ -0,0 +1,50 @@
1
+ import re
2
+
3
+ from clue.common.exceptions import ClueValueError
4
+ from clue.config import CLASSIFICATION
5
+
6
+
7
+ def validate_plugin_identifier(value: str) -> str:
8
+ """Allow safe ASCII components separated by dots, never dot segments."""
9
+ if re.fullmatch(r"[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*", value) is None:
10
+ raise ClueValueError("Invalid plugin identifier.", status_code=400)
11
+ return value
12
+
13
+
14
+ MAX_TASK_ID_LENGTH = 256
15
+
16
+
17
+ def validate_task_id(value: str) -> str:
18
+ """Accept any opaque plugin-issued task id that stays a single path segment once percent-encoded.
19
+
20
+ Task ids are minted by plugins (Celery UUIDs by default, but legacy/third-party backends may use other
21
+ formats), so only characters that cannot be safely encoded or routed are rejected. Callers must still encode
22
+ the value with ``quote_plugin_path_segment`` before building an upstream URL.
23
+ """
24
+ if (
25
+ not value
26
+ or len(value) > MAX_TASK_ID_LENGTH
27
+ or value in {".", ".."}
28
+ or "/" in value
29
+ or re.search(r"[\x00-\x1f\x7f]", value)
30
+ ):
31
+ raise ClueValueError("Invalid task id.", status_code=400)
32
+ return value
33
+
34
+
35
+ def validate_classification(classification: str):
36
+ """Validates the provided classification.
37
+
38
+ Args:
39
+ classification (str): The classification to validate.
40
+
41
+ Raises:
42
+ AssertionError: Raised whenever the provided classification is not valid.
43
+
44
+ Returns:
45
+ str: The validated classification.
46
+ """
47
+ if not CLASSIFICATION.is_valid(classification):
48
+ raise AssertionError(f"{classification} is not a valid classification.")
49
+
50
+ return classification
@@ -5,17 +5,18 @@ from urllib.parse import urljoin
5
5
  import requests
6
6
  from elasticapm.traces import capture_span
7
7
  from flask import request
8
- from pydantic import TypeAdapter
8
+ from pydantic import TypeAdapter, ValidationError
9
9
  from requests import JSONDecodeError, exceptions
10
10
 
11
- from clue.common.exceptions import AuthenticationException, ClueException, NotFoundException
11
+ from clue.common.exceptions import AuthenticationException, ClueException, ClueValueError, NotFoundException
12
12
  from clue.common.logging import get_logger
13
13
  from clue.config import CLASSIFICATION, config
14
14
  from clue.helper.headers import generate_headers
15
15
  from clue.helper.obo import get_obo_access_token
16
- from clue.helper.plugin_requests import request_with_safe_redirects
16
+ from clue.helper.plugin_requests import quote_plugin_path_segment, request_with_safe_redirects
17
17
  from clue.models.actions import ActionResult, ActionSpec
18
18
  from clue.models.config import ExternalSource
19
+ from clue.models.validators import validate_plugin_identifier, validate_task_id
19
20
 
20
21
  logger = get_logger(__file__)
21
22
 
@@ -60,7 +61,15 @@ def get_supported_actions(
60
61
  _raise_action_metadata_unavailable()
61
62
  return {}
62
63
 
63
- return TypeAdapter(dict[str, ActionSpec]).validate_python(result["api_response"])
64
+ actions = TypeAdapter(dict[str, ActionSpec]).validate_python(result["api_response"])
65
+ for identifier in actions:
66
+ validate_plugin_identifier(identifier)
67
+ return actions
68
+ except (ClueValueError, ValidationError) as err:
69
+ logger.warning("Invalid action metadata from %s", source.name)
70
+ if raise_on_error:
71
+ _raise_action_metadata_unavailable(err)
72
+ return {}
64
73
  except ClueException:
65
74
  raise
66
75
  except Exception as err:
@@ -141,6 +150,7 @@ def execute_action(plugin_id: str, action_id: str, user: dict[str, Any]) -> Acti
141
150
  Returns:
142
151
  ActionResult: The result of the action.
143
152
  """
153
+ validate_plugin_identifier(action_id)
144
154
  plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
145
155
 
146
156
  if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
@@ -164,7 +174,7 @@ def execute_action(plugin_id: str, action_id: str, user: dict[str, Any]) -> Acti
164
174
  parameters = {}
165
175
 
166
176
  try:
167
- req_url = urljoin(plugin.url, f"actions/{action_id}")
177
+ req_url = urljoin(plugin.url, f"actions/{quote_plugin_path_segment(action_id)}")
168
178
  logger.debug("Executing action %s for user %s", req_url, user["uname"])
169
179
 
170
180
  response = request_with_safe_redirects(
@@ -205,6 +215,8 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, user: dict[s
205
215
  Returns:
206
216
  ActionResult: The result of the action.
207
217
  """
218
+ validate_plugin_identifier(action_id)
219
+ validate_task_id(task_id)
208
220
  plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
209
221
 
210
222
  if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
@@ -232,7 +244,9 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, user: dict[s
232
244
  remaining_timeout = max(timeout - (monotonic() - metadata_started), 0.001)
233
245
 
234
246
  try:
235
- req_url = urljoin(plugin.url, f"actions/{action_id}/status/{task_id}")
247
+ req_url = urljoin(
248
+ plugin.url, f"actions/{quote_plugin_path_segment(action_id)}/status/{quote_plugin_path_segment(task_id)}"
249
+ )
236
250
  logger.debug("Getting status for action %s with task_id %s for user %s", req_url, task_id, user["uname"])
237
251
 
238
252
  response = request_with_safe_redirects(
@@ -19,10 +19,11 @@ from clue.common.logging import get_logger
19
19
  from clue.config import CLASSIFICATION, config
20
20
  from clue.helper.headers import generate_headers
21
21
  from clue.helper.obo import get_obo_access_token
22
- from clue.helper.plugin_requests import request_with_safe_redirects
22
+ from clue.helper.plugin_requests import quote_plugin_path_segment, request_with_safe_redirects
23
23
  from clue.models.config import ExternalSource
24
24
  from clue.models.fetchers import FetcherDefinition, FetcherResult
25
25
  from clue.models.selector import Selector
26
+ from clue.models.validators import validate_plugin_identifier, validate_task_id
26
27
 
27
28
  logger = get_logger(__file__)
28
29
 
@@ -67,7 +68,15 @@ def get_supported_fetchers(
67
68
  _raise_fetcher_metadata_unavailable()
68
69
  return {}
69
70
 
70
- return TypeAdapter(dict[str, FetcherDefinition]).validate_python(result["api_response"])
71
+ fetchers = TypeAdapter(dict[str, FetcherDefinition]).validate_python(result["api_response"])
72
+ for identifier in fetchers:
73
+ validate_plugin_identifier(identifier)
74
+ return fetchers
75
+ except (ClueValueError, ValidationError) as err:
76
+ logger.warning("Invalid fetcher metadata from %s", source.name)
77
+ if raise_on_error:
78
+ _raise_fetcher_metadata_unavailable(err)
79
+ return {}
71
80
  except ClueException:
72
81
  raise
73
82
  except Exception as err:
@@ -157,6 +166,7 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
157
166
  Returns:
158
167
  ActionResult: The result of the action.
159
168
  """
169
+ validate_plugin_identifier(fetcher_id)
160
170
  plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
161
171
 
162
172
  if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
@@ -188,7 +198,7 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
188
198
 
189
199
  response = request_with_safe_redirects(
190
200
  requests.post,
191
- urljoin(plugin.url, f"fetchers/{fetcher_id}"),
201
+ urljoin(plugin.url, f"fetchers/{quote_plugin_path_segment(fetcher_id)}"),
192
202
  get_method=requests.get,
193
203
  json=parameters,
194
204
  headers=headers,
@@ -232,6 +242,8 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict
232
242
  Returns:
233
243
  ActionResult: The result of the action.
234
244
  """
245
+ validate_plugin_identifier(fetcher_id)
246
+ validate_task_id(task_id)
235
247
  plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
236
248
 
237
249
  if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
@@ -256,7 +268,9 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict
256
268
  remaining_timeout = max(timeout - (monotonic() - metadata_started), 0.001)
257
269
 
258
270
  try:
259
- req_url = urljoin(plugin.url, f"fetchers/{fetcher_id}/status/{task_id}")
271
+ req_url = urljoin(
272
+ plugin.url, f"fetchers/{quote_plugin_path_segment(fetcher_id)}/status/{quote_plugin_path_segment(task_id)}"
273
+ )
260
274
  logger.debug("Getting status for action %s with task_id %s for user %s", req_url, task_id, user["uname"])
261
275
 
262
276
  response = request_with_safe_redirects(
@@ -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.dev465"
144
+ version = "1.7.0.dev468"
145
145
  description = "Clue distributed enrichment service"
146
146
  authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
147
147
  license = "MIT"
@@ -1,19 +0,0 @@
1
- from clue.config import CLASSIFICATION
2
-
3
-
4
- def validate_classification(classification: str):
5
- """Validates the provided classification.
6
-
7
- Args:
8
- classification (str): The classification to validate.
9
-
10
- Raises:
11
- AssertionError: Raised whenever the provided classification is not valid.
12
-
13
- Returns:
14
- str: The validated classification.
15
- """
16
- if not CLASSIFICATION.is_valid(classification):
17
- raise AssertionError(f"{classification} is not a valid classification.")
18
-
19
- return classification
File without changes