open-shield-python 0.2.0__tar.gz → 0.2.1__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.
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/PKG-INFO +1 -1
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/pyproject.toml +1 -1
- open_shield_python-0.2.1/src/open_shield/domain/ports/__init__.py +5 -0
- open_shield_python-0.2.1/src/open_shield/domain/ports/tenant_resolver.py +37 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/services/token_service.py +47 -10
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/unit/domain/test_claim_mapping.py +94 -1
- open_shield_python-0.2.0/src/open_shield/domain/ports/__init__.py +0 -4
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/python-packaging/SKILL.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/python-packaging/resources/implementation-playbook.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/python-pro/SKILL.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/python-testing-patterns/SKILL.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/python-testing-patterns/resources/implementation-playbook.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/solid_architecture/SKILL.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/solid_architecture/examples/before_after_refactor.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/solid_architecture/examples/clean_architecture_layout.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/solid_architecture/examples/dip_ports_adapters.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/solid_architecture/resources/code_review_checklist.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.github/workflows/ci.yml +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/.gitignore +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/CHANGELOG.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/CONTRIBUTING.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/LICENSE +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/None +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/README.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/architecture/system-overview.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/design/overview.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/implementation/guidelines.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/README.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/backlog.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/decisions/0000-template.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/decisions/001-clean-architecture.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/decisions/002-modern-tooling.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/decisions/003-pydantic-usage.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/phases/phase-0-init.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/phases/phase-1-core.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/phases/phase-2-authz.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/phases/phase-3-integration.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/phases/phase-4-publish.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/roadmap.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/specs.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/tasks.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/tech-debt.md +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/main.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/adapters/__init__.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/adapters/config.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/adapters/key_provider.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/adapters/token_validator.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/api/__init__.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/api/fastapi/__init__.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/api/fastapi/dependencies.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/api/fastapi/middleware.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/__init__.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/entities.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/exceptions.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/ports/key_provider.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/ports/token_validator.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/services/__init__.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/services/authorization_service.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/services/claim_mapping.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/integration/adapters/test_key_provider.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/integration/adapters/test_token_validator.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/integration/api/test_fastapi.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/unit/adapters/test_config.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/unit/domain/test_authorization_service.py +0 -0
- {open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/unit/domain/test_token_service.py +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Port for resolving tenant context from machine client credentials.
|
|
2
|
+
|
|
3
|
+
This port supports Case 3 (M2M clients) in the tenant resolution cascade:
|
|
4
|
+
|
|
5
|
+
1. M2M client → lookup_client_tenant(client_id) → tenant_id
|
|
6
|
+
2. Organization claim → organization_id → tenant_id
|
|
7
|
+
3. Fallback → sub → tenant_id (individual user mode only)
|
|
8
|
+
|
|
9
|
+
Consumers implement this port to map client_id → tenant_id using their
|
|
10
|
+
own registry (database, config file, Logto Management API, etc.).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TenantResolverPort(ABC):
|
|
17
|
+
"""Resolves tenant context for machine-to-machine (M2M) clients.
|
|
18
|
+
|
|
19
|
+
M2M tokens (client_credentials flow) don't carry organization claims.
|
|
20
|
+
This port allows consumers to map a ``client_id`` to a ``tenant_id``
|
|
21
|
+
using their own backend registry.
|
|
22
|
+
|
|
23
|
+
If no resolver is provided to ``TokenService``, M2M tokens fall through
|
|
24
|
+
to the organization claim or sub fallback.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def resolve_tenant(self, client_id: str) -> str | None:
|
|
29
|
+
"""Look up the tenant for a machine client.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
client_id: The OAuth2 client_id from the token.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The tenant_id if found, or None to continue the cascade.
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
from open_shield.domain.entities import TenantContext, Token, User, UserContext
|
|
2
2
|
from open_shield.domain.exceptions import TokenValidationError
|
|
3
|
-
from open_shield.domain.ports import TokenValidatorPort
|
|
3
|
+
from open_shield.domain.ports import TenantResolverPort, TokenValidatorPort
|
|
4
4
|
from open_shield.domain.services.claim_mapping import ClaimMapping
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class TokenService:
|
|
8
8
|
"""Domain service for orchestrating token validation and context extraction.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Implements a 3-step tenant resolution cascade:
|
|
11
|
+
1. M2M client → lookup via TenantResolverPort (if provided)
|
|
12
|
+
2. Organization claim → configurable tenant_id_claim
|
|
13
|
+
3. Fallback → sub (if tenant_fallback="sub")
|
|
11
14
|
|
|
12
15
|
Args:
|
|
13
16
|
validator: Port for JWT validation.
|
|
14
17
|
claim_mapping: Configurable claim-to-field mapping.
|
|
15
|
-
|
|
18
|
+
tenant_resolver: Optional port for M2M client→tenant lookup.
|
|
19
|
+
If not provided, M2M tokens fall through to the org claim.
|
|
16
20
|
"""
|
|
17
21
|
|
|
18
22
|
def __init__(
|
|
19
23
|
self,
|
|
20
24
|
validator: TokenValidatorPort,
|
|
21
25
|
claim_mapping: ClaimMapping | None = None,
|
|
26
|
+
tenant_resolver: TenantResolverPort | None = None,
|
|
22
27
|
):
|
|
23
28
|
self.validator = validator
|
|
24
29
|
self.claims = claim_mapping or ClaimMapping()
|
|
30
|
+
self.tenant_resolver = tenant_resolver
|
|
25
31
|
|
|
26
32
|
def validate_and_extract(self, token_string: str) -> UserContext:
|
|
27
33
|
"""Validate a raw token string and extract the user context.
|
|
@@ -80,18 +86,49 @@ class TokenService:
|
|
|
80
86
|
)
|
|
81
87
|
|
|
82
88
|
def _extract_tenant(self, token: Token) -> TenantContext | None:
|
|
83
|
-
"""Extract tenant context
|
|
89
|
+
"""Extract tenant context using a 3-step cascade.
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
Resolution order:
|
|
92
|
+
1. M2M client lookup — If this is a client_credentials token
|
|
93
|
+
(sub == client_id) AND a TenantResolverPort is configured,
|
|
94
|
+
resolve tenant from the client registry.
|
|
95
|
+
2. Organization claim — Read the configured tenant_id_claim
|
|
96
|
+
(e.g. organization_id, tid, org_id).
|
|
97
|
+
3. Sub fallback — If tenant_fallback="sub", use the user's
|
|
98
|
+
subject as tenant_id (individual user mode).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
TenantContext if resolved, None if no tenant could be determined.
|
|
87
102
|
"""
|
|
88
|
-
|
|
103
|
+
claims = token.claims
|
|
104
|
+
sub = token.subject
|
|
105
|
+
|
|
106
|
+
# --- Step 1: M2M client → tenant lookup ---
|
|
107
|
+
client_id = claims.get("client_id", claims.get("azp"))
|
|
108
|
+
is_m2m = client_id and sub and sub == client_id
|
|
89
109
|
|
|
90
|
-
if
|
|
91
|
-
|
|
110
|
+
if is_m2m and self.tenant_resolver:
|
|
111
|
+
resolved = self.tenant_resolver.resolve_tenant(client_id)
|
|
112
|
+
if resolved:
|
|
113
|
+
return TenantContext(
|
|
114
|
+
tenant_id=resolved,
|
|
115
|
+
metadata={"resolution": "m2m_lookup", "client_id": client_id},
|
|
116
|
+
)
|
|
92
117
|
|
|
118
|
+
# --- Step 2: Organization claim ---
|
|
119
|
+
tid = claims.get(self.claims.tenant_id_claim)
|
|
93
120
|
if tid:
|
|
94
|
-
return TenantContext(
|
|
121
|
+
return TenantContext(
|
|
122
|
+
tenant_id=tid,
|
|
123
|
+
metadata={"resolution": "claim", "claim": self.claims.tenant_id_claim},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# --- Step 3: Sub fallback (individual user mode) ---
|
|
127
|
+
if self.claims.tenant_fallback == "sub" and sub:
|
|
128
|
+
return TenantContext(
|
|
129
|
+
tenant_id=sub,
|
|
130
|
+
metadata={"resolution": "sub_fallback"},
|
|
131
|
+
)
|
|
95
132
|
|
|
96
133
|
return None
|
|
97
134
|
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/unit/domain/test_claim_mapping.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
from open_shield.domain.entities import Token
|
|
4
|
-
from open_shield.domain.ports import TokenValidatorPort
|
|
4
|
+
from open_shield.domain.ports import TenantResolverPort, TokenValidatorPort
|
|
5
5
|
from open_shield.domain.services import TokenService
|
|
6
6
|
from open_shield.domain.services.claim_mapping import ClaimMapping
|
|
7
7
|
|
|
@@ -152,3 +152,96 @@ def test_actor_type_azp_fallback() -> None:
|
|
|
152
152
|
|
|
153
153
|
ctx = service.validate_and_extract("token")
|
|
154
154
|
assert ctx.user.actor_type == "service"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Tenant Resolution Cascade Tests
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _MockTenantResolver(TenantResolverPort):
|
|
163
|
+
"""Mock tenant resolver for testing M2M lookup."""
|
|
164
|
+
|
|
165
|
+
def __init__(self, mapping: dict[str, str]) -> None:
|
|
166
|
+
self._mapping = mapping
|
|
167
|
+
|
|
168
|
+
def resolve_tenant(self, client_id: str) -> str | None:
|
|
169
|
+
return self._mapping.get(client_id)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_cascade_step1_m2m_resolver() -> None:
|
|
173
|
+
"""Step 1: M2M token with resolver → tenant from registry."""
|
|
174
|
+
claims = {"sub": "client_abc", "client_id": "client_abc"}
|
|
175
|
+
resolver = _MockTenantResolver({"client_abc": "tenant_from_registry"})
|
|
176
|
+
service = TokenService(
|
|
177
|
+
_MockValidator(claims),
|
|
178
|
+
tenant_resolver=resolver,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
ctx = service.validate_and_extract("token")
|
|
182
|
+
assert ctx.tenant is not None
|
|
183
|
+
assert ctx.tenant.tenant_id == "tenant_from_registry"
|
|
184
|
+
assert ctx.tenant.metadata["resolution"] == "m2m_lookup"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_cascade_step1_m2m_no_resolver_falls_through() -> None:
|
|
188
|
+
"""Step 1 skipped: M2M token but no resolver → falls to step 2/3."""
|
|
189
|
+
claims = {"sub": "client_abc", "client_id": "client_abc", "tid": "org_123"}
|
|
190
|
+
service = TokenService(
|
|
191
|
+
_MockValidator(claims),
|
|
192
|
+
# No tenant_resolver provided
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
ctx = service.validate_and_extract("token")
|
|
196
|
+
assert ctx.tenant is not None
|
|
197
|
+
assert ctx.tenant.tenant_id == "org_123"
|
|
198
|
+
assert ctx.tenant.metadata["resolution"] == "claim"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_cascade_step1_resolver_returns_none_falls_through() -> None:
|
|
202
|
+
"""Step 1: Resolver doesn't know this client → falls to step 2."""
|
|
203
|
+
claims = {
|
|
204
|
+
"sub": "unknown_client",
|
|
205
|
+
"client_id": "unknown_client",
|
|
206
|
+
"organization_id": "fallback_org",
|
|
207
|
+
}
|
|
208
|
+
resolver = _MockTenantResolver({}) # Empty — won't match
|
|
209
|
+
cm = ClaimMapping(tenant_id_claim="organization_id")
|
|
210
|
+
service = TokenService(
|
|
211
|
+
_MockValidator(claims),
|
|
212
|
+
claim_mapping=cm,
|
|
213
|
+
tenant_resolver=resolver,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
ctx = service.validate_and_extract("token")
|
|
217
|
+
assert ctx.tenant is not None
|
|
218
|
+
assert ctx.tenant.tenant_id == "fallback_org"
|
|
219
|
+
assert ctx.tenant.metadata["resolution"] == "claim"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_cascade_step2_org_claim_priority() -> None:
|
|
223
|
+
"""Step 2: Organization claim takes priority over sub fallback."""
|
|
224
|
+
claims = {"sub": "user_123", "organization_id": "team_abc"}
|
|
225
|
+
cm = ClaimMapping(
|
|
226
|
+
tenant_id_claim="organization_id",
|
|
227
|
+
tenant_fallback="sub", # Would use sub, but org claim wins
|
|
228
|
+
)
|
|
229
|
+
service = TokenService(_MockValidator(claims), claim_mapping=cm)
|
|
230
|
+
|
|
231
|
+
ctx = service.validate_and_extract("token")
|
|
232
|
+
assert ctx.tenant is not None
|
|
233
|
+
assert ctx.tenant.tenant_id == "team_abc"
|
|
234
|
+
assert ctx.tenant.metadata["resolution"] == "claim"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_cascade_step3_sub_fallback_metadata() -> None:
|
|
238
|
+
"""Step 3: Sub fallback includes resolution metadata."""
|
|
239
|
+
claims = {"sub": "solo_user"}
|
|
240
|
+
cm = ClaimMapping(tenant_fallback="sub")
|
|
241
|
+
service = TokenService(_MockValidator(claims), claim_mapping=cm)
|
|
242
|
+
|
|
243
|
+
ctx = service.validate_and_extract("token")
|
|
244
|
+
assert ctx.tenant is not None
|
|
245
|
+
assert ctx.tenant.tenant_id == "solo_user"
|
|
246
|
+
assert ctx.tenant.metadata["resolution"] == "sub_fallback"
|
|
247
|
+
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/python-packaging/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/python-testing-patterns/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/.agent/skills/solid_architecture/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
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/decisions/0000-template.md
RENAMED
|
File without changes
|
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/decisions/002-modern-tooling.md
RENAMED
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/decisions/003-pydantic-usage.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/phases/phase-3-integration.md
RENAMED
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/docs/planning/phases/phase-4-publish.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
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/adapters/key_provider.py
RENAMED
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/adapters/token_validator.py
RENAMED
|
File without changes
|
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/api/fastapi/__init__.py
RENAMED
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/api/fastapi/dependencies.py
RENAMED
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/api/fastapi/middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/ports/key_provider.py
RENAMED
|
File without changes
|
|
File without changes
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/src/open_shield/domain/services/__init__.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
|
{open_shield_python-0.2.0 → open_shield_python-0.2.1}/tests/unit/domain/test_token_service.py
RENAMED
|
File without changes
|