clue-api 1.7.0.dev433__tar.gz → 1.7.0.dev435__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 (102) hide show
  1. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/PKG-INFO +1 -1
  2. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/actions.py +4 -2
  3. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/fetchers.py +3 -1
  4. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/helper/headers.py +6 -7
  5. clue_api-1.7.0.dev435/clue/helper/obo.py +31 -0
  6. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/services/action_service.py +76 -73
  7. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/services/auth_service.py +6 -6
  8. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/services/fetcher_service.py +68 -77
  9. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/services/lookup_service.py +28 -36
  10. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/services/type_service.py +15 -18
  11. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/pyproject.toml +1 -1
  12. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/LICENSE +0 -0
  13. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/README.md +0 -0
  14. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/.gitignore +0 -0
  15. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/__init__.py +0 -0
  16. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/__init__.py +0 -0
  17. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/base.py +0 -0
  18. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/__init__.py +0 -0
  19. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/auth.py +0 -0
  20. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/configs.py +0 -0
  21. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/lookup.py +0 -0
  22. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/registration.py +0 -0
  23. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/static.py +0 -0
  24. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/api/v1/sync.py +0 -0
  25. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/app.py +0 -0
  26. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/cache/__init__.py +0 -0
  27. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/__init__.py +0 -0
  28. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/bytes_utils.py +0 -0
  29. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/classification.py +0 -0
  30. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/classification.yml +0 -0
  31. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/dict_utils.py +0 -0
  32. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/exceptions.py +0 -0
  33. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/forge.py +0 -0
  34. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/json_utils.py +0 -0
  35. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/list_utils.py +0 -0
  36. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/logging/__init__.py +0 -0
  37. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/logging/audit.py +0 -0
  38. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/logging/format.py +0 -0
  39. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/regex.py +0 -0
  40. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/str_utils.py +0 -0
  41. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/swagger.py +0 -0
  42. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/common/uid.py +0 -0
  43. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/config.py +0 -0
  44. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/constants/__init__.py +0 -0
  45. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/constants/env.py +0 -0
  46. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/constants/supported_types.py +0 -0
  47. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/cronjobs/__init__.py +0 -0
  48. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/cronjobs/plugins.py +0 -0
  49. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/error.py +0 -0
  50. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/extensions/__init__.py +0 -0
  51. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/extensions/config.py +0 -0
  52. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/gunicorn_config.py +0 -0
  53. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/healthz.py +0 -0
  54. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/helper/discover.py +0 -0
  55. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/helper/oauth.py +0 -0
  56. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/helper/plugin_requests.py +0 -0
  57. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/__init__.py +0 -0
  58. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/actions.py +0 -0
  59. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/auth_user.py +0 -0
  60. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/config.py +0 -0
  61. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/fetchers.py +0 -0
  62. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/graph.py +0 -0
  63. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/model_list.py +0 -0
  64. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/network.py +0 -0
  65. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/results/__init__.py +0 -0
  66. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/results/base.py +0 -0
  67. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/results/file.py +0 -0
  68. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/results/graph.py +0 -0
  69. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/results/image.py +0 -0
  70. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/results/status.py +0 -0
  71. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/results/validation.py +0 -0
  72. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/schema.py +0 -0
  73. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/selector.py +0 -0
  74. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/sync.py +0 -0
  75. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/models/validators.py +0 -0
  76. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/patched.py +0 -0
  77. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/__init__.py +0 -0
  78. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/celery_app.py +0 -0
  79. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/helpers/__init__.py +0 -0
  80. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/helpers/central_server.py +0 -0
  81. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/helpers/email_render.py +0 -0
  82. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/helpers/token.py +0 -0
  83. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/helpers/trino.py +0 -0
  84. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/models.py +0 -0
  85. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/plugin/utils.py +0 -0
  86. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/py.typed +0 -0
  87. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/__init__.py +0 -0
  88. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/datatypes/__init__.py +0 -0
  89. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/datatypes/cache.py +0 -0
  90. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/datatypes/events.py +0 -0
  91. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/datatypes/hash.py +0 -0
  92. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/datatypes/queues/__init__.py +0 -0
  93. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/datatypes/queues/comms.py +0 -0
  94. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/datatypes/set.py +0 -0
  95. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  96. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/security/__init__.py +0 -0
  97. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/security/obo.py +0 -0
  98. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/security/utils.py +0 -0
  99. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/services/config_service.py +0 -0
  100. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/services/jwt_service.py +0 -0
  101. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/clue/services/mongo_service.py +0 -0
  102. {clue_api-1.7.0.dev433 → clue_api-1.7.0.dev435}/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.dev433
3
+ Version: 1.7.0.dev435
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -8,7 +8,7 @@ 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
11
+ from clue.api import 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
@@ -22,7 +22,7 @@ logger = get_logger(__file__)
22
22
 
23
23
  SUB_API = "actions"
24
24
  actions_api = make_subapi_blueprint(SUB_API, api_version=1)
25
- actions_api._doc = "Run actions on data through configured external data sources/systems."
25
+ actions_api._doc = "Run actions on data through configured external data sources/systems." # type: ignore
26
26
 
27
27
  CORS(actions_api, origins=config.ui.cors_origins, supports_credentials=True)
28
28
 
@@ -122,4 +122,6 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, **kwargs) ->
122
122
  except NotFoundException as err:
123
123
  return not_found(err=err.message)
124
124
  except ClueException as err:
125
+ if err.status_code == 503:
126
+ return service_unavailable(err=err.message)
125
127
  return internal_error(err=err.message)
@@ -8,7 +8,7 @@ List and execute fetchers that provide data to be rendered client-side
8
8
 
9
9
  from flask_cors import CORS
10
10
 
11
- from clue.api import bad_gateway, bad_request, make_subapi_blueprint, not_found, ok
11
+ from clue.api import bad_gateway, bad_request, 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
@@ -120,6 +120,8 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, **kwargs):
120
120
  except NotFoundException as err:
121
121
  return not_found(err=err.message)
122
122
  except ClueException as err:
123
+ if err.status_code == 503:
124
+ return service_unavailable(err=err.message)
123
125
  if err.status_code == 400:
124
126
  logger.warning(
125
127
  "Bad request from fetcher %s.%s with task_id: %s: %s", plugin_id, fetcher_id, task_id, err.message
@@ -1,11 +1,10 @@
1
1
  from clue.common.logging import get_logger
2
- from clue.config import DEBUG, cache, config
2
+ from clue.config import config
3
3
 
4
4
  logger = get_logger(__file__)
5
5
 
6
6
 
7
- @cache.memoize(timeout=1 if DEBUG else 5 * 60) # Cached for 5 minutes
8
- def generate_headers(access_token: str | None, clue_access_token: str | None) -> dict[str, str]:
7
+ def generate_headers(obo_access_token: str | None, access_token: str | None) -> dict[str, str]:
9
8
  """Generates the request headers.
10
9
 
11
10
  Args:
@@ -19,12 +18,12 @@ def generate_headers(access_token: str | None, clue_access_token: str | None) ->
19
18
  "content-type": "application/json",
20
19
  }
21
20
 
22
- if access_token:
21
+ if obo_access_token or access_token:
23
22
  logger.debug("Appending authorization header")
24
- _headers["Authorization"] = f"Bearer {access_token}"
23
+ _headers["Authorization"] = f"Bearer {obo_access_token or access_token}"
25
24
 
26
- if config.auth.propagate_clue_key and clue_access_token:
25
+ if config.auth.propagate_clue_key and obo_access_token and access_token:
27
26
  logger.debug("Appending custom authorization header")
28
- _headers["X-Clue-Authorization"] = clue_access_token
27
+ _headers["X-Clue-Authorization"] = access_token
29
28
 
30
29
  return _headers
@@ -0,0 +1,31 @@
1
+ from typing import Any, Optional
2
+
3
+ from flask import has_request_context, request
4
+
5
+ from clue.common.exceptions import AuthenticationException
6
+ from clue.common.logging import get_logger
7
+ from clue.models.config import ExternalSource
8
+ from clue.services import auth_service
9
+
10
+ logger = get_logger(__file__)
11
+
12
+
13
+ def get_obo_access_token(
14
+ source: ExternalSource, user: dict[str, Any], access_token: Optional[str] = None
15
+ ) -> tuple[Optional[str], Optional[str]]:
16
+ """Get the caller access token and an OBO token for an external source when needed."""
17
+ if access_token is None and has_request_context():
18
+ auth_header = request.headers.get("Authorization", type=str)
19
+ if auth_header:
20
+ parts = auth_header.split(" ", 1)
21
+ access_token = parts[1] if len(parts) == 2 else auth_header
22
+
23
+ if not access_token:
24
+ return None, None
25
+
26
+ obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
27
+ if error:
28
+ logger.error("%s: %s", source.name, error)
29
+ raise AuthenticationException("Invalid token provided for this enrichment.")
30
+
31
+ return access_token, obo_access_token
@@ -1,80 +1,76 @@
1
- from typing import Any, Optional
1
+ from time import monotonic
2
+ from typing import Any
2
3
  from urllib.parse import urljoin
3
4
 
4
5
  import requests
5
6
  from elasticapm.traces import capture_span
6
7
  from flask import request
7
- from pydantic import TypeAdapter, ValidationError
8
+ from pydantic import TypeAdapter
8
9
  from requests import JSONDecodeError, exceptions
9
10
 
10
- from clue.common.exceptions import ClueException, NotFoundException
11
+ from clue.common.exceptions import AuthenticationException, ClueException, NotFoundException
11
12
  from clue.common.logging import get_logger
12
13
  from clue.config import CLASSIFICATION, config
13
14
  from clue.helper.headers import generate_headers
15
+ from clue.helper.obo import get_obo_access_token
14
16
  from clue.helper.plugin_requests import request_with_safe_redirects
15
17
  from clue.models.actions import ActionResult, ActionSpec
16
18
  from clue.models.config import ExternalSource
17
- from clue.services import auth_service
18
19
 
19
20
  logger = get_logger(__file__)
20
21
 
21
22
 
23
+ def _raise_action_metadata_unavailable(error: Exception | None = None) -> None:
24
+ raise ClueException(
25
+ "Unable to verify action availability with the upstream source.", error, status_code=503
26
+ ) from error
27
+
28
+
22
29
  def get_supported_actions(
23
- source: ExternalSource, user: dict[str, Any], access_token: Optional[str] = None
30
+ source: ExternalSource,
31
+ headers: dict[str, str],
32
+ *,
33
+ timeout: float = 10.0,
34
+ raise_on_error: bool = False,
24
35
  ) -> dict[str, ActionSpec]:
25
36
  """Gets all supported actions for a source
26
37
 
27
38
  Args:
28
- source_url (str): The URL of the source
29
- access_token (Optional[str], optional): The access token to use, if necessary. Defaults to None.
39
+ source (ExternalSource): The source whose actions to retrieve.
40
+ headers (dict[str, str]): Headers to include in the upstream request.
41
+ timeout (float): The upstream request timeout in seconds.
42
+ raise_on_error (bool): Raise a 503 when metadata cannot be verified.
30
43
 
31
44
  Returns:
32
45
  dict[str, ActionSpec]: A dict of each action and their schema
33
46
  """
34
47
  logger.info("Fetching actions for source %s", source.name)
35
-
36
48
  url = urljoin(source.url, "actions/")
37
49
 
38
- obo_access_token = None
39
- if access_token:
40
- obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
41
-
42
- if error:
43
- logger.error("%s: %s", source.name, error)
44
- return {}
45
-
46
- headers = generate_headers(obo_access_token or access_token, access_token if obo_access_token else None)
47
-
48
50
  with capture_span(f"GET {url}", span_type="http"):
49
51
  rsp = None
50
52
  try:
51
- rsp = request_with_safe_redirects(requests.get, url, headers=headers, timeout=10.0)
53
+ rsp = request_with_safe_redirects(requests.get, url, headers=headers, timeout=timeout)
52
54
  result = rsp.json()
53
55
 
54
56
  if not rsp.ok:
55
57
  err = result["api_error_message"]
56
58
  logger.error(f"Error from upstream server: {rsp.status_code=}, {err=}")
59
+ if raise_on_error:
60
+ _raise_action_metadata_unavailable()
61
+ return {}
57
62
 
58
63
  return TypeAdapter(dict[str, ActionSpec]).validate_python(result["api_response"])
59
- except (exceptions.ConnectionError, exceptions.Timeout):
60
- # any errors are logged and no result is saved to local cache to enable retry on next query
61
- logger.exception("Unable to connect: %s", url)
62
- return {}
63
- except (requests.exceptions.JSONDecodeError, KeyError, JSONDecodeError):
64
- logger.exception(
65
- "External API did not return expected format. Full data:\n\n%s\n\nStack Trace:",
66
- rsp.text if rsp else "None",
67
- )
68
- return {}
69
- except ValidationError:
70
- logger.exception("ValidationError in response from %s:\n%s", source.url)
71
- return {}
72
- except Exception:
73
- logger.exception("Unknown exception occurred on action fetching:")
64
+ except ClueException:
65
+ raise
66
+ except Exception as err:
67
+ logger.exception("Unable to retrieve action metadata from %s", source.url)
68
+ if raise_on_error:
69
+ _raise_action_metadata_unavailable(err)
74
70
  return {}
75
71
 
76
72
 
77
- def all_supported_actions(user: dict[str, Any], access_token: Optional[str] = None) -> dict[str, ActionSpec]:
73
+ def all_supported_actions(user: dict[str, Any]) -> dict[str, ActionSpec]:
78
74
  """Gets all supported actions for all sources
79
75
 
80
76
  Args:
@@ -86,7 +82,17 @@ def all_supported_actions(user: dict[str, Any], access_token: Optional[str] = No
86
82
  all_actions: dict[str, ActionSpec] = {}
87
83
 
88
84
  for source in config.api.external_sources:
89
- supported_actions = get_supported_actions(source, user, access_token=access_token)
85
+ if not CLASSIFICATION.is_accessible(user["classification"], source.classification):
86
+ continue
87
+
88
+ try:
89
+ access_token, obo_access_token = get_obo_access_token(source, user)
90
+ except AuthenticationException:
91
+ continue
92
+
93
+ supported_actions = get_supported_actions(
94
+ source, generate_headers(obo_access_token=obo_access_token, access_token=access_token)
95
+ )
90
96
  total_actions = 0
91
97
  for key, action in supported_actions.items():
92
98
  total_actions += 1
@@ -100,14 +106,7 @@ def get_plugins_supported_actions(user: dict[str, Any]) -> dict[str, ActionSpec]
100
106
  """Return the supported actions of each external service, filtered to what the user has access to."""
101
107
  available_actions: dict[str, ActionSpec] = {}
102
108
 
103
- access_token = request.headers.get("Authorization", type=str)
104
- if access_token:
105
- access_token = access_token.split(" ")[1]
106
-
107
- all_actions = all_supported_actions(
108
- user,
109
- access_token=access_token,
110
- )
109
+ all_actions = all_supported_actions(user)
111
110
 
112
111
  logger.info("Fetching actions for classification %s", user["classification"])
113
112
 
@@ -144,22 +143,19 @@ def execute_action(plugin_id: str, action_id: str, user: dict[str, Any]) -> Acti
144
143
  """
145
144
  plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
146
145
 
147
- if not plugin:
148
- raise NotFoundException(f"Plugin {plugin_id} does not exist.")
149
-
150
- access_token = request.headers.get("Authorization", type=str)
151
- if access_token:
152
- access_token = access_token.split(" ")[1]
146
+ if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
147
+ raise NotFoundException("Action not found.", status_code=404)
153
148
 
154
- obo_access_token = None
155
- if access_token:
156
- obo_access_token, error = auth_service.check_obo(plugin, access_token, user["uname"])
149
+ try:
150
+ access_token, obo_access_token = get_obo_access_token(plugin, user)
151
+ except AuthenticationException:
152
+ return ActionResult(outcome="failure", summary="Invalid token provided for this enrichment.")
157
153
 
158
- if error:
159
- logger.error("%s: %s", plugin.name, error)
160
- return ActionResult(outcome="failure", summary="Invalid token provided for this enrichment.")
154
+ headers = generate_headers(obo_access_token=obo_access_token, access_token=access_token)
161
155
 
162
- headers = generate_headers(obo_access_token or access_token, access_token if obo_access_token else None)
156
+ action = get_supported_actions(plugin, headers).get(action_id)
157
+ if action is None or not CLASSIFICATION.is_accessible(user["classification"], action.classification):
158
+ raise NotFoundException("Action not found.", status_code=404)
163
159
 
164
160
  if request.content_type == "application/json":
165
161
  parameters = request.json
@@ -211,22 +207,29 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, user: dict[s
211
207
  """
212
208
  plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
213
209
 
214
- if not plugin:
215
- raise NotFoundException(f"Plugin {plugin_id} does not exist.")
216
-
217
- access_token = request.headers.get("Authorization", type=str)
218
- if access_token:
219
- access_token = access_token.split(" ")[1]
220
-
221
- obo_access_token = None
222
- if access_token:
223
- obo_access_token, error = auth_service.check_obo(plugin, access_token, user["uname"])
210
+ if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
211
+ raise NotFoundException("Action not found.", status_code=404)
224
212
 
225
- if error:
226
- logger.error("%s: %s", plugin.name, error)
227
- return ActionResult(outcome="failure", summary="Invalid token provided.")
228
-
229
- headers = generate_headers(obo_access_token or access_token, access_token if obo_access_token else None)
213
+ try:
214
+ access_token, obo_access_token = get_obo_access_token(plugin, user)
215
+ except AuthenticationException:
216
+ return ActionResult(outcome="failure", summary="Invalid token provided.")
217
+
218
+ headers = generate_headers(obo_access_token=obo_access_token, access_token=access_token)
219
+
220
+ timeout = request.args.get("max_timeout", plugin.default_timeout, type=float)
221
+ metadata_started = monotonic()
222
+ # Authorization metadata must stay fresh; include this lookup in the caller's timeout budget.
223
+ action = get_supported_actions(
224
+ plugin,
225
+ headers,
226
+ timeout=max(min(timeout, 10.0), 0.001),
227
+ raise_on_error=True,
228
+ ).get(action_id)
229
+ if action is None or not CLASSIFICATION.is_accessible(user["classification"], action.classification):
230
+ raise NotFoundException("Action not found.", status_code=404)
231
+
232
+ remaining_timeout = max(timeout - (monotonic() - metadata_started), 0.001)
230
233
 
231
234
  try:
232
235
  req_url = urljoin(plugin.url, f"actions/{action_id}/status/{task_id}")
@@ -236,7 +239,7 @@ def get_action_status(plugin_id: str, action_id: str, task_id: str, user: dict[s
236
239
  requests.get,
237
240
  req_url,
238
241
  headers=headers,
239
- timeout=request.args.get("max_timeout", plugin.default_timeout, type=float),
242
+ timeout=remaining_timeout,
240
243
  )
241
244
 
242
245
  result = response.json()
@@ -283,7 +283,7 @@ def basic_auth(data: str, is_base64: bool = True) -> AuthResult:
283
283
 
284
284
 
285
285
  # TODO: sa-clue support
286
- def check_obo(source: ExternalSource, access_token: str, username: str) -> tuple[Optional[str], Optional[str]]:
286
+ def check_obo(source: ExternalSource, access_token: str | None, username: str) -> tuple[Optional[str], Optional[str]]:
287
287
  """Checks whether a token's audience matches the source, and if it doesn't, tries to get an OBO token for the source
288
288
 
289
289
  Args:
@@ -295,16 +295,16 @@ def check_obo(source: ExternalSource, access_token: str, username: str) -> tuple
295
295
  tuple[Optional[str], Optional[str]]: A tuple with either the valid access token in the first arg, or the error
296
296
  message in the second arg.
297
297
  """
298
- obo_access_token: Optional[str] = None
298
+ obo_access_token: str | None = None
299
299
  if source.obo_target:
300
300
  logger.debug("Source %s requires OBO to %s", source.name, source.obo_target)
301
301
 
302
- if "." not in access_token:
302
+ if access_token and "." not in access_token:
303
303
  logger.warning("JWT not provided, using service account.")
304
- if not (sa_token := jwt_service.fetch_sa_token()):
305
- return None, None
304
+ access_token = jwt_service.fetch_sa_token()
306
305
 
307
- access_token = sa_token
306
+ if not access_token:
307
+ return None, "Valid access token not provided."
308
308
 
309
309
  audience = jwt_service.extract_audience(access_token)
310
310
 
@@ -1,9 +1,10 @@
1
- from typing import Any, Optional
1
+ from time import monotonic
2
+ from typing import Any
2
3
  from urllib.parse import urljoin
3
4
 
4
5
  import requests
5
6
  from elasticapm.traces import capture_span
6
- from flask import has_request_context, request
7
+ from flask import request
7
8
  from pydantic import TypeAdapter, ValidationError
8
9
  from requests import JSONDecodeError, exceptions
9
10
 
@@ -15,48 +16,37 @@ from clue.common.exceptions import (
15
16
  NotFoundException,
16
17
  )
17
18
  from clue.common.logging import get_logger
18
- from clue.config import CLASSIFICATION, DEBUG, cache, config
19
+ from clue.config import CLASSIFICATION, config
20
+ from clue.helper.headers import generate_headers
21
+ from clue.helper.obo import get_obo_access_token
19
22
  from clue.helper.plugin_requests import request_with_safe_redirects
20
23
  from clue.models.config import ExternalSource
21
24
  from clue.models.fetchers import FetcherDefinition, FetcherResult
22
25
  from clue.models.selector import Selector
23
- from clue.services import auth_service
24
26
 
25
27
  logger = get_logger(__file__)
26
28
 
27
- # Either cache for one second in debug mode, or five minutes in production
28
- CACHE_TIMEOUT: int = 1 if DEBUG else 5 * 60
29
29
 
30
+ def _raise_fetcher_metadata_unavailable(error: Exception | None = None) -> None:
31
+ raise ClueException(
32
+ "Unable to verify fetcher availability with the upstream source.", error, status_code=503
33
+ ) from error
30
34
 
31
- def get_obo_access_token(
32
- source: ExternalSource, user: dict[str, Any], access_token: Optional[str] = None
33
- ) -> tuple[Optional[str], Optional[str]]:
34
- """Get the caller access token and an OBO token for an external source when needed."""
35
- if access_token is None and has_request_context():
36
- auth_header = request.headers.get("Authorization", type=str)
37
- if auth_header:
38
- parts = auth_header.split(" ", 1)
39
- access_token = parts[1] if len(parts) == 2 and parts[0].lower() == "bearer" else auth_header
40
- if not access_token:
41
- return None, None
42
35
 
43
- obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
44
- if error:
45
- logger.error("%s: %s", source.name, error)
46
- raise AuthenticationException("Invalid token provided for this enrichment.")
47
-
48
- return access_token, obo_access_token
49
-
50
-
51
- @cache.memoize(timeout=1 if DEBUG else 5 * 60, args_to_ignore=["access_token"]) # Cached for 5 minutes
52
36
  def get_supported_fetchers(
53
- source: ExternalSource, user: dict[str, Any], access_token: Optional[str] = None
37
+ source: ExternalSource,
38
+ headers: dict[str, str],
39
+ *,
40
+ timeout: float = 5.0,
41
+ raise_on_error: bool = False,
54
42
  ) -> dict[str, FetcherDefinition]:
55
- """Gets all supported fetchers for a source
43
+ """Fetch current metadata without caching authorization classifications.
56
44
 
57
45
  Args:
58
- source_url (str): The URL of the source
59
- access_token (Optional[str], optional): The access token to use, if necessary. Defaults to None.
46
+ source (ExternalSource): The source whose fetchers to retrieve.
47
+ headers (dict[str, str]): Headers to include in the upstream request.
48
+ timeout (float): The upstream request timeout in seconds.
49
+ raise_on_error (bool): Raise a 503 when metadata cannot be verified.
60
50
 
61
51
  Returns:
62
52
  dict[str, FetcherDefinition]: A dict of each ids mapped to fetcher metadata
@@ -65,42 +55,33 @@ def get_supported_fetchers(
65
55
 
66
56
  url = urljoin(source.url, "fetchers/")
67
57
 
68
- try:
69
- access_token, obo_access_token = get_obo_access_token(source, user, access_token)
70
- except AuthenticationException:
71
- return {}
72
-
73
- headers = {"Accept": "application/json"}
74
- if obo_access_token or access_token:
75
- headers["Authorization"] = f"Bearer {obo_access_token or access_token}"
76
-
77
58
  with capture_span(f"GET {url}", span_type="http"):
78
59
  try:
79
- rsp = request_with_safe_redirects(requests.get, url, headers=headers, timeout=5.0)
60
+ rsp = request_with_safe_redirects(requests.get, url, headers=headers, timeout=timeout)
80
61
  result = rsp.json()
81
62
 
82
63
  if not rsp.ok:
83
64
  err = result["api_error_message"]
84
65
  logger.error(f"Error from upstream server: {rsp.status_code=}, {err=}")
66
+ if raise_on_error:
67
+ _raise_fetcher_metadata_unavailable()
68
+ return {}
85
69
 
86
70
  return TypeAdapter(dict[str, FetcherDefinition]).validate_python(result["api_response"])
87
- except (exceptions.ConnectionError, exceptions.Timeout):
88
- # any errors are logged and no result is saved to local cache to enable retry on next query
89
- logger.exception("Unable to connect: %s", url)
90
- return {}
91
- except (requests.exceptions.JSONDecodeError, KeyError):
92
- logger.exception("External API did not return expected format:")
93
- return {}
94
- except ValidationError:
95
- logger.exception("ValidationError in response from %s:", source.url)
71
+ except ClueException:
72
+ raise
73
+ except Exception as err:
74
+ logger.exception("Unable to retrieve fetcher metadata from %s", source.url)
75
+ if raise_on_error:
76
+ _raise_fetcher_metadata_unavailable(err)
96
77
  return {}
97
78
 
98
79
 
99
- def all_supported_fetchers(user: dict[str, Any], access_token: Optional[str] = None) -> dict[str, FetcherDefinition]:
80
+ def all_supported_fetchers(user: dict[str, Any]) -> dict[str, FetcherDefinition]:
100
81
  """Gets all supported fetchers for all sources
101
82
 
102
83
  Args:
103
- access_token (Optional[str], optional): The access token to use, if necessary. Defaults to None.
84
+ user (dict[str, Any]): The user requesting the fetchers.
104
85
 
105
86
  Returns:
106
87
  dict[str, FetcherDefinition]: A dict of all fetchers and their matching schema
@@ -108,7 +89,17 @@ def all_supported_fetchers(user: dict[str, Any], access_token: Optional[str] = N
108
89
  all_fetchers: dict[str, FetcherDefinition] = {}
109
90
 
110
91
  for source in config.api.external_sources:
111
- supported_fetchers = get_supported_fetchers(source, user, access_token=access_token)
92
+ if not CLASSIFICATION.is_accessible(user["classification"], source.classification):
93
+ continue
94
+
95
+ try:
96
+ access_token, obo_access_token = get_obo_access_token(source, user)
97
+ except AuthenticationException:
98
+ continue
99
+
100
+ supported_fetchers = get_supported_fetchers(
101
+ source, generate_headers(obo_access_token=obo_access_token, access_token=access_token)
102
+ )
112
103
  total_fetchers = 0
113
104
  for key, action in supported_fetchers.items():
114
105
  total_fetchers += 1
@@ -122,14 +113,7 @@ def get_plugins_supported_fetchers(user: dict[str, Any]) -> dict[str, FetcherDef
122
113
  """Return the supported fetchers of each external service, filtered to what the user has access to."""
123
114
  available_fetchers: dict[str, FetcherDefinition] = {}
124
115
 
125
- access_token = request.headers.get("Authorization", type=str)
126
- if access_token:
127
- access_token = access_token.split(" ")[1]
128
-
129
- all_fetchers = all_supported_fetchers(
130
- user,
131
- access_token=access_token,
132
- )
116
+ all_fetchers = all_supported_fetchers(user)
133
117
 
134
118
  logger.info("Retrieving fetchers for classification %s", user["classification"])
135
119
 
@@ -175,14 +159,12 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
175
159
  """
176
160
  plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
177
161
 
178
- if not plugin:
179
- raise NotFoundException(f"Plugin {plugin_id} does not exist.")
162
+ if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
163
+ raise NotFoundException("Fetcher not found.", status_code=404)
180
164
 
181
165
  access_token, obo_access_token = get_obo_access_token(plugin, user)
182
166
 
183
- headers = {"Accept": "application/json"}
184
- if obo_access_token or access_token:
185
- headers["Authorization"] = f"Bearer {obo_access_token or access_token}"
167
+ headers = generate_headers(obo_access_token=obo_access_token, access_token=access_token)
186
168
 
187
169
  if request.is_json:
188
170
  parameters = request.json
@@ -197,14 +179,11 @@ def run_fetcher(plugin_id: str, fetcher_id: str, user: dict[str, Any]) -> Fetche
197
179
 
198
180
  try:
199
181
  selector = Selector.model_validate(parameters)
200
- supported_fetchers = get_supported_fetchers(plugin, user, access_token=access_token)
201
-
202
- if len(supported_fetchers) < 1:
203
- raise NotFoundException(f"{plugin_id} does not support any fetchers.")
182
+ supported_fetchers = get_supported_fetchers(plugin, headers)
204
183
 
205
184
  fetcher = supported_fetchers.get(fetcher_id)
206
- if fetcher is None:
207
- raise NotFoundException(f"Fetcher {fetcher_id} does not exist", status_code=404)
185
+ if fetcher is None or not CLASSIFICATION.is_accessible(user["classification"], fetcher.classification):
186
+ raise NotFoundException("Fetcher not found.", status_code=404)
208
187
  _validate_fetcher_classification(fetcher, selector, fetcher_id)
209
188
 
210
189
  response = request_with_safe_redirects(
@@ -255,14 +234,26 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict
255
234
  """
256
235
  plugin = next((source for source in config.api.external_sources if source.name == plugin_id), None)
257
236
 
258
- if not plugin:
259
- raise NotFoundException(f"Plugin {plugin_id} does not exist.")
237
+ if not plugin or not CLASSIFICATION.is_accessible(user["classification"], plugin.classification):
238
+ raise NotFoundException("Fetcher not found.", status_code=404)
260
239
 
261
240
  access_token, obo_access_token = get_obo_access_token(plugin, user)
262
241
 
263
- headers = {"Accept": "application/json"}
264
- if obo_access_token or access_token:
265
- headers["Authorization"] = f"Bearer {obo_access_token or access_token}"
242
+ headers = generate_headers(obo_access_token=obo_access_token, access_token=access_token)
243
+
244
+ timeout = request.args.get("max_timeout", 60.0, type=float)
245
+ metadata_started = monotonic()
246
+ # Authorization metadata must stay fresh; include this lookup in the caller's timeout budget.
247
+ fetcher = get_supported_fetchers(
248
+ plugin,
249
+ headers,
250
+ timeout=max(min(timeout, 5.0), 0.001),
251
+ raise_on_error=True,
252
+ ).get(fetcher_id)
253
+ if fetcher is None or not CLASSIFICATION.is_accessible(user["classification"], fetcher.classification):
254
+ raise NotFoundException("Fetcher not found.", status_code=404)
255
+
256
+ remaining_timeout = max(timeout - (monotonic() - metadata_started), 0.001)
266
257
 
267
258
  try:
268
259
  req_url = urljoin(plugin.url, f"fetchers/{fetcher_id}/status/{task_id}")
@@ -272,7 +263,7 @@ def get_fetcher_status(plugin_id: str, fetcher_id: str, task_id: str, user: dict
272
263
  requests.get,
273
264
  req_url,
274
265
  headers=headers,
275
- timeout=request.args.get("max_timeout", 60.0, type=float),
266
+ timeout=remaining_timeout,
276
267
  )
277
268
 
278
269
  result = response.json()
@@ -26,11 +26,12 @@ from clue.common.logging.audit import audit
26
26
  from clue.config import CLASSIFICATION as CLASSIFICATION
27
27
  from clue.config import DEBUG, config
28
28
  from clue.helper.headers import generate_headers
29
+ from clue.helper.obo import get_obo_access_token
29
30
  from clue.helper.plugin_requests import request_with_safe_redirects
30
31
  from clue.models.config import ExternalSource
31
32
  from clue.models.network import QueryEntry, QueryResult
32
33
  from clue.models.selector import Selector
33
- from clue.services import auth_service, mongo_service, type_service, user_service
34
+ from clue.services import mongo_service, type_service, user_service
34
35
 
35
36
  logger = get_logger(__file__)
36
37
  CLIENTS: dict[str, Session] = {}
@@ -361,8 +362,8 @@ def query_external(
361
362
  value: str,
362
363
  limit: int,
363
364
  timeout: float,
364
- access_token: str,
365
- clue_access_token: str | None,
365
+ access_token: str | None,
366
+ obo_access_token: str | None,
366
367
  no_annotation: bool = False,
367
368
  no_cache: bool = False,
368
369
  include_raw: bool = True,
@@ -375,7 +376,10 @@ def query_external(
375
376
  finish_result = functools.partial(build_result, type_name, value, source)
376
377
 
377
378
  with capture_span(query_external.__name__, span_type="greenlet"):
378
- if type_name not in type_service.all_supported_types(user, access_token=access_token).get(source.name, {}):
379
+ supported_types = type_service.get_supported_types(
380
+ source.url, access_token=access_token, obo_access_token=obo_access_token
381
+ )
382
+ if not supported_types or type_name not in supported_types:
379
383
  return finish_result(error="invalid_type")
380
384
 
381
385
  if config.api.audit:
@@ -407,7 +411,7 @@ def query_external(
407
411
  get_client(source.url, timeout).get,
408
412
  url,
409
413
  params=generate_params(limit, timeout, no_annotation, include_raw, no_cache),
410
- headers=generate_headers(access_token, clue_access_token),
414
+ headers=generate_headers(obo_access_token=obo_access_token, access_token=access_token),
411
415
  timeout=(timeout, timeout * 3),
412
416
  )
413
417
  rsp.raise_for_status()
@@ -493,15 +497,10 @@ def enrich(type_name: str, value: str, user: dict[str, Any]): # noqa: C901
493
497
 
494
498
  finish_result = functools.partial(build_result, type_name, value, source)
495
499
 
496
- obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
497
-
498
- # TODO: sa-clue support
499
- if not obo_access_token and source.obo_target:
500
- results[source.name] = finish_result(error="You must have a valid JWT to access this plugin.")
501
- continue
502
-
503
- if error:
504
- results[source.name] = finish_result(error=error)
500
+ try:
501
+ access_token, obo_access_token = get_obo_access_token(source, user)
502
+ except AuthenticationException as err:
503
+ results[source.name] = finish_result(error=err.message)
505
504
  continue
506
505
 
507
506
  # check query against the max supported classification of the external system
@@ -527,8 +526,8 @@ def enrich(type_name: str, value: str, user: dict[str, Any]): # noqa: C901
527
526
  value=value,
528
527
  limit=query_params.limit,
529
528
  timeout=query_params.max_timeout,
530
- access_token=obo_access_token or access_token,
531
- clue_access_token=access_token if obo_access_token else None,
529
+ access_token=access_token,
530
+ obo_access_token=obo_access_token,
532
531
  no_annotation=query_params.no_annotation,
533
532
  include_raw=query_params.include_raw,
534
533
  no_cache=query_params.no_cache,
@@ -562,8 +561,8 @@ def bulk_query_external( # noqa: C901
562
561
  source: ExternalSource,
563
562
  limit: int,
564
563
  timeout: float,
565
- access_token: str,
566
- clue_access_token: str | None,
564
+ access_token: str | None,
565
+ obo_access_token: str | None,
567
566
  no_annotation: bool = False,
568
567
  no_cache: bool = False,
569
568
  include_raw: bool = True,
@@ -574,14 +573,16 @@ def bulk_query_external( # noqa: C901
574
573
  execution_context.set_transaction(apm_transaction)
575
574
 
576
575
  with capture_span(bulk_query_external.__name__, span_type="greenlet"):
577
- supported_types = type_service.all_supported_types(user, access_token=access_token).get(source.name, {})
576
+ supported_types = type_service.get_supported_types(
577
+ source.url, access_token=access_token, obo_access_token=obo_access_token
578
+ )
578
579
  bulk_result: dict[str, dict[str, QueryResult]] = {}
579
580
 
580
581
  filtered_data: list[Selector] = []
581
582
  for entry in data:
582
583
  bulk_result.setdefault(entry.type, {})
583
584
 
584
- if entry.type not in supported_types:
585
+ if not supported_types or entry.type not in supported_types:
585
586
  bulk_result[entry.type][entry.value] = build_result(entry.type, entry.value, source, "invalid_type")
586
587
  continue
587
588
 
@@ -629,7 +630,7 @@ def bulk_query_external( # noqa: C901
629
630
  get_method=client.get,
630
631
  params=generate_params(limit, timeout, no_annotation, include_raw, no_cache),
631
632
  json=[entry.model_dump(exclude_none=True, exclude_unset=True) for entry in data],
632
- headers=generate_headers(access_token, clue_access_token),
633
+ headers=generate_headers(obo_access_token=obo_access_token, access_token=access_token),
633
634
  timeout=(timeout * 3, timeout * 3),
634
635
  )
635
636
  rsp.raise_for_status()
@@ -697,11 +698,6 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
697
698
  f"excluding sources [{','.join(excluded_sources)}]"
698
699
  )
699
700
 
700
- access_token = request.headers.get("Authorization", type=str)
701
- if not access_token:
702
- raise AuthenticationException("Access token is required to enrich.")
703
- access_token = access_token.split(" ")[1]
704
-
705
701
  if len(data) < 1:
706
702
  raise InvalidDataException("You must provide at least one value to lookup.")
707
703
 
@@ -744,16 +740,12 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
744
740
 
745
741
  greenlets: list[tuple[list[Selector], ExternalSource, Greenlet[Any, dict[str, dict[str, QueryResult]]]]] = []
746
742
  for source in selected_sources:
747
- obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
748
-
749
- if error:
750
- logger.error("%s: %s", source.name, error)
751
-
752
- # TODO: sa-clue support
753
- if not obo_access_token and source.obo_target:
743
+ try:
744
+ access_token, obo_access_token = get_obo_access_token(source, user)
745
+ except AuthenticationException as err:
754
746
  for entry in data:
755
747
  bulk_result[entry.type][entry.value][source.name] = build_result(
756
- entry.type, entry.value, source, "You must have a valid JWT to access this plugin."
748
+ entry.type, entry.value, source, err.message
757
749
  )
758
750
  continue
759
751
 
@@ -811,8 +803,8 @@ def bulk_enrich(data: list[Selector], user: dict[str, Any]): # noqa: C901
811
803
  source=source,
812
804
  limit=query_params.limit,
813
805
  timeout=query_params.max_timeout,
814
- access_token=obo_access_token or access_token,
815
- clue_access_token=access_token if obo_access_token else None,
806
+ access_token=access_token,
807
+ obo_access_token=obo_access_token,
816
808
  no_annotation=query_params.no_annotation,
817
809
  no_cache=query_params.no_cache,
818
810
  include_raw=query_params.include_raw,
@@ -5,15 +5,16 @@ from elasticapm.traces import capture_span
5
5
  from flask import request
6
6
  from requests import exceptions
7
7
 
8
+ from clue.common.exceptions import AuthenticationException
8
9
  from clue.common.logging import get_logger
9
10
  from clue.config import CLASSIFICATION, DEBUG, cache, config
10
11
  from clue.constants.env import DISABLE_CACHE
11
12
  from clue.constants.supported_types import SUPPORTED_TYPES
12
13
  from clue.helper.headers import generate_headers
14
+ from clue.helper.obo import get_obo_access_token
13
15
  from clue.helper.plugin_requests import request_with_safe_redirects
14
16
  from clue.models.config import ExternalSource
15
17
  from clue.remote.datatypes.cache import RedisCache
16
- from clue.services import auth_service
17
18
 
18
19
  logger = get_logger(__file__)
19
20
 
@@ -28,10 +29,7 @@ def get_types_regular_expressions(user: dict[str, Any]):
28
29
  if access_token:
29
30
  access_token = access_token.split(" ")[1]
30
31
 
31
- all_types = all_supported_types(
32
- user,
33
- access_token=access_token,
34
- )
32
+ all_types = all_supported_types(user)
35
33
 
36
34
  type_detection = {}
37
35
 
@@ -47,7 +45,9 @@ def get_types_regular_expressions(user: dict[str, Any]):
47
45
 
48
46
 
49
47
  @cache.memoize(timeout=CACHE_TIMEOUT)
50
- def get_supported_types(source_url: str, access_token: str | None = None, obo_access_token: str | None = None):
48
+ def get_supported_types(
49
+ source_url: str, access_token: str | None = None, obo_access_token: str | None = None
50
+ ) -> dict[str, str] | None:
51
51
  """Gets all supported types for the specified source.
52
52
 
53
53
  Args:
@@ -65,7 +65,7 @@ def get_supported_types(source_url: str, access_token: str | None = None, obo_ac
65
65
 
66
66
  logger.debug("Cache miss, polling plugin")
67
67
  with capture_span(f"GET {url}", span_type="http"):
68
- headers = generate_headers(obo_access_token or access_token, access_token if obo_access_token else None)
68
+ headers = generate_headers(obo_access_token=obo_access_token, access_token=access_token)
69
69
 
70
70
  try:
71
71
  rsp = request_with_safe_redirects(requests.get, url, headers=headers, timeout=3.0)
@@ -111,7 +111,7 @@ def get_supported_types(source_url: str, access_token: str | None = None, obo_ac
111
111
  return None
112
112
 
113
113
 
114
- def all_supported_types(user: dict[str, Any], access_token: str | None = None) -> dict[str, dict[str, str]]:
114
+ def all_supported_types(user: dict[str, Any]) -> dict[str, dict[str, str]]:
115
115
  """Gets supported types by all sources.
116
116
 
117
117
  Args:
@@ -123,12 +123,13 @@ def all_supported_types(user: dict[str, Any], access_token: str | None = None) -
123
123
  all_types = {}
124
124
 
125
125
  for source in config.api.external_sources:
126
- obo_access_token = None
127
- if access_token:
128
- obo_access_token, error = auth_service.check_obo(source, access_token, user["uname"])
126
+ if not CLASSIFICATION.is_accessible(user["classification"], source.classification):
127
+ continue
129
128
 
130
- if error:
131
- logger.error("%s: %s", source.name, error)
129
+ try:
130
+ access_token, obo_access_token = get_obo_access_token(source, user)
131
+ except AuthenticationException:
132
+ continue
132
133
 
133
134
  supported_types = get_supported_types(source.url, access_token=access_token, obo_access_token=obo_access_token)
134
135
  if supported_types is not None:
@@ -142,11 +143,7 @@ def get_plugins_supported_types(user: dict[str, Any]) -> dict[str, list[str]]:
142
143
  configured_sources: list[ExternalSource] = getattr(config.api, "external_sources", [])
143
144
  available_types: dict[str, list[str]] = {}
144
145
 
145
- access_token = request.headers.get("Authorization", type=str)
146
- if access_token:
147
- access_token = access_token.split(" ")[1]
148
-
149
- all_types = all_supported_types(user, access_token=access_token)
146
+ all_types = all_supported_types(user)
150
147
 
151
148
  logger.info("Fetching sources for classification %s", user["classification"])
152
149
 
@@ -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.dev433"
144
+ version = "1.7.0.dev435"
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