mainsequence 9.0.26__tar.gz → 9.0.27.dev71__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.26/mainsequence.egg-info → mainsequence-9.0.27.dev71}/PKG-INFO +1 -1
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/application_surfaces/api_surfaces/SKILL.md +2 -4
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/platform_operations/access_control_and_sharing/SKILL.md +4 -4
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/models_user.py +1 -44
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/caller_assertions.py +10 -35
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/fastapi.py +0 -2
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71/mainsequence.egg-info}/PKG-INFO +1 -1
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/pyproject.toml +1 -1
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_caller_assertions.py +32 -85
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_fastapi_mcp.py +6 -4
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_fastapi_request_identity.py +26 -36
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_models_user_request_bound_auth.py +1 -5
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_team_memberships.py +7 -18
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/LICENSE +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/README.md +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/mainsequence-inference/SKILL.md +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/maintenance/bug_auditor/SKILL.md +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/maintenance/code_repository_maintenance/SKILL.md +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/maintenance/metatables_transition/SKILL.md +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/platform_operations/orchestration_and_releases/SKILL.md +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/sdk_code_repository_execution/SKILL.md +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/__init__.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/__main__.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/_request_identity.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/_team_memberships.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/_transport.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/bootstrap.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/__init__.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/api.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/browser_auth.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/cli.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/code_repository_status.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/config.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/docker_utils.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/doctor.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/local_ops.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/model_filters.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/pydantic_cli.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/sdk_utils.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/ssh_utils.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/ui.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/__init__.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/agent_runtime_models.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/base.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/client.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/compute_validation.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/exceptions.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/github_issues.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/inference.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/models_foundry.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/models_helpers.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/observability.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/utils.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/value_sets.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/code_repository_context.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/code_repository_skills.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/defaults.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/instrumentation/__init__.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/instrumentation/utils.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/logconf.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/repository_identity_security.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/runtime_credential_exchange.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/runtime_flags.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/scaffold_skills.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/__init__.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/_mcp_skills_upstream/__init__.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/_mcp_skills_upstream/server.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/_mcp_skills_upstream/shared.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/mcp_skills.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence.egg-info/SOURCES.txt +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence.egg-info/dependency_links.txt +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence.egg-info/entry_points.txt +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence.egg-info/requires.txt +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence.egg-info/top_level.txt +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/setup.cfg +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_auth_bootstrap.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_auth_precedence.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_canonical_api_route_contract.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_cli_auth_contract.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_cli_browser_auth.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_cli_request_budget.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_cli_secure_storage.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_client.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_code_repository_branch_contract.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_code_repository_documentation_contract.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_code_repository_runtime_context.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_code_repository_skills.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_credential_repr.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_dependency_extras.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_dev_release_version.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_environment_resource_context.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_environment_response_contracts.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_github_issues_client.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_http_retry_budget.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_independent_context.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_inference_client.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_instrumentation.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_job_client.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_local_ops.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_logconf.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_mcp_skills.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_notification_uid_contract.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_observability_client.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_runtime_credential_exchange.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_scaffold_skills.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_secret_client_model.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_source_ownership.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_ssh_utils.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_tolerant_response_reading.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_tolerant_value_sets.py +0 -0
- {mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_user_application_facts.py +0 -0
|
@@ -199,10 +199,8 @@ def publish_report(team_uid: str) -> dict[str, str]:
|
|
|
199
199
|
Call it from a `def` handler, or from an `async def` handler with
|
|
200
200
|
`await asyncio.to_thread(caller.in_teams, team_uids)`, which keeps the
|
|
201
201
|
request's identity.
|
|
202
|
-
- `team_uids
|
|
203
|
-
|
|
204
|
-
team list, as every assertion will once the platform stops signing them.
|
|
205
|
-
Replace every check that reads it with `in_teams()`.
|
|
202
|
+
- The identity has no `team_uids`; the platform signs no team list. Code
|
|
203
|
+
written for releases that had it reads `in_teams()` instead.
|
|
206
204
|
- Membership is not team administration and not a grant on an object: check
|
|
207
205
|
the exact object and action as well.
|
|
208
206
|
|
|
@@ -137,7 +137,7 @@ Keep these separate:
|
|
|
137
137
|
- administration of the team itself
|
|
138
138
|
|
|
139
139
|
An application that checks whether its caller or requester belongs to a team
|
|
140
|
-
uses `in_teams()
|
|
140
|
+
uses `in_teams()`; the identity carries no team list. See
|
|
141
141
|
`.agents/skills/mainsequence/application_surfaces/api_surfaces/SKILL.md`.
|
|
142
142
|
|
|
143
143
|
### 5. `Constant` vs `Secret` is a security boundary
|
|
@@ -308,9 +308,9 @@ When reviewing an access-control task, look for:
|
|
|
308
308
|
- using direct user grants where a team should be used
|
|
309
309
|
- using a team when the access is clearly one-off
|
|
310
310
|
- treating team membership as team administration
|
|
311
|
-
- team checks that
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
- team checks that do not use `in_teams()`, that read an `in_teams()` error
|
|
312
|
+
as "not a member", or that reuse an older answer (no `max_age_seconds=0`)
|
|
313
|
+
before a write that must see a removal
|
|
314
314
|
- storing sensitive data in a `Constant`
|
|
315
315
|
- creating a `Constant` or `Secret` blindly without resolving whether the name already exists
|
|
316
316
|
- weak or unverified claims about who can access a resource
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
4
|
import math
|
|
5
|
-
import warnings
|
|
6
5
|
from collections.abc import Iterable
|
|
7
6
|
from typing import Any, ClassVar, Literal
|
|
8
7
|
from uuid import UUID
|
|
@@ -43,7 +42,7 @@ class RequestUserIdentity(BaseModel):
|
|
|
43
42
|
``in_teams()`` asks the platform which teams either of them belongs to.
|
|
44
43
|
"""
|
|
45
44
|
|
|
46
|
-
model_config = ConfigDict(extra="forbid", frozen=True
|
|
45
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
47
46
|
|
|
48
47
|
uid: str = Field(
|
|
49
48
|
...,
|
|
@@ -58,17 +57,6 @@ class RequestUserIdentity(BaseModel):
|
|
|
58
57
|
title="Username",
|
|
59
58
|
description="Optional display name authenticated by the runtime gateway.",
|
|
60
59
|
)
|
|
61
|
-
signed_team_uids: tuple[str, ...] | None = Field(
|
|
62
|
-
(),
|
|
63
|
-
alias="team_uids",
|
|
64
|
-
title="Signed team UIDs (deprecated)",
|
|
65
|
-
description=(
|
|
66
|
-
"Canonical UIDs of the active teams the platform signed in the caller "
|
|
67
|
-
"assertion, None when it signed none. Deprecated: read membership with "
|
|
68
|
-
"in_teams() (#225). Empty in local mode and on WebSockets, which carry no "
|
|
69
|
-
"such facts."
|
|
70
|
-
),
|
|
71
|
-
)
|
|
72
60
|
is_organization_admin: bool = Field(
|
|
73
61
|
False,
|
|
74
62
|
title="Organization admin",
|
|
@@ -87,37 +75,6 @@ class RequestUserIdentity(BaseModel):
|
|
|
87
75
|
except (AttributeError, TypeError, ValueError) as exc:
|
|
88
76
|
raise ValueError("Request user UID must be a valid UUID.") from exc
|
|
89
77
|
|
|
90
|
-
@field_validator("signed_team_uids", mode="before")
|
|
91
|
-
@classmethod
|
|
92
|
-
def _canonicalize_team_uids(cls, value: Any) -> tuple[str, ...] | None:
|
|
93
|
-
if value is None:
|
|
94
|
-
return None
|
|
95
|
-
try:
|
|
96
|
-
return tuple(str(UUID(str(item).strip())) for item in value)
|
|
97
|
-
except (AttributeError, TypeError, ValueError) as exc:
|
|
98
|
-
raise ValueError("Team UIDs must be valid UUIDs.") from exc
|
|
99
|
-
|
|
100
|
-
@property
|
|
101
|
-
def team_uids(self) -> tuple[str, ...]:
|
|
102
|
-
"""Deprecated: the team UIDs the platform signed. Use ``in_teams()``.
|
|
103
|
-
|
|
104
|
-
Warns when read. When the caller assertion carried no team list it
|
|
105
|
-
raises ``RequestIdentityError``; it never answers an empty list for
|
|
106
|
-
that. Empty in local mode and on WebSockets, as before (#225).
|
|
107
|
-
"""
|
|
108
|
-
warnings.warn(
|
|
109
|
-
"RequestUserIdentity.team_uids is deprecated; check membership with "
|
|
110
|
-
"in_teams() instead.",
|
|
111
|
-
DeprecationWarning,
|
|
112
|
-
stacklevel=2,
|
|
113
|
-
)
|
|
114
|
-
if self.signed_team_uids is None:
|
|
115
|
-
raise RequestIdentityError(
|
|
116
|
-
"The caller assertion carries no team list. Check team membership "
|
|
117
|
-
"with in_teams()."
|
|
118
|
-
)
|
|
119
|
-
return self.signed_team_uids
|
|
120
|
-
|
|
121
78
|
def in_teams(
|
|
122
79
|
self, team_uids: Iterable[str | UUID], *, max_age_seconds: float | None = None
|
|
123
80
|
) -> frozenset[str]:
|
|
@@ -63,8 +63,6 @@ class _VerifiedRequester:
|
|
|
63
63
|
"""The person a requester-bound call works for, as the platform signed it."""
|
|
64
64
|
|
|
65
65
|
user_uid: str
|
|
66
|
-
# None when the assertion carries no team list (#225).
|
|
67
|
-
team_uids: tuple[str, ...] | None
|
|
68
66
|
is_organization_admin: bool
|
|
69
67
|
|
|
70
68
|
|
|
@@ -75,66 +73,44 @@ class AuthenticatedCaller:
|
|
|
75
73
|
environment_uid: str
|
|
76
74
|
issued_at: int
|
|
77
75
|
expires_at: int
|
|
78
|
-
# None when the assertion carries no team list (#225).
|
|
79
|
-
team_uids: tuple[str, ...] | None
|
|
80
76
|
is_organization_admin: bool
|
|
81
77
|
# The person a requester-bound call works for, None on any other call. The
|
|
82
78
|
# fields above stay the caller's: the acting application (ADR-0036).
|
|
83
79
|
_requester: _VerifiedRequester | None = None
|
|
84
80
|
|
|
85
81
|
|
|
86
|
-
def _team_uids(value: object, *, owner: str) -> tuple[str, ...]:
|
|
87
|
-
if not isinstance(value, list):
|
|
88
|
-
raise InvalidCallerAssertion(f"{owner} team UIDs are invalid.")
|
|
89
|
-
team_uids = tuple(_canonical_uid(item) for item in value)
|
|
90
|
-
if list(team_uids) != sorted(set(team_uids)):
|
|
91
|
-
raise InvalidCallerAssertion(f"{owner} team UIDs must be sorted and unique.")
|
|
92
|
-
return team_uids
|
|
93
|
-
|
|
94
|
-
|
|
95
82
|
def _admin_flag(value: object, *, owner: str) -> bool:
|
|
96
83
|
if type(value) is not bool:
|
|
97
84
|
raise InvalidCallerAssertion(f"{owner} admin flag is invalid.")
|
|
98
85
|
return value
|
|
99
86
|
|
|
100
87
|
|
|
101
|
-
def
|
|
102
|
-
"""The
|
|
88
|
+
def _caller_admin_flag(payload: dict) -> bool:
|
|
89
|
+
"""The caller's admin flag, after checking the exact claim set.
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
(#225)
|
|
91
|
+
The assertion names no teams: membership is checked on demand with
|
|
92
|
+
``RequestUserIdentity.in_teams()`` (#225), and an assertion that lists
|
|
93
|
+
teams is refused.
|
|
106
94
|
"""
|
|
107
|
-
if "team_uids" not in claims:
|
|
108
|
-
return None
|
|
109
|
-
return _team_uids(claims["team_uids"], owner=owner)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _caller_facts(payload: dict) -> tuple[tuple[str, ...] | None, bool]:
|
|
113
|
-
"""The caller's admin flag, and its team UIDs while the platform signs them."""
|
|
114
95
|
# A requester-bound call also carries "requester", which _requester() checks.
|
|
115
|
-
if set(payload) - {"requester"
|
|
96
|
+
if set(payload) - {"requester"} != REQUIRED_CLAIMS:
|
|
116
97
|
raise InvalidCallerAssertion("Caller assertion claims are invalid.")
|
|
117
|
-
return (
|
|
118
|
-
_signed_team_uids(payload, owner="Caller"),
|
|
119
|
-
_admin_flag(payload["is_organization_admin"], owner="Caller"),
|
|
120
|
-
)
|
|
98
|
+
return _admin_flag(payload["is_organization_admin"], owner="Caller")
|
|
121
99
|
|
|
122
100
|
|
|
123
101
|
def _requester(payload: dict) -> _VerifiedRequester | None:
|
|
124
102
|
"""The person a requester-bound call works for, or None on any other call.
|
|
125
103
|
|
|
126
104
|
The claim has exactly ``sub`` and ``is_organization_admin``, the person's
|
|
127
|
-
own User UID and Organization-admin flag
|
|
128
|
-
deprecated ``team_uids`` (#225).
|
|
105
|
+
own User UID and Organization-admin flag.
|
|
129
106
|
"""
|
|
130
107
|
if "requester" not in payload:
|
|
131
108
|
return None
|
|
132
109
|
claim = payload["requester"]
|
|
133
|
-
if not isinstance(claim, dict) or set(claim)
|
|
110
|
+
if not isinstance(claim, dict) or set(claim) != REQUESTER_CLAIMS:
|
|
134
111
|
raise InvalidCallerAssertion("Caller assertion requester is invalid.")
|
|
135
112
|
return _VerifiedRequester(
|
|
136
113
|
user_uid=_canonical_uid(claim["sub"]),
|
|
137
|
-
team_uids=_signed_team_uids(claim, owner="Requester"),
|
|
138
114
|
is_organization_admin=_admin_flag(claim["is_organization_admin"], owner="Requester"),
|
|
139
115
|
)
|
|
140
116
|
|
|
@@ -284,7 +260,7 @@ class CallerAssertionVerifier:
|
|
|
284
260
|
audience=f"urn:mainsequence:fapi:{self.release_uid}",
|
|
285
261
|
options={"require": list(REQUIRED_CLAIMS)},
|
|
286
262
|
)
|
|
287
|
-
|
|
263
|
+
is_organization_admin = _caller_admin_flag(payload)
|
|
288
264
|
requester = _requester(payload)
|
|
289
265
|
if (
|
|
290
266
|
payload["aud"] != f"urn:mainsequence:fapi:{self.release_uid}"
|
|
@@ -304,7 +280,6 @@ class CallerAssertionVerifier:
|
|
|
304
280
|
environment_uid=self.environment_uid,
|
|
305
281
|
issued_at=iat,
|
|
306
282
|
expires_at=exp,
|
|
307
|
-
team_uids=team_uids,
|
|
308
283
|
is_organization_admin=is_organization_admin,
|
|
309
284
|
_requester=requester,
|
|
310
285
|
)
|
|
@@ -229,7 +229,6 @@ class _RequestIdentityMiddleware:
|
|
|
229
229
|
caller = await asyncio.to_thread(self.verifier.verify, assertion)
|
|
230
230
|
user = RequestUserIdentity(
|
|
231
231
|
uid=caller.user_uid,
|
|
232
|
-
team_uids=caller.team_uids,
|
|
233
232
|
is_organization_admin=caller.is_organization_admin,
|
|
234
233
|
)
|
|
235
234
|
state["resource_release_uid"] = caller.release_uid
|
|
@@ -244,7 +243,6 @@ class _RequestIdentityMiddleware:
|
|
|
244
243
|
# The person this call works for, with the person's own facts.
|
|
245
244
|
context.requester = RequestUserIdentity(
|
|
246
245
|
uid=caller._requester.user_uid,
|
|
247
|
-
team_uids=caller._requester.team_uids,
|
|
248
246
|
is_organization_admin=caller._requester.is_organization_admin,
|
|
249
247
|
)
|
|
250
248
|
else:
|
|
@@ -55,7 +55,6 @@ def token(key, *, kid="first", headers=None, omit=(), **changes):
|
|
|
55
55
|
"iat": now,
|
|
56
56
|
"nbf": now,
|
|
57
57
|
"exp": now + 120,
|
|
58
|
-
"team_uids": [],
|
|
59
58
|
"is_organization_admin": False,
|
|
60
59
|
}
|
|
61
60
|
claims.update(changes)
|
|
@@ -79,57 +78,43 @@ def verifier(fetch):
|
|
|
79
78
|
TEAMS = sorted([str(uuid4()), str(uuid4())])
|
|
80
79
|
|
|
81
80
|
|
|
82
|
-
def
|
|
81
|
+
def test_exposes_the_callers_admin_flag_and_names_no_teams(keys):
|
|
83
82
|
key, public = keys[0]
|
|
84
83
|
check = verifier(lambda: {"keys": [public]})
|
|
85
84
|
|
|
86
|
-
caller = check.verify(token(key,
|
|
85
|
+
caller = check.verify(token(key, is_organization_admin=True))
|
|
87
86
|
|
|
88
|
-
assert caller.team_uids == tuple(TEAMS)
|
|
89
87
|
assert caller.is_organization_admin is True
|
|
88
|
+
# Membership is checked on demand with in_teams() (#225).
|
|
89
|
+
assert not hasattr(caller, "team_uids")
|
|
90
90
|
|
|
91
91
|
|
|
92
|
-
def
|
|
92
|
+
def test_a_caller_who_is_not_an_admin(keys):
|
|
93
93
|
key, public = keys[0]
|
|
94
94
|
|
|
95
95
|
caller = verifier(lambda: {"keys": [public]}).verify(token(key))
|
|
96
96
|
|
|
97
|
-
assert caller.team_uids == ()
|
|
98
97
|
assert caller.is_organization_admin is False
|
|
99
98
|
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
"omit",
|
|
103
|
-
[("team_uids", "is_organization_admin"), ("is_organization_admin",)],
|
|
104
|
-
ids=["no_facts", "no_admin_flag"],
|
|
105
|
-
)
|
|
106
|
-
def test_rejects_an_assertion_without_the_callers_admin_flag(keys, omit):
|
|
100
|
+
def test_rejects_an_assertion_without_the_callers_admin_flag(keys):
|
|
107
101
|
"""Every assertion states the caller's admin flag; there is no older shape."""
|
|
108
102
|
key, public = keys[0]
|
|
109
103
|
with pytest.raises(InvalidCallerAssertion):
|
|
110
|
-
verifier(lambda: {"keys": [public]}).verify(token(key, omit=
|
|
104
|
+
verifier(lambda: {"keys": [public]}).verify(token(key, omit=("is_organization_admin",)))
|
|
111
105
|
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
@pytest.mark.parametrize("team_uids", [TEAMS, [], None], ids=["team_list", "empty_list", "null"])
|
|
108
|
+
def test_rejects_an_assertion_that_lists_teams(keys, team_uids):
|
|
109
|
+
"""The platform signs no team list; an assertion that carries one is refused (#225)."""
|
|
115
110
|
key, public = keys[0]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
token(key, omit=("team_uids",), is_organization_admin=True)
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
assert caller.team_uids is None
|
|
122
|
-
assert caller.is_organization_admin is True
|
|
111
|
+
with pytest.raises(InvalidCallerAssertion):
|
|
112
|
+
verifier(lambda: {"keys": [public]}).verify(token(key, team_uids=team_uids))
|
|
123
113
|
|
|
124
114
|
|
|
125
115
|
@pytest.mark.parametrize(
|
|
126
116
|
"changes",
|
|
127
117
|
[
|
|
128
|
-
{"team_uids": None},
|
|
129
|
-
{"team_uids": TEAMS[0]},
|
|
130
|
-
{"team_uids": list(reversed(TEAMS))},
|
|
131
|
-
{"team_uids": [TEAMS[0], TEAMS[0]]},
|
|
132
|
-
{"team_uids": [TEAMS[0].upper()]},
|
|
133
118
|
{"is_organization_admin": None},
|
|
134
119
|
{"is_organization_admin": "true"},
|
|
135
120
|
{"is_organization_admin": 1},
|
|
@@ -146,11 +131,11 @@ def test_rejects_malformed_caller_facts(keys, changes):
|
|
|
146
131
|
#
|
|
147
132
|
# Another application calls while it works for a person, the requester. The
|
|
148
133
|
# assertion's `sub` and facts stay the caller's, the acting application, and
|
|
149
|
-
# `requester` names the person with the person's own facts: exactly `sub
|
|
150
|
-
# `
|
|
134
|
+
# `requester` names the person with the person's own facts: exactly `sub` and
|
|
135
|
+
# `is_organization_admin`.
|
|
151
136
|
|
|
152
137
|
PERSON = "5b0f9a8e-3c2d-4e1f-9a7b-6c5d4e3f2a1b"
|
|
153
|
-
REQUESTER = {"sub": PERSON, "
|
|
138
|
+
REQUESTER = {"sub": PERSON, "is_organization_admin": True}
|
|
154
139
|
|
|
155
140
|
|
|
156
141
|
@pytest.mark.parametrize(
|
|
@@ -160,45 +145,32 @@ REQUESTER = {"sub": PERSON, "team_uids": TEAMS, "is_organization_admin": True}
|
|
|
160
145
|
)
|
|
161
146
|
def test_exposes_the_requester_beside_the_acting_caller(keys, caller_admin, person_admin):
|
|
162
147
|
key, public = keys[0]
|
|
163
|
-
caller_teams = [str(uuid4())]
|
|
164
148
|
check = verifier(lambda: {"keys": [public]})
|
|
165
149
|
|
|
166
150
|
caller = check.verify(
|
|
167
151
|
token(
|
|
168
152
|
key,
|
|
169
|
-
team_uids=caller_teams,
|
|
170
153
|
is_organization_admin=caller_admin,
|
|
171
154
|
requester={**REQUESTER, "is_organization_admin": person_admin},
|
|
172
155
|
)
|
|
173
156
|
)
|
|
174
157
|
|
|
175
158
|
# Each identity keeps its own facts: neither admin flag reaches the other.
|
|
176
|
-
assert (caller.user_uid, caller.
|
|
177
|
-
USER,
|
|
178
|
-
tuple(caller_teams),
|
|
179
|
-
caller_admin,
|
|
180
|
-
)
|
|
159
|
+
assert (caller.user_uid, caller.is_organization_admin) == (USER, caller_admin)
|
|
181
160
|
requester = caller._requester
|
|
182
|
-
assert (requester.user_uid, requester.
|
|
183
|
-
|
|
184
|
-
tuple(TEAMS),
|
|
185
|
-
person_admin,
|
|
186
|
-
)
|
|
161
|
+
assert (requester.user_uid, requester.is_organization_admin) == (PERSON, person_admin)
|
|
162
|
+
assert not hasattr(requester, "team_uids")
|
|
187
163
|
|
|
188
164
|
|
|
189
|
-
def
|
|
165
|
+
def test_a_requester_who_is_not_an_admin(keys):
|
|
190
166
|
key, public = keys[0]
|
|
191
167
|
|
|
192
168
|
caller = verifier(lambda: {"keys": [public]}).verify(
|
|
193
|
-
token(key, requester={"sub": PERSON, "
|
|
169
|
+
token(key, requester={"sub": PERSON, "is_organization_admin": False})
|
|
194
170
|
)
|
|
195
171
|
|
|
196
172
|
requester = caller._requester
|
|
197
|
-
assert (requester.user_uid, requester.
|
|
198
|
-
PERSON,
|
|
199
|
-
(),
|
|
200
|
-
False,
|
|
201
|
-
)
|
|
173
|
+
assert (requester.user_uid, requester.is_organization_admin) == (PERSON, False)
|
|
202
174
|
|
|
203
175
|
|
|
204
176
|
def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
|
|
@@ -206,7 +178,7 @@ def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
|
|
|
206
178
|
check = verifier(lambda: {"keys": [public]})
|
|
207
179
|
|
|
208
180
|
assert check.verify(token(key))._requester is None
|
|
209
|
-
assert check.verify(token(key,
|
|
181
|
+
assert check.verify(token(key, is_organization_admin=True))._requester is None
|
|
210
182
|
|
|
211
183
|
|
|
212
184
|
@pytest.mark.parametrize(
|
|
@@ -214,12 +186,14 @@ def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
|
|
|
214
186
|
[
|
|
215
187
|
None,
|
|
216
188
|
PERSON,
|
|
217
|
-
[PERSON,
|
|
189
|
+
[PERSON, True],
|
|
218
190
|
{},
|
|
219
|
-
{"sub": PERSON
|
|
220
|
-
{"
|
|
191
|
+
{"sub": PERSON},
|
|
192
|
+
{"is_organization_admin": True},
|
|
221
193
|
{**REQUESTER, "username": "person"},
|
|
222
194
|
{**REQUESTER, "requester": {"sub": PERSON}},
|
|
195
|
+
{**REQUESTER, "team_uids": TEAMS},
|
|
196
|
+
{**REQUESTER, "team_uids": []},
|
|
223
197
|
{**REQUESTER, "is_organization_admin": None},
|
|
224
198
|
{**REQUESTER, "is_organization_admin": 1},
|
|
225
199
|
{**REQUESTER, "is_organization_admin": 0},
|
|
@@ -230,22 +204,18 @@ def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
|
|
|
230
204
|
{**REQUESTER, "sub": "not-a-uuid"},
|
|
231
205
|
{**REQUESTER, "sub": None},
|
|
232
206
|
{**REQUESTER, "sub": 1},
|
|
233
|
-
{**REQUESTER, "team_uids": None},
|
|
234
|
-
{**REQUESTER, "team_uids": TEAMS[0]},
|
|
235
|
-
{**REQUESTER, "team_uids": [1]},
|
|
236
|
-
{**REQUESTER, "team_uids": [TEAMS[0].upper()]},
|
|
237
|
-
{**REQUESTER, "team_uids": list(reversed(TEAMS))},
|
|
238
|
-
{**REQUESTER, "team_uids": [TEAMS[0], TEAMS[0]]},
|
|
239
207
|
],
|
|
240
208
|
ids=[
|
|
241
209
|
"null",
|
|
242
210
|
"string",
|
|
243
211
|
"list",
|
|
244
212
|
"empty",
|
|
245
|
-
"
|
|
213
|
+
"missing_admin_flag",
|
|
246
214
|
"missing_sub",
|
|
247
215
|
"extra_key",
|
|
248
216
|
"nested_requester",
|
|
217
|
+
"team_list",
|
|
218
|
+
"empty_team_list",
|
|
249
219
|
"null_admin_flag",
|
|
250
220
|
"integer_true_admin_flag",
|
|
251
221
|
"integer_false_admin_flag",
|
|
@@ -256,12 +226,6 @@ def test_a_call_that_is_not_requester_bound_has_no_requester(keys):
|
|
|
256
226
|
"invalid_sub",
|
|
257
227
|
"null_sub",
|
|
258
228
|
"integer_sub",
|
|
259
|
-
"null_team_uids",
|
|
260
|
-
"string_team_uids",
|
|
261
|
-
"integer_team_uid",
|
|
262
|
-
"uppercase_team_uid",
|
|
263
|
-
"unsorted_team_uids",
|
|
264
|
-
"duplicate_team_uids",
|
|
265
229
|
],
|
|
266
230
|
)
|
|
267
231
|
def test_rejects_a_malformed_requester(keys, requester):
|
|
@@ -270,35 +234,18 @@ def test_rejects_a_malformed_requester(keys, requester):
|
|
|
270
234
|
verifier(lambda: {"keys": [public]}).verify(token(key, requester=requester))
|
|
271
235
|
|
|
272
236
|
|
|
273
|
-
def test_a_requester_and_caller_without_team_lists_are_accepted(keys):
|
|
274
|
-
"""Neither the caller's facts nor the requester's need a team list (#225)."""
|
|
275
|
-
key, public = keys[0]
|
|
276
|
-
|
|
277
|
-
caller = verifier(lambda: {"keys": [public]}).verify(
|
|
278
|
-
token(
|
|
279
|
-
key,
|
|
280
|
-
omit=("team_uids",),
|
|
281
|
-
requester={"sub": PERSON, "is_organization_admin": True},
|
|
282
|
-
)
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
assert caller.team_uids is None
|
|
286
|
-
assert (caller._requester.user_uid, caller._requester.team_uids) == (PERSON, None)
|
|
287
|
-
assert caller._requester.is_organization_admin is True
|
|
288
|
-
|
|
289
|
-
|
|
290
237
|
@pytest.mark.parametrize(
|
|
291
238
|
("changes", "omit"),
|
|
292
239
|
[
|
|
293
240
|
({"extra": "not an identity claim"}, ()),
|
|
294
241
|
({"requester_is_organization_admin": True}, ()),
|
|
295
|
-
({
|
|
242
|
+
({"team_uids": TEAMS}, ()),
|
|
296
243
|
({}, ("is_organization_admin",)),
|
|
297
244
|
],
|
|
298
245
|
ids=[
|
|
299
246
|
"unknown_claim",
|
|
300
247
|
"requester_admin_claim",
|
|
301
|
-
"
|
|
248
|
+
"caller_team_list",
|
|
302
249
|
"no_caller_admin_flag",
|
|
303
250
|
],
|
|
304
251
|
)
|
|
@@ -254,14 +254,16 @@ def test_a_tool_with_an_optional_person_never_combines_grants(signed):
|
|
|
254
254
|
|
|
255
255
|
REJECTED_ASSERTIONS = {
|
|
256
256
|
"missing": None,
|
|
257
|
-
"
|
|
257
|
+
"requester_without_admin_flag": dict(requester={"sub": PERSON}),
|
|
258
|
+
"requester_with_a_team_list": dict(requester={**requester_claim(PERSON), "team_uids": []}),
|
|
258
259
|
"requester_extra_key": dict(requester={**requester_claim(PERSON), "username": "person"}),
|
|
259
260
|
"requester_integer_admin_flag": dict(
|
|
260
261
|
requester=requester_claim(PERSON, is_organization_admin=1)
|
|
261
262
|
),
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
|
|
263
|
+
"without_caller_admin_flag": dict(omit=("is_organization_admin",)),
|
|
264
|
+
"with_a_caller_team_list": dict(team_uids=[]),
|
|
265
|
+
"requester_bound_without_caller_admin_flag": dict(
|
|
266
|
+
requester=requester_claim(PERSON), omit=("is_organization_admin",)
|
|
265
267
|
),
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -69,7 +69,6 @@ def signed(monkeypatch):
|
|
|
69
69
|
iat=now,
|
|
70
70
|
nbf=now,
|
|
71
71
|
exp=now + 120,
|
|
72
|
-
team_uids=[],
|
|
73
72
|
is_organization_admin=False,
|
|
74
73
|
)
|
|
75
74
|
claims.update(changes)
|
|
@@ -114,21 +113,18 @@ def test_sync_getter_signed_proof_once_and_process_credentials_untouched(signed,
|
|
|
114
113
|
|
|
115
114
|
def test_signed_caller_facts_reach_the_logged_user(signed):
|
|
116
115
|
token, _, _ = signed
|
|
117
|
-
teams = sorted([str(uuid4()), str(uuid4())])
|
|
118
116
|
|
|
119
117
|
def handler():
|
|
120
118
|
user = User.get_logged_user()
|
|
121
|
-
return {"
|
|
119
|
+
return {"admin": user.is_organization_admin, "teams": hasattr(user, "team_uids")}
|
|
122
120
|
|
|
123
121
|
with TestClient(app_for(handler)) as client:
|
|
124
|
-
admin = client.get(
|
|
125
|
-
|
|
126
|
-
headers={ASSERTION_HEADER: token(team_uids=teams, is_organization_admin=True)},
|
|
127
|
-
)
|
|
128
|
-
member_of_none = client.get("/me", headers={ASSERTION_HEADER: token()})
|
|
122
|
+
admin = client.get("/me", headers={ASSERTION_HEADER: token(is_organization_admin=True)})
|
|
123
|
+
member = client.get("/me", headers={ASSERTION_HEADER: token()})
|
|
129
124
|
|
|
130
|
-
|
|
131
|
-
assert
|
|
125
|
+
# The identity names no teams: membership is checked with in_teams() (#225).
|
|
126
|
+
assert admin.json() == {"admin": True, "teams": False}
|
|
127
|
+
assert member.json() == {"admin": False, "teams": False}
|
|
132
128
|
|
|
133
129
|
|
|
134
130
|
@pytest.mark.parametrize(
|
|
@@ -139,7 +135,7 @@ def test_signed_caller_facts_reach_the_logged_user(signed):
|
|
|
139
135
|
"expired",
|
|
140
136
|
"wrong_target",
|
|
141
137
|
"duplicate",
|
|
142
|
-
"
|
|
138
|
+
"with_a_team_list",
|
|
143
139
|
"without_admin_flag",
|
|
144
140
|
],
|
|
145
141
|
)
|
|
@@ -164,8 +160,8 @@ def test_invalid_proof_never_reaches_route(signed, case, monkeypatch):
|
|
|
164
160
|
headers[ASSERTION_HEADER] = token(resource_release_uid=OTHER)
|
|
165
161
|
elif case == "duplicate":
|
|
166
162
|
headers = [(ASSERTION_HEADER, token()), (ASSERTION_HEADER, token())]
|
|
167
|
-
elif case == "
|
|
168
|
-
headers[ASSERTION_HEADER] = token(
|
|
163
|
+
elif case == "with_a_team_list":
|
|
164
|
+
headers[ASSERTION_HEADER] = token(team_uids=[])
|
|
169
165
|
elif case == "without_admin_flag":
|
|
170
166
|
headers[ASSERTION_HEADER] = token(omit=("is_organization_admin",))
|
|
171
167
|
assert TestClient(app_for(handler)).get("/me", headers=headers).status_code == 401
|
|
@@ -575,24 +571,18 @@ def test_reads_as_caller_needs_a_signed_http_request(monkeypatch):
|
|
|
575
571
|
PERSON = "5b0f9a8e-3c2d-4e1f-9a7b-6c5d4e3f2a1b"
|
|
576
572
|
|
|
577
573
|
|
|
578
|
-
def requester_claim(sub=PERSON,
|
|
574
|
+
def requester_claim(sub=PERSON, is_organization_admin=False):
|
|
579
575
|
"""The requester claim as the platform signs it."""
|
|
580
|
-
return {
|
|
581
|
-
"sub": sub,
|
|
582
|
-
"team_uids": list(team_uids),
|
|
583
|
-
"is_organization_admin": is_organization_admin,
|
|
584
|
-
}
|
|
576
|
+
return {"sub": sub, "is_organization_admin": is_organization_admin}
|
|
585
577
|
|
|
586
578
|
|
|
587
579
|
def _identities():
|
|
588
580
|
caller = User.get_logged_user()
|
|
589
581
|
requester = User.get_requester()
|
|
590
582
|
return {
|
|
591
|
-
"caller": [caller.uid,
|
|
583
|
+
"caller": [caller.uid, caller.is_organization_admin],
|
|
592
584
|
"requester": (
|
|
593
|
-
None
|
|
594
|
-
if requester is None
|
|
595
|
-
else [requester.uid, list(requester.team_uids), requester.is_organization_admin]
|
|
585
|
+
None if requester is None else [requester.uid, requester.is_organization_admin]
|
|
596
586
|
),
|
|
597
587
|
}
|
|
598
588
|
|
|
@@ -606,11 +596,9 @@ def test_requester_bound_call_binds_the_requester_beside_the_caller(
|
|
|
606
596
|
signed, caller_admin, person_admin
|
|
607
597
|
):
|
|
608
598
|
token, _, _ = signed
|
|
609
|
-
caller_teams = sorted([str(uuid4()), str(uuid4())])
|
|
610
|
-
requester_teams = sorted([str(uuid4()), str(uuid4())])
|
|
611
599
|
# Each identity keeps its own facts: neither admin flag reaches the other.
|
|
612
|
-
facts = dict(
|
|
613
|
-
person = requester_claim(
|
|
600
|
+
facts = dict(is_organization_admin=caller_admin)
|
|
601
|
+
person = requester_claim(is_organization_admin=person_admin)
|
|
614
602
|
|
|
615
603
|
with TestClient(app_for(_identities)) as client:
|
|
616
604
|
bound = client.get("/me", headers={ASSERTION_HEADER: token(**facts, requester=person)})
|
|
@@ -622,10 +610,10 @@ def test_requester_bound_call_binds_the_requester_beside_the_caller(
|
|
|
622
610
|
)
|
|
623
611
|
|
|
624
612
|
assert bound.json() == {
|
|
625
|
-
"caller": [USER,
|
|
626
|
-
"requester": [PERSON,
|
|
613
|
+
"caller": [USER, caller_admin],
|
|
614
|
+
"requester": [PERSON, person_admin],
|
|
627
615
|
}
|
|
628
|
-
assert direct.json() == {"caller": [USER,
|
|
616
|
+
assert direct.json() == {"caller": [USER, caller_admin], "requester": None}
|
|
629
617
|
with pytest.raises(RequestIdentityError, match=r"User\.get_requester\(\)"):
|
|
630
618
|
User.get_requester()
|
|
631
619
|
|
|
@@ -634,23 +622,25 @@ def test_requester_bound_call_binds_the_requester_beside_the_caller(
|
|
|
634
622
|
("requester", "omit"),
|
|
635
623
|
[
|
|
636
624
|
(None, ()),
|
|
637
|
-
({"sub": PERSON
|
|
625
|
+
({"sub": PERSON}, ()),
|
|
626
|
+
({**requester_claim(), "team_uids": []}, ()),
|
|
638
627
|
({**requester_claim(), "username": "person"}, ()),
|
|
639
628
|
(requester_claim(is_organization_admin=1), ()),
|
|
640
629
|
(requester_claim(is_organization_admin="true"), ()),
|
|
641
630
|
(requester_claim(is_organization_admin=None), ()),
|
|
642
631
|
(requester_claim(sub=PERSON.upper()), ()),
|
|
643
|
-
(requester_claim(), ("
|
|
632
|
+
(requester_claim(), ("is_organization_admin",)),
|
|
644
633
|
],
|
|
645
634
|
ids=[
|
|
646
635
|
"null",
|
|
647
|
-
"
|
|
636
|
+
"without_admin_flag",
|
|
637
|
+
"with_a_team_list",
|
|
648
638
|
"extra_key",
|
|
649
639
|
"integer_admin_flag",
|
|
650
640
|
"string_admin_flag",
|
|
651
641
|
"null_admin_flag",
|
|
652
642
|
"non_canonical_sub",
|
|
653
|
-
"
|
|
643
|
+
"without_caller_admin_flag",
|
|
654
644
|
],
|
|
655
645
|
)
|
|
656
646
|
def test_malformed_requester_never_reaches_route(signed, requester, omit):
|
|
@@ -682,7 +672,7 @@ def test_requester_is_none_in_local_mode_and_on_websockets(monkeypatch):
|
|
|
682
672
|
install_request_identity(app)
|
|
683
673
|
client = TestClient(app)
|
|
684
674
|
|
|
685
|
-
expected = {"caller": [USER,
|
|
675
|
+
expected = {"caller": [USER, False], "requester": None}
|
|
686
676
|
assert client.get("/me", headers={"Authorization": "Bearer local-token"}).json() == expected
|
|
687
677
|
with client.websocket_connect("/ws", headers={"X-User-UID": USER}) as connection:
|
|
688
678
|
assert connection.receive_json() == expected
|
|
@@ -766,5 +756,5 @@ def test_reads_as_caller_refuses_a_requester_bound_request(signed, monkeypatch):
|
|
|
766
756
|
with TestClient(app_for(handler)) as client:
|
|
767
757
|
response = client.get("/me", headers={ASSERTION_HEADER: token(requester=person)})
|
|
768
758
|
|
|
769
|
-
assert response.json() == {"caller": [USER,
|
|
759
|
+
assert response.json() == {"caller": [USER, False], "requester": [PERSON, True]}
|
|
770
760
|
assert sent == []
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_models_user_request_bound_auth.py
RENAMED
|
@@ -51,7 +51,6 @@ def test_process_profile_still_uses_process_session(monkeypatch):
|
|
|
51
51
|
# A requester-bound request: the caller is the acting application, and the
|
|
52
52
|
# request scope also holds the person it works for (ADR-0036).
|
|
53
53
|
REQUESTER_UID = "5b0f9a8e-3c2d-4e1f-9a7b-6c5d4e3f2a1b"
|
|
54
|
-
TEAM_UID = "3f1cc452-43ec-49cb-b2ba-87dbac164d29"
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def test_requester_getter_requires_authenticated_request():
|
|
@@ -67,15 +66,12 @@ def test_requester_getter_requires_authenticated_request():
|
|
|
67
66
|
|
|
68
67
|
def test_requester_getter_returns_the_requester_beside_the_unchanged_caller():
|
|
69
68
|
caller = RequestUserIdentity(uid=USER_UID)
|
|
70
|
-
requester = RequestUserIdentity(
|
|
71
|
-
uid=REQUESTER_UID, team_uids=[TEAM_UID], is_organization_admin=True
|
|
72
|
-
)
|
|
69
|
+
requester = RequestUserIdentity(uid=REQUESTER_UID, is_organization_admin=True)
|
|
73
70
|
with _request_scope() as context:
|
|
74
71
|
context.user = caller
|
|
75
72
|
assert User.get_requester() is None
|
|
76
73
|
context.requester = requester
|
|
77
74
|
assert User.get_requester() is requester
|
|
78
|
-
assert User.get_requester().team_uids == (TEAM_UID,)
|
|
79
75
|
# The person's own admin flag, never the caller's.
|
|
80
76
|
assert User.get_requester().is_organization_admin is True
|
|
81
77
|
assert User.get_logged_user().is_organization_admin is False
|
|
@@ -7,7 +7,6 @@ memberships and records every call.
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import time
|
|
10
|
-
import warnings
|
|
11
10
|
from types import SimpleNamespace
|
|
12
11
|
from unittest.mock import Mock
|
|
13
12
|
from uuid import uuid4
|
|
@@ -393,11 +392,7 @@ def test_local_mode_has_no_assertion_to_ask_with(platform, monkeypatch):
|
|
|
393
392
|
)
|
|
394
393
|
|
|
395
394
|
def handler():
|
|
396
|
-
|
|
397
|
-
with warnings.catch_warnings():
|
|
398
|
-
warnings.simplefilter("ignore", DeprecationWarning)
|
|
399
|
-
assert user.team_uids == ()
|
|
400
|
-
user.in_teams(teams(1))
|
|
395
|
+
User.get_logged_user().in_teams(teams(1))
|
|
401
396
|
|
|
402
397
|
with TestClient(app_with(handler), raise_server_exceptions=True) as client:
|
|
403
398
|
with pytest.raises(RequestIdentityError):
|
|
@@ -405,26 +400,20 @@ def test_local_mode_has_no_assertion_to_ask_with(platform, monkeypatch):
|
|
|
405
400
|
assert platform.calls == []
|
|
406
401
|
|
|
407
402
|
|
|
408
|
-
def
|
|
409
|
-
(
|
|
403
|
+
def test_the_identity_has_no_team_list(sign, platform):
|
|
404
|
+
# Step 5 (#225): the deprecated team_uids is gone; in_teams() answers.
|
|
410
405
|
seen = {}
|
|
411
406
|
|
|
412
407
|
def handler():
|
|
413
408
|
user = User.get_logged_user()
|
|
414
|
-
|
|
415
|
-
try:
|
|
416
|
-
seen["teams"] = user.team_uids
|
|
417
|
-
except RequestIdentityError as error:
|
|
418
|
-
seen["error"] = str(error)
|
|
409
|
+
seen["has_team_uids"] = hasattr(user, "team_uids")
|
|
419
410
|
|
|
420
411
|
with TestClient(app_with(handler)) as client:
|
|
421
|
-
client.get("/check", headers={ASSERTION_HEADER: sign(team_uids=[research])})
|
|
422
|
-
signed = dict(seen)
|
|
423
|
-
seen.clear()
|
|
424
412
|
client.get("/check", headers={ASSERTION_HEADER: sign()})
|
|
425
413
|
|
|
426
|
-
assert
|
|
427
|
-
|
|
414
|
+
assert seen == {"has_team_uids": False}
|
|
415
|
+
with pytest.raises(ValueError):
|
|
416
|
+
RequestUserIdentity(uid=CALLER, team_uids=[])
|
|
428
417
|
|
|
429
418
|
|
|
430
419
|
@pytest.mark.parametrize(
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/agent_scaffold/skills/a2a_sdk_execution/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/cli/code_repository_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/client/agent_runtime_models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/repository_identity_security.py
RENAMED
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/runtime_credential_exchange.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/_mcp_skills_upstream/server.py
RENAMED
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence/server/_mcp_skills_upstream/shared.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/mainsequence.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_canonical_api_route_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_code_repository_branch_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_code_repository_runtime_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_environment_resource_context.py
RENAMED
|
File without changes
|
{mainsequence-9.0.26 → mainsequence-9.0.27.dev71}/tests/test_environment_response_contracts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|