mainsequence 9.0.12.dev51__tar.gz → 9.0.13__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.
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/PKG-INFO +1 -1
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/config.py +3 -1
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/models_user.py +14 -2
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/utils.py +8 -22
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/logconf.py +1 -8
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/runtime_credential_exchange.py +28 -36
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/PKG-INFO +1 -1
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/pyproject.toml +1 -1
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_auth_precedence.py +33 -26
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_canonical_api_route_contract.py +1 -4
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_cli_secure_storage.py +8 -10
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_runtime_context.py +4 -13
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_credential_repr.py +1 -27
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_http_retry_budget.py +6 -2
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_runtime_credential_exchange.py +130 -53
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/LICENSE +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/README.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/AGENTS.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/__init__.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/__main__.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/_request_identity.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/bootstrap.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/api.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/cli.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/code_repository_status.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/cli/ui.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/__init__.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/_transport.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/base.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/client.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/github_issues.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/inference.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/observability.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/client/value_sets.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/code_repository_context.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/code_repository_skills.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/defaults.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/repository_identity_security.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/server/__init__.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/server/caller_assertions.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/server/fastapi.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/SOURCES.txt +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/setup.cfg +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_auth_bootstrap.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_caller_assertions.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_cli_auth_contract.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_client.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_branch_contract.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_documentation_contract.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_skills.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_dependency_extras.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_dev_release_version.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_environment_resource_context.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_environment_response_contracts.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_fastapi_request_identity.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_github_issues_client.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_independent_context.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_inference_client.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_instrumentation.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_job_client.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_local_ops.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_logconf.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_models_user_request_bound_auth.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_observability_client.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_secret_client_model.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_source_ownership.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_ssh_utils.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_tolerant_response_reading.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_tolerant_value_sets.py +0 -0
- {mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_user_application_facts.py +0 -0
|
@@ -75,6 +75,8 @@ KEYCHAIN_ACCOUNT = "default"
|
|
|
75
75
|
AUTH_RECORD_VERSION = 1
|
|
76
76
|
# Credential entries a project `.env` must not hold. The CLI removes them and
|
|
77
77
|
# never writes them; the session lives in the operating system credential store.
|
|
78
|
+
# The runtime credential secret and `MAINSEQUENCE_TOKEN` are no longer read: they
|
|
79
|
+
# stay listed so that an entry an earlier version left in a checkout is removed.
|
|
78
80
|
PROJECT_ENV_CREDENTIAL_KEYS = (
|
|
79
81
|
"MAINSEQUENCE_ACCESS_TOKEN",
|
|
80
82
|
"MAINSEQUENCE_REFRESH_TOKEN",
|
|
@@ -805,7 +807,7 @@ def session_report() -> dict:
|
|
|
805
807
|
refresh_expires_at = token_expiry(refresh)
|
|
806
808
|
|
|
807
809
|
if auth_mode == "runtime_credential":
|
|
808
|
-
# The credential is an ID with
|
|
810
|
+
# The credential is an ID with the identity token file that proves it.
|
|
809
811
|
authenticated = bool(access or runtime_credential_configured())
|
|
810
812
|
session_expires_at = None
|
|
811
813
|
elif refresh:
|
|
@@ -983,8 +983,10 @@ class User(_CallerDirectoryReadMixin, UserApiBaseObjectOrm, BasePydanticModel):
|
|
|
983
983
|
``User.filter(identity_type="workload")``. Its row carries ``uid``,
|
|
984
984
|
``identity_type`` (``"workload"``), ``is_active`` and the UID of its
|
|
985
985
|
workload in ``job_uid``, ``resource_release_uid`` or ``agent_uid``, without
|
|
986
|
-
a username, email, join date or name.
|
|
987
|
-
``
|
|
986
|
+
a username, email, join date or person's name. A listed row of a workload
|
|
987
|
+
the caller can view also carries ``workload_name``, the name of that Job,
|
|
988
|
+
release or Agent; a lookup by UID does not. Fields the row does not carry
|
|
989
|
+
are ``None``, or empty for lists.
|
|
988
990
|
|
|
989
991
|
The reverse direction is ``workload_user_uid`` on ``Job``,
|
|
990
992
|
``ResourceRelease`` and ``Agent``: the UID of the User that workload runs
|
|
@@ -1059,6 +1061,16 @@ class User(_CallerDirectoryReadMixin, UserApiBaseObjectOrm, BasePydanticModel):
|
|
|
1059
1061
|
description="UID of the Agent a workload identity belongs to; None otherwise.",
|
|
1060
1062
|
examples=[None],
|
|
1061
1063
|
)
|
|
1064
|
+
workload_name: str | None = Field(
|
|
1065
|
+
None,
|
|
1066
|
+
title="Workload Name",
|
|
1067
|
+
description=(
|
|
1068
|
+
"Name of the Job, release or Agent a workload identity belongs to, on a "
|
|
1069
|
+
"listed row of a workload the caller can view. None otherwise, including "
|
|
1070
|
+
"on a lookup by UID and on a person."
|
|
1071
|
+
),
|
|
1072
|
+
examples=["nightly-prices"],
|
|
1073
|
+
)
|
|
1062
1074
|
managed_by_caller: bool | None = Field(
|
|
1063
1075
|
None,
|
|
1064
1076
|
title="Managed By Caller",
|
|
@@ -23,8 +23,6 @@ from mainsequence.defaults import resolve_backend_endpoint
|
|
|
23
23
|
from mainsequence.logconf import logger
|
|
24
24
|
from mainsequence.runtime_credential_exchange import (
|
|
25
25
|
RUNTIME_CREDENTIAL_ID_ENV,
|
|
26
|
-
RUNTIME_CREDENTIAL_SECRET_ENV,
|
|
27
|
-
RUNTIME_IDENTITY_TOKEN_FILE_ENV,
|
|
28
26
|
RuntimeIdentityTokenError,
|
|
29
27
|
exchange_runtime_credential,
|
|
30
28
|
identity_token_file_from_environment,
|
|
@@ -321,17 +319,16 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
|
321
319
|
"""
|
|
322
320
|
Exchange the runtime credential the platform injected for short-lived access tokens.
|
|
323
321
|
|
|
324
|
-
The proof is the projected workload identity token
|
|
325
|
-
`MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
provider
|
|
329
|
-
(`429`) and temporarily unavailable (`503`) exchanges are retried
|
|
330
|
-
timeout budget; a rejected one (`401`) fails at once.
|
|
322
|
+
The proof is the projected workload identity token in the file named by
|
|
323
|
+
`MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE`, read for every exchange. It is the
|
|
324
|
+
only proof: an unset variable, or a missing, unreadable or empty file, raises
|
|
325
|
+
`AuthError` instead of sending the exchange. The token is never kept on the
|
|
326
|
+
provider.
|
|
327
|
+
Throttled (`429`) and temporarily unavailable (`503`) exchanges are retried
|
|
328
|
+
within the timeout budget; a rejected one (`401`) fails at once.
|
|
331
329
|
"""
|
|
332
330
|
|
|
333
331
|
credential_id: str | None = None
|
|
334
|
-
credential_secret: str | None = field(default=None, repr=False)
|
|
335
332
|
token_url: str | None = None
|
|
336
333
|
token_type: str = "Bearer"
|
|
337
334
|
refresh_skew_seconds: int = 30
|
|
@@ -347,8 +344,6 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
|
347
344
|
self.identity_token_file = identity_token_file_from_environment()
|
|
348
345
|
else:
|
|
349
346
|
self.identity_token_file = str(self.identity_token_file).strip() or None
|
|
350
|
-
if self.credential_secret is None and not self.identity_token_file:
|
|
351
|
-
self.credential_secret = os.getenv(RUNTIME_CREDENTIAL_SECRET_ENV)
|
|
352
347
|
if not self.token_url:
|
|
353
348
|
self.token_url = f"{API_ENDPOINT}/runtime-credentials/token/"
|
|
354
349
|
|
|
@@ -376,19 +371,11 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
|
376
371
|
|
|
377
372
|
def _exchange(self) -> requests.Response:
|
|
378
373
|
credential_id = (self.credential_id or "").strip()
|
|
379
|
-
identity_token_file = self.identity_token_file
|
|
380
|
-
credential_secret = None if identity_token_file else (self.credential_secret or "").strip()
|
|
381
374
|
if not credential_id:
|
|
382
375
|
raise AuthError(
|
|
383
376
|
"MAINSEQUENCE_RUNTIME_CREDENTIAL_ID is required when "
|
|
384
377
|
"MAINSEQUENCE_AUTH_MODE=runtime_credential."
|
|
385
378
|
)
|
|
386
|
-
if not identity_token_file and not credential_secret:
|
|
387
|
-
raise AuthError(
|
|
388
|
-
"MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET is required when "
|
|
389
|
-
"MAINSEQUENCE_AUTH_MODE=runtime_credential and "
|
|
390
|
-
f"{RUNTIME_IDENTITY_TOKEN_FILE_ENV} is not set."
|
|
391
|
-
)
|
|
392
379
|
try:
|
|
393
380
|
# One budget for every attempt and wait, inside the caller's budget if any.
|
|
394
381
|
with request_budget(self.timeout) as budget:
|
|
@@ -405,8 +392,7 @@ class RuntimeCredentialAuthProvider(BaseAuthProvider):
|
|
|
405
392
|
return exchange_runtime_credential(
|
|
406
393
|
post,
|
|
407
394
|
credential_id=credential_id,
|
|
408
|
-
|
|
409
|
-
identity_token_file=identity_token_file,
|
|
395
|
+
identity_token_file=self.identity_token_file,
|
|
410
396
|
deadline=budget.deadline,
|
|
411
397
|
on_retry=self._log_exchange_retry,
|
|
412
398
|
)
|
|
@@ -25,7 +25,6 @@ from .instrumentation import OTelJSONRenderer
|
|
|
25
25
|
from .repository_identity_security import UNSUPPORTED_SOURCE_IDENTITY_ENV_NAMES
|
|
26
26
|
from .runtime_credential_exchange import (
|
|
27
27
|
RUNTIME_CREDENTIAL_ID_ENV,
|
|
28
|
-
RUNTIME_CREDENTIAL_SECRET_ENV,
|
|
29
28
|
exchange_runtime_credential,
|
|
30
29
|
identity_token_file_from_environment,
|
|
31
30
|
runtime_credential_configured,
|
|
@@ -104,7 +103,6 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
|
|
|
104
103
|
def _exchange_runtime_credential() -> bool:
|
|
105
104
|
if not runtime_credential_configured():
|
|
106
105
|
return False
|
|
107
|
-
identity_token_file = identity_token_file_from_environment()
|
|
108
106
|
token_url = f"{_backend_base_url()}/api/v1/runtime-credentials/token/"
|
|
109
107
|
|
|
110
108
|
def _post(body: dict[str, str]) -> requests.Response:
|
|
@@ -120,12 +118,7 @@ def _request_job_startup_state(*, timeout_s: float = 10.0) -> dict[str, Any]:
|
|
|
120
118
|
token_resp = exchange_runtime_credential(
|
|
121
119
|
_post,
|
|
122
120
|
credential_id=(os.getenv(RUNTIME_CREDENTIAL_ID_ENV) or "").strip(),
|
|
123
|
-
|
|
124
|
-
None
|
|
125
|
-
if identity_token_file
|
|
126
|
-
else (os.getenv(RUNTIME_CREDENTIAL_SECRET_ENV) or "").strip()
|
|
127
|
-
),
|
|
128
|
-
identity_token_file=identity_token_file,
|
|
121
|
+
identity_token_file=identity_token_file_from_environment(),
|
|
129
122
|
deadline=time.monotonic() + timeout_s,
|
|
130
123
|
)
|
|
131
124
|
except Exception:
|
{mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/mainsequence/runtime_credential_exchange.py
RENAMED
|
@@ -3,13 +3,11 @@ The runtime credential exchange, shared by the auth provider and the logger.
|
|
|
3
3
|
|
|
4
4
|
A runtime started with ``MAINSEQUENCE_AUTH_MODE=runtime_credential`` exchanges its
|
|
5
5
|
credential at ``POST /api/v1/runtime-credentials/token/`` for a short-lived access
|
|
6
|
-
token. The request carries ``credential_id`` and
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
unreadable or empty file is an error, never a fallback to the secret.
|
|
12
|
-
* ``credential_secret`` otherwise: ``MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET``.
|
|
6
|
+
token. The request carries exactly ``credential_id`` and ``workload_identity_token``:
|
|
7
|
+
the projected token read from the file named by
|
|
8
|
+
``MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE`` for every exchange, because the file is
|
|
9
|
+
rotated. That token is the only proof. An unset variable and a missing, unreadable
|
|
10
|
+
or empty file are errors, raised instead of sending the request.
|
|
13
11
|
|
|
14
12
|
The token exists only inside one exchange request. It is not kept on any object,
|
|
15
13
|
written to the environment or a file, logged, or included in an error message.
|
|
@@ -29,7 +27,6 @@ from urllib3.exceptions import InvalidHeader
|
|
|
29
27
|
from urllib3.util import Retry
|
|
30
28
|
|
|
31
29
|
RUNTIME_CREDENTIAL_ID_ENV = "MAINSEQUENCE_RUNTIME_CREDENTIAL_ID"
|
|
32
|
-
RUNTIME_CREDENTIAL_SECRET_ENV = "MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET"
|
|
33
30
|
RUNTIME_IDENTITY_TOKEN_FILE_ENV = "MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE"
|
|
34
31
|
|
|
35
32
|
# 429: the exchange is throttled. 503: verification is temporarily unavailable.
|
|
@@ -42,7 +39,7 @@ _RETRY_AFTER = Retry(total=0)
|
|
|
42
39
|
|
|
43
40
|
|
|
44
41
|
class RuntimeIdentityTokenError(Exception):
|
|
45
|
-
"""The
|
|
42
|
+
"""The identity token file is unset or cannot be used. The message never holds its content."""
|
|
46
43
|
|
|
47
44
|
|
|
48
45
|
def _environment_value(name: str) -> str:
|
|
@@ -58,18 +55,22 @@ def runtime_credential_configured() -> bool:
|
|
|
58
55
|
"""
|
|
59
56
|
Say whether the environment holds a runtime credential to exchange.
|
|
60
57
|
|
|
61
|
-
That is a credential ID and
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
That is a credential ID and the identity token file that proves it. Only the
|
|
59
|
+
presence of both settings is checked. The token file is read by the exchange
|
|
60
|
+
itself, which reports a missing or empty file.
|
|
64
61
|
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return True
|
|
69
|
-
return bool(_environment_value(RUNTIME_CREDENTIAL_SECRET_ENV))
|
|
62
|
+
return bool(
|
|
63
|
+
_environment_value(RUNTIME_CREDENTIAL_ID_ENV) and identity_token_file_from_environment()
|
|
64
|
+
)
|
|
70
65
|
|
|
71
66
|
|
|
72
|
-
def _read_identity_token(path: str) -> str:
|
|
67
|
+
def _read_identity_token(path: str | None) -> str:
|
|
68
|
+
if not path:
|
|
69
|
+
raise RuntimeIdentityTokenError(
|
|
70
|
+
f"{RUNTIME_IDENTITY_TOKEN_FILE_ENV} is required when "
|
|
71
|
+
"MAINSEQUENCE_AUTH_MODE=runtime_credential: a runtime credential is exchanged "
|
|
72
|
+
"only with the projected workload identity token in that file."
|
|
73
|
+
)
|
|
73
74
|
source = f"The runtime identity token file {path} ({RUNTIME_IDENTITY_TOKEN_FILE_ENV})"
|
|
74
75
|
try:
|
|
75
76
|
with open(path, "rb") as token_file:
|
|
@@ -92,17 +93,11 @@ def _read_identity_token(path: str) -> str:
|
|
|
92
93
|
return token
|
|
93
94
|
|
|
94
95
|
|
|
95
|
-
def _request_body(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if identity_token_file:
|
|
101
|
-
return {
|
|
102
|
-
"credential_id": credential_id,
|
|
103
|
-
"workload_identity_token": _read_identity_token(identity_token_file),
|
|
104
|
-
}
|
|
105
|
-
return {"credential_id": credential_id, "credential_secret": credential_secret or ""}
|
|
96
|
+
def _request_body(credential_id: str, identity_token_file: str | None) -> dict[str, str]:
|
|
97
|
+
return {
|
|
98
|
+
"credential_id": credential_id,
|
|
99
|
+
"workload_identity_token": _read_identity_token(identity_token_file),
|
|
100
|
+
}
|
|
106
101
|
|
|
107
102
|
|
|
108
103
|
def _retry_delay(response: Any, attempt: int) -> float:
|
|
@@ -120,7 +115,6 @@ def exchange_runtime_credential(
|
|
|
120
115
|
send: Callable[[dict[str, str]], Any],
|
|
121
116
|
*,
|
|
122
117
|
credential_id: str,
|
|
123
|
-
credential_secret: str | None,
|
|
124
118
|
identity_token_file: str | None,
|
|
125
119
|
deadline: float,
|
|
126
120
|
on_retry: Callable[[int, float], None] | None = None,
|
|
@@ -135,8 +129,6 @@ def exchange_runtime_credential(
|
|
|
135
129
|
response. It is called once per attempt.
|
|
136
130
|
credential_id:
|
|
137
131
|
The runtime credential ID.
|
|
138
|
-
credential_secret:
|
|
139
|
-
The bootstrap secret. Ignored when ``identity_token_file`` is set.
|
|
140
132
|
identity_token_file:
|
|
141
133
|
The projected token file. It is read again for every attempt.
|
|
142
134
|
deadline:
|
|
@@ -147,12 +139,12 @@ def exchange_runtime_credential(
|
|
|
147
139
|
A ``429`` or ``503`` answer is retried up to ``EXCHANGE_RETRIES`` times. The
|
|
148
140
|
wait is the exponential backoff or the answer's ``Retry-After``, whichever is
|
|
149
141
|
longer; a wait that would not end before ``deadline`` ends the retries. Any
|
|
150
|
-
other answer, ``401`` included, is returned at once
|
|
151
|
-
|
|
152
|
-
be used.
|
|
142
|
+
other answer, ``401`` included, is returned at once. ``RuntimeIdentityTokenError``
|
|
143
|
+
is raised instead of sending an attempt when ``identity_token_file`` is not set
|
|
144
|
+
or the file cannot be used.
|
|
153
145
|
"""
|
|
154
146
|
for attempt in range(EXCHANGE_RETRIES + 1):
|
|
155
|
-
response = send(_request_body(credential_id,
|
|
147
|
+
response = send(_request_body(credential_id, identity_token_file))
|
|
156
148
|
status = response.status_code
|
|
157
149
|
if status not in RETRYABLE_EXCHANGE_STATUSES or attempt == EXCHANGE_RETRIES:
|
|
158
150
|
return response
|
|
@@ -41,6 +41,13 @@ def _jwt_with_exp(exp: int) -> str:
|
|
|
41
41
|
return f"{header}.{payload}.signature"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def _use_identity_token_file(monkeypatch, directory) -> str:
|
|
45
|
+
token_file = directory / "token"
|
|
46
|
+
token_file.write_text("projected-token", encoding="utf-8")
|
|
47
|
+
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", str(token_file))
|
|
48
|
+
return str(token_file)
|
|
49
|
+
|
|
50
|
+
|
|
44
51
|
def test_build_default_auth_provider_uses_jwt(monkeypatch):
|
|
45
52
|
monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
|
|
46
53
|
monkeypatch.delenv("MAINSEQUENCE_REFRESH_TOKEN", raising=False)
|
|
@@ -110,12 +117,12 @@ def test_logconf_refreshes_jwt_before_startup_state_request(monkeypatch):
|
|
|
110
117
|
assert get_calls[0]["headers"]["Authorization"] == "Bearer jwt-access"
|
|
111
118
|
|
|
112
119
|
|
|
113
|
-
def test_logconf_runtime_credential_exchanges_before_startup_state_request(monkeypatch):
|
|
120
|
+
def test_logconf_runtime_credential_exchanges_before_startup_state_request(monkeypatch, tmp_path):
|
|
114
121
|
monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
|
|
115
122
|
monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
|
|
116
123
|
monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
|
|
117
124
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
118
|
-
monkeypatch
|
|
125
|
+
_use_identity_token_file(monkeypatch, tmp_path)
|
|
119
126
|
monkeypatch.setenv("JOB_RUN_UID", "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")
|
|
120
127
|
monkeypatch.setenv("MAINSEQUENCE_ENDPOINT", "https://backend.example")
|
|
121
128
|
|
|
@@ -147,7 +154,7 @@ def test_logconf_runtime_credential_exchanges_before_startup_state_request(monke
|
|
|
147
154
|
"headers": {"Content-Type": "application/json"},
|
|
148
155
|
"json": {
|
|
149
156
|
"credential_id": "cred-id",
|
|
150
|
-
"
|
|
157
|
+
"workload_identity_token": "projected-token",
|
|
151
158
|
},
|
|
152
159
|
"timeout": 10.0,
|
|
153
160
|
"allow_redirects": False,
|
|
@@ -158,12 +165,12 @@ def test_logconf_runtime_credential_exchanges_before_startup_state_request(monke
|
|
|
158
165
|
assert get_calls[0]["headers"]["Authorization"] == "Bearer runtime-access"
|
|
159
166
|
|
|
160
167
|
|
|
161
|
-
def test_logconf_runtime_credential_retries_after_auth_failure(monkeypatch):
|
|
168
|
+
def test_logconf_runtime_credential_retries_after_auth_failure(monkeypatch, tmp_path):
|
|
162
169
|
monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "stale-runtime-access")
|
|
163
170
|
monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
|
|
164
171
|
monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
|
|
165
172
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
166
|
-
monkeypatch
|
|
173
|
+
_use_identity_token_file(monkeypatch, tmp_path)
|
|
167
174
|
monkeypatch.setenv("JOB_RUN_UID", "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")
|
|
168
175
|
monkeypatch.setenv("MAINSEQUENCE_ENDPOINT", "https://backend.example")
|
|
169
176
|
|
|
@@ -197,11 +204,11 @@ def test_logconf_runtime_credential_retries_after_auth_failure(monkeypatch):
|
|
|
197
204
|
assert os.environ["MAINSEQUENCE_ACCESS_TOKEN"] == "fresh-runtime-access"
|
|
198
205
|
|
|
199
206
|
|
|
200
|
-
def test_runtime_credential_provider_exchanges_and_writes_access_token(monkeypatch):
|
|
207
|
+
def test_runtime_credential_provider_exchanges_and_writes_access_token(monkeypatch, tmp_path):
|
|
201
208
|
monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
|
|
202
209
|
monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
|
|
203
210
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
204
|
-
monkeypatch
|
|
211
|
+
_use_identity_token_file(monkeypatch, tmp_path)
|
|
205
212
|
|
|
206
213
|
utils = _load_mainsequence_submodule("mainsequence.client.utils")
|
|
207
214
|
runtime_context_module = importlib.import_module("mainsequence.code_repository_context")
|
|
@@ -238,7 +245,7 @@ def test_runtime_credential_provider_exchanges_and_writes_access_token(monkeypat
|
|
|
238
245
|
assert headers["Authorization"] == "Bearer runtime-access"
|
|
239
246
|
assert calls[0]["json"] == {
|
|
240
247
|
"credential_id": "cred-id",
|
|
241
|
-
"
|
|
248
|
+
"workload_identity_token": "projected-token",
|
|
242
249
|
}
|
|
243
250
|
assert calls[0]["url"].endswith("/api/v1/runtime-credentials/token/")
|
|
244
251
|
assert calls[0]["headers"]["Content-Type"] == "application/json"
|
|
@@ -254,11 +261,11 @@ def test_runtime_credential_provider_exchanges_and_writes_access_token(monkeypat
|
|
|
254
261
|
]
|
|
255
262
|
|
|
256
263
|
|
|
257
|
-
def test_runtime_credential_provider_reuses_valid_exchanged_access_token(monkeypatch):
|
|
264
|
+
def test_runtime_credential_provider_reuses_valid_exchanged_access_token(monkeypatch, tmp_path):
|
|
258
265
|
monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
|
|
259
266
|
monkeypatch.delenv("MAINSEQUENCE_REFRESH_TOKEN", raising=False)
|
|
260
267
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
261
|
-
monkeypatch
|
|
268
|
+
_use_identity_token_file(monkeypatch, tmp_path)
|
|
262
269
|
|
|
263
270
|
utils = _load_mainsequence_submodule("mainsequence.client.utils")
|
|
264
271
|
calls = {"count": 0}
|
|
@@ -286,11 +293,11 @@ def test_runtime_credential_provider_reuses_valid_exchanged_access_token(monkeyp
|
|
|
286
293
|
|
|
287
294
|
|
|
288
295
|
def test_runtime_credential_provider_does_not_mutate_unsupported_identity_environment(
|
|
289
|
-
monkeypatch,
|
|
296
|
+
monkeypatch, tmp_path
|
|
290
297
|
):
|
|
291
298
|
monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
|
|
292
299
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
293
|
-
monkeypatch
|
|
300
|
+
_use_identity_token_file(monkeypatch, tmp_path)
|
|
294
301
|
monkeypatch.setenv(
|
|
295
302
|
_UNSUPPORTED_REPOSITORY_BRANCH_UID_ENV,
|
|
296
303
|
"caller-supplied-value-must-be-ignored",
|
|
@@ -325,11 +332,11 @@ def test_runtime_credential_provider_does_not_mutate_unsupported_identity_enviro
|
|
|
325
332
|
)
|
|
326
333
|
|
|
327
334
|
|
|
328
|
-
def test_runtime_credential_provider_force_refresh_exchanges_again(monkeypatch):
|
|
335
|
+
def test_runtime_credential_provider_force_refresh_exchanges_again(monkeypatch, tmp_path):
|
|
329
336
|
monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
|
|
330
337
|
monkeypatch.delenv("MAINSEQUENCE_REFRESH_TOKEN", raising=False)
|
|
331
338
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
332
|
-
monkeypatch
|
|
339
|
+
_use_identity_token_file(monkeypatch, tmp_path)
|
|
333
340
|
|
|
334
341
|
utils = _load_mainsequence_submodule("mainsequence.client.utils")
|
|
335
342
|
calls = {"count": 0}
|
|
@@ -355,11 +362,11 @@ def test_runtime_credential_provider_force_refresh_exchanges_again(monkeypatch):
|
|
|
355
362
|
assert calls["count"] == 2
|
|
356
363
|
|
|
357
364
|
|
|
358
|
-
def test_runtime_credential_provider_exchanges_near_expiry_access_token(monkeypatch):
|
|
365
|
+
def test_runtime_credential_provider_exchanges_near_expiry_access_token(monkeypatch, tmp_path):
|
|
359
366
|
monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", _jwt_with_exp(int(time.time()) + 10))
|
|
360
367
|
monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
|
|
361
368
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
362
|
-
monkeypatch
|
|
369
|
+
_use_identity_token_file(monkeypatch, tmp_path)
|
|
363
370
|
|
|
364
371
|
utils = _load_mainsequence_submodule("mainsequence.client.utils")
|
|
365
372
|
calls: list[dict] = []
|
|
@@ -386,12 +393,12 @@ def test_runtime_credential_provider_exchanges_near_expiry_access_token(monkeypa
|
|
|
386
393
|
assert len(calls) == 1
|
|
387
394
|
|
|
388
395
|
|
|
389
|
-
def test_runtime_credential_make_request_401_forces_exchange_and_retry(monkeypatch):
|
|
396
|
+
def test_runtime_credential_make_request_401_forces_exchange_and_retry(monkeypatch, tmp_path):
|
|
390
397
|
monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
|
|
391
398
|
monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "stale-runtime-access")
|
|
392
399
|
monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "must-not-be-used")
|
|
393
400
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
394
|
-
monkeypatch
|
|
401
|
+
_use_identity_token_file(monkeypatch, tmp_path)
|
|
395
402
|
|
|
396
403
|
utils = _load_mainsequence_submodule("mainsequence.client.utils")
|
|
397
404
|
post_calls: list[dict] = []
|
|
@@ -441,7 +448,7 @@ def test_runtime_credential_make_request_401_forces_exchange_and_retry(monkeypat
|
|
|
441
448
|
assert len(post_calls) == 1
|
|
442
449
|
assert post_calls[0]["json"] == {
|
|
443
450
|
"credential_id": "cred-id",
|
|
444
|
-
"
|
|
451
|
+
"workload_identity_token": "projected-token",
|
|
445
452
|
}
|
|
446
453
|
assert os.environ["MAINSEQUENCE_ACCESS_TOKEN"] == "fresh-runtime-access"
|
|
447
454
|
assert os.environ["MAINSEQUENCE_REFRESH_TOKEN"] == "must-not-be-used"
|
|
@@ -451,7 +458,7 @@ def test_runtime_credential_provider_requires_credential_env(monkeypatch):
|
|
|
451
458
|
monkeypatch.delenv("MAINSEQUENCE_ACCESS_TOKEN", raising=False)
|
|
452
459
|
monkeypatch.delenv("MAINSEQUENCE_REFRESH_TOKEN", raising=False)
|
|
453
460
|
monkeypatch.delenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", raising=False)
|
|
454
|
-
monkeypatch.delenv("
|
|
461
|
+
monkeypatch.delenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", raising=False)
|
|
455
462
|
|
|
456
463
|
utils = _load_mainsequence_submodule("mainsequence.client.utils")
|
|
457
464
|
|
|
@@ -463,12 +470,12 @@ def test_runtime_credential_provider_requires_credential_env(monkeypatch):
|
|
|
463
470
|
assert "MAINSEQUENCE_RUNTIME_CREDENTIAL_ID is required" in str(exc)
|
|
464
471
|
|
|
465
472
|
|
|
466
|
-
def test_build_default_auth_provider_uses_runtime_credential_mode(monkeypatch):
|
|
473
|
+
def test_build_default_auth_provider_uses_runtime_credential_mode(monkeypatch, tmp_path):
|
|
467
474
|
monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
|
|
468
475
|
monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "jwt-access")
|
|
469
476
|
monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "jwt-refresh")
|
|
470
477
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
471
|
-
monkeypatch
|
|
478
|
+
token_file = _use_identity_token_file(monkeypatch, tmp_path)
|
|
472
479
|
|
|
473
480
|
utils = _load_mainsequence_submodule("mainsequence.client.utils")
|
|
474
481
|
|
|
@@ -476,15 +483,15 @@ def test_build_default_auth_provider_uses_runtime_credential_mode(monkeypatch):
|
|
|
476
483
|
|
|
477
484
|
assert isinstance(provider, utils.RuntimeCredentialAuthProvider)
|
|
478
485
|
assert provider.credential_id == "cred-id"
|
|
479
|
-
assert provider.
|
|
486
|
+
assert provider.identity_token_file == token_file
|
|
480
487
|
|
|
481
488
|
|
|
482
|
-
def test_auth_loaders_switch_to_runtime_credential_mode(monkeypatch):
|
|
489
|
+
def test_auth_loaders_switch_to_runtime_credential_mode(monkeypatch, tmp_path):
|
|
483
490
|
monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
|
|
484
491
|
monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", "jwt-access")
|
|
485
492
|
monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", "jwt-refresh")
|
|
486
493
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
487
|
-
monkeypatch
|
|
494
|
+
token_file = _use_identity_token_file(monkeypatch, tmp_path)
|
|
488
495
|
|
|
489
496
|
utils = _load_mainsequence_submodule("mainsequence.client.utils")
|
|
490
497
|
loaders = utils.AuthLoaders()
|
|
@@ -494,7 +501,7 @@ def test_auth_loaders_switch_to_runtime_credential_mode(monkeypatch):
|
|
|
494
501
|
|
|
495
502
|
assert isinstance(provider, utils.RuntimeCredentialAuthProvider)
|
|
496
503
|
assert provider.credential_id == "cred-id"
|
|
497
|
-
assert provider.
|
|
504
|
+
assert provider.identity_token_file == token_file
|
|
498
505
|
|
|
499
506
|
|
|
500
507
|
def test_build_default_auth_provider_defaults_to_jwt_when_mode_unset(monkeypatch):
|
{mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_canonical_api_route_contract.py
RENAMED
|
@@ -107,10 +107,7 @@ def test_client_auth_provider_routes_match_backend_contract(monkeypatch):
|
|
|
107
107
|
access_token="access",
|
|
108
108
|
refresh_token="refresh",
|
|
109
109
|
)
|
|
110
|
-
runtime_provider = utils.RuntimeCredentialAuthProvider(
|
|
111
|
-
credential_id="credential-id",
|
|
112
|
-
credential_secret="credential-secret",
|
|
113
|
-
)
|
|
110
|
+
runtime_provider = utils.RuntimeCredentialAuthProvider(credential_id="credential-id")
|
|
114
111
|
|
|
115
112
|
assert jwt_provider.obtain_url == "https://backend.example/auth/jwt-token/token/"
|
|
116
113
|
assert jwt_provider.refresh_url == "https://backend.example/auth/jwt-token/token/refresh/"
|
|
@@ -1011,30 +1011,28 @@ def test_session_report_judges_the_session_by_its_refresh_token(monkeypatch, tmp
|
|
|
1011
1011
|
def test_session_report_for_a_runtime_credential_has_no_session_expiry(monkeypatch, tmp_path):
|
|
1012
1012
|
_isolate_auth(monkeypatch, tmp_path, None)
|
|
1013
1013
|
monkeypatch.setattr(config, "auth_persistence_label", lambda: "test store")
|
|
1014
|
+
token_file = tmp_path / "token"
|
|
1015
|
+
token_file.write_text("projected-token", encoding="utf-8")
|
|
1014
1016
|
monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
|
|
1015
1017
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
1016
|
-
monkeypatch.setenv("
|
|
1018
|
+
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", str(token_file))
|
|
1017
1019
|
|
|
1018
1020
|
report = config.session_report()
|
|
1019
1021
|
|
|
1020
1022
|
assert report["authenticated"] is True
|
|
1021
1023
|
assert report["session_expires_at"] is None
|
|
1022
1024
|
assert report["auth_mode"] == "runtime_credential"
|
|
1023
|
-
assert "
|
|
1025
|
+
assert "projected-token" not in json.dumps(report)
|
|
1024
1026
|
|
|
1025
1027
|
|
|
1026
|
-
def
|
|
1028
|
+
def test_session_report_needs_the_runtime_identity_token_file(monkeypatch, tmp_path):
|
|
1027
1029
|
_isolate_auth(monkeypatch, tmp_path, None)
|
|
1028
1030
|
monkeypatch.setattr(config, "auth_persistence_label", lambda: "test store")
|
|
1029
|
-
token_file = tmp_path / "token"
|
|
1030
|
-
token_file.write_text("projected-token", encoding="utf-8")
|
|
1031
1031
|
monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
|
|
1032
1032
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
1033
|
-
monkeypatch.delenv("
|
|
1034
|
-
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", str(token_file))
|
|
1033
|
+
monkeypatch.delenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", raising=False)
|
|
1035
1034
|
|
|
1036
1035
|
report = config.session_report()
|
|
1037
1036
|
|
|
1038
|
-
assert report["authenticated"] is
|
|
1039
|
-
assert report["
|
|
1040
|
-
assert "projected-token" not in json.dumps(report)
|
|
1037
|
+
assert report["authenticated"] is False
|
|
1038
|
+
assert report["auth_mode"] == "runtime_credential"
|
{mainsequence-9.0.12.dev51 → mainsequence-9.0.13}/tests/test_code_repository_runtime_context.py
RENAMED
|
@@ -368,21 +368,12 @@ def test_authenticated_runtime_context_detects_git_drift(monkeypatch):
|
|
|
368
368
|
code_repository_context.validate_code_repository_source_context(context=context)
|
|
369
369
|
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
("proof_variable", "proof_value"),
|
|
373
|
-
[
|
|
374
|
-
("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", "credential-secret"),
|
|
375
|
-
("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", "/var/run/secrets/runtime-identity/token"),
|
|
376
|
-
],
|
|
377
|
-
)
|
|
378
|
-
def test_configured_runtime_credential_exchanges_context_before_resolution(
|
|
379
|
-
monkeypatch, proof_variable, proof_value
|
|
380
|
-
):
|
|
371
|
+
def test_configured_runtime_credential_exchanges_context_before_resolution(monkeypatch):
|
|
381
372
|
monkeypatch.setenv("MAINSEQUENCE_AUTH_MODE", "runtime_credential")
|
|
382
373
|
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "credential-id")
|
|
383
|
-
monkeypatch.
|
|
384
|
-
|
|
385
|
-
|
|
374
|
+
monkeypatch.setenv(
|
|
375
|
+
"MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", "/var/run/secrets/runtime-identity/token"
|
|
376
|
+
)
|
|
386
377
|
events = []
|
|
387
378
|
monkeypatch.setattr(
|
|
388
379
|
code_repository_context,
|
|
@@ -15,21 +15,15 @@ from mainsequence.cli.browser_auth import BrowserAuthCallback
|
|
|
15
15
|
from mainsequence.client.agent_runtime_models import AgentSessionRuntimeAccess
|
|
16
16
|
from mainsequence.client.models_foundry import Secret
|
|
17
17
|
from mainsequence.client.models_helpers import ResourceReleaseRuntimeAccess
|
|
18
|
-
from mainsequence.client.utils import
|
|
19
|
-
JWTAuthProvider,
|
|
20
|
-
RuntimeCredentialAuthProvider,
|
|
21
|
-
SessionJWTAuthProvider,
|
|
22
|
-
)
|
|
18
|
+
from mainsequence.client.utils import JWTAuthProvider, SessionJWTAuthProvider
|
|
23
19
|
from tests.client.support import _ready_runtime_contract, _runtime_access_payload
|
|
24
20
|
|
|
25
|
-
BOOTSTRAP_SECRET = "bootstrap-secret-7c1d"
|
|
26
21
|
ACCESS_TOKEN = "access-token-5e2a"
|
|
27
22
|
REFRESH_TOKEN = "refresh-token-9b4f"
|
|
28
23
|
GATEWAY_TOKEN = "gateway-token-3a8e"
|
|
29
24
|
RELEASE_TOKEN = "release-token-6d0c"
|
|
30
25
|
AUTHORIZATION_CODE = "authorization-code-2f7b"
|
|
31
26
|
SECRET_VALUE = "secret-value-8e5a"
|
|
32
|
-
TOKEN_URL = "https://backend.example/api/v1/runtime-credentials/token/"
|
|
33
27
|
|
|
34
28
|
|
|
35
29
|
def _shown(obj, caplog) -> str:
|
|
@@ -44,26 +38,6 @@ def _error_text(raised: pytest.ExceptionInfo) -> str:
|
|
|
44
38
|
return f"{raised.value}\n{raised.value!r}"
|
|
45
39
|
|
|
46
40
|
|
|
47
|
-
@pytest.mark.parametrize("source", ["environment", "argument"])
|
|
48
|
-
def test_runtime_credential_provider_leaves_the_bootstrap_secret_out(monkeypatch, caplog, source):
|
|
49
|
-
monkeypatch.delenv("MAINSEQUENCE_RUNTIME_IDENTITY_TOKEN_FILE", raising=False)
|
|
50
|
-
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_ID", "cred-id")
|
|
51
|
-
if source == "environment":
|
|
52
|
-
monkeypatch.setenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", BOOTSTRAP_SECRET)
|
|
53
|
-
provider = RuntimeCredentialAuthProvider(token_url=TOKEN_URL)
|
|
54
|
-
else:
|
|
55
|
-
monkeypatch.delenv("MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET", raising=False)
|
|
56
|
-
provider = RuntimeCredentialAuthProvider(
|
|
57
|
-
credential_secret=BOOTSTRAP_SECRET, token_url=TOKEN_URL
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
shown = _shown(provider, caplog)
|
|
61
|
-
|
|
62
|
-
assert provider.credential_secret == BOOTSTRAP_SECRET
|
|
63
|
-
assert BOOTSTRAP_SECRET not in shown
|
|
64
|
-
assert "credential_id='cred-id'" in shown
|
|
65
|
-
|
|
66
|
-
|
|
67
41
|
def test_jwt_provider_leaves_its_tokens_out(monkeypatch, caplog):
|
|
68
42
|
monkeypatch.setenv("MAINSEQUENCE_ACCESS_TOKEN", ACCESS_TOKEN)
|
|
69
43
|
monkeypatch.setenv("MAINSEQUENCE_REFRESH_TOKEN", REFRESH_TOKEN)
|