mcp-hangar 0.2.0__py3-none-any.whl
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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""RBAC (Role-Based Access Control) authorization implementation.
|
|
2
|
+
|
|
3
|
+
Provides authorizer and in-memory role store for role-based access control.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
|
|
10
|
+
from ...domain.contracts.authorization import AuthorizationRequest, AuthorizationResult, IAuthorizer, IRoleStore
|
|
11
|
+
from ...domain.security.roles import BUILTIN_ROLES
|
|
12
|
+
from ...domain.value_objects import Permission, Principal, Role
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RBACAuthorizer(IAuthorizer):
|
|
18
|
+
"""Role-Based Access Control authorizer.
|
|
19
|
+
|
|
20
|
+
Checks authorization based on:
|
|
21
|
+
1. System principal - always allowed
|
|
22
|
+
2. Direct role assignments to principal
|
|
23
|
+
3. Role assignments to principal's groups
|
|
24
|
+
|
|
25
|
+
Multiple scopes are supported (global, tenant, namespace).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, role_store: IRoleStore):
|
|
29
|
+
"""Initialize the RBAC authorizer.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
role_store: Storage backend for roles and assignments.
|
|
33
|
+
"""
|
|
34
|
+
self._role_store = role_store
|
|
35
|
+
|
|
36
|
+
def authorize(self, request: AuthorizationRequest) -> AuthorizationResult:
|
|
37
|
+
"""Check if principal is authorized for the action.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
request: Authorization request with principal, action, and resource.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
AuthorizationResult with allowed status and reason.
|
|
44
|
+
"""
|
|
45
|
+
principal = request.principal
|
|
46
|
+
|
|
47
|
+
# System principal has full access
|
|
48
|
+
if principal.is_system():
|
|
49
|
+
logger.debug(
|
|
50
|
+
"authorization_granted_system",
|
|
51
|
+
action=request.action,
|
|
52
|
+
resource_type=request.resource_type,
|
|
53
|
+
resource_id=request.resource_id,
|
|
54
|
+
)
|
|
55
|
+
return AuthorizationResult.allow(reason="system_principal")
|
|
56
|
+
|
|
57
|
+
# Collect all roles for principal
|
|
58
|
+
roles = self._collect_roles(principal)
|
|
59
|
+
|
|
60
|
+
# Check each role for matching permission
|
|
61
|
+
for role in roles:
|
|
62
|
+
if role.has_permission(request.resource_type, request.action, request.resource_id):
|
|
63
|
+
# Find the specific permission that matched (for audit)
|
|
64
|
+
matched_permission = self._find_matching_permission(
|
|
65
|
+
role, request.resource_type, request.action, request.resource_id
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger.debug(
|
|
69
|
+
"authorization_granted",
|
|
70
|
+
principal_id=principal.id.value,
|
|
71
|
+
role=role.name,
|
|
72
|
+
action=request.action,
|
|
73
|
+
resource_type=request.resource_type,
|
|
74
|
+
resource_id=request.resource_id,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return AuthorizationResult.allow(
|
|
78
|
+
reason=f"granted_by_role:{role.name}",
|
|
79
|
+
permission=matched_permission,
|
|
80
|
+
role=role.name,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# No matching permission found
|
|
84
|
+
logger.warning(
|
|
85
|
+
"authorization_denied",
|
|
86
|
+
principal_id=principal.id.value,
|
|
87
|
+
action=request.action,
|
|
88
|
+
resource_type=request.resource_type,
|
|
89
|
+
resource_id=request.resource_id,
|
|
90
|
+
roles_checked=[r.name for r in roles],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return AuthorizationResult.deny(reason="no_matching_permission")
|
|
94
|
+
|
|
95
|
+
def _collect_roles(self, principal: Principal) -> list[Role]:
|
|
96
|
+
"""Collect all roles for a principal.
|
|
97
|
+
|
|
98
|
+
Includes:
|
|
99
|
+
- Direct role assignments (global scope)
|
|
100
|
+
- Group-based role assignments (global scope)
|
|
101
|
+
- Tenant-scoped assignments if principal has tenant_id
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
principal: The principal to collect roles for.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of all applicable roles.
|
|
108
|
+
"""
|
|
109
|
+
roles: list[Role] = []
|
|
110
|
+
|
|
111
|
+
# Direct assignments - global scope only
|
|
112
|
+
direct_roles = self._role_store.get_roles_for_principal(principal.id.value, scope="global")
|
|
113
|
+
roles.extend(direct_roles)
|
|
114
|
+
|
|
115
|
+
# Group-based assignments - global scope only
|
|
116
|
+
for group in principal.groups:
|
|
117
|
+
group_roles = self._role_store.get_roles_for_principal(f"group:{group}", scope="global")
|
|
118
|
+
roles.extend(group_roles)
|
|
119
|
+
|
|
120
|
+
# Tenant-scoped assignments (only if principal has tenant_id)
|
|
121
|
+
if principal.tenant_id:
|
|
122
|
+
tenant_scope = f"tenant:{principal.tenant_id}"
|
|
123
|
+
# Direct tenant-scoped roles
|
|
124
|
+
tenant_roles = self._role_store.get_roles_for_principal(principal.id.value, scope=tenant_scope)
|
|
125
|
+
roles.extend(tenant_roles)
|
|
126
|
+
# Group-based tenant-scoped roles
|
|
127
|
+
for group in principal.groups:
|
|
128
|
+
group_tenant_roles = self._role_store.get_roles_for_principal(f"group:{group}", scope=tenant_scope)
|
|
129
|
+
roles.extend(group_tenant_roles)
|
|
130
|
+
|
|
131
|
+
return roles
|
|
132
|
+
|
|
133
|
+
def _find_matching_permission(
|
|
134
|
+
self,
|
|
135
|
+
role: Role,
|
|
136
|
+
resource_type: str,
|
|
137
|
+
action: str,
|
|
138
|
+
resource_id: str,
|
|
139
|
+
) -> Permission | None:
|
|
140
|
+
"""Find the specific permission that matched.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
role: Role to search.
|
|
144
|
+
resource_type: Resource type to match.
|
|
145
|
+
action: Action to match.
|
|
146
|
+
resource_id: Resource ID to match.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The matching Permission, or None if not found.
|
|
150
|
+
"""
|
|
151
|
+
for permission in role.permissions:
|
|
152
|
+
if permission.matches(resource_type, action, resource_id):
|
|
153
|
+
return permission
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class InMemoryRoleStore(IRoleStore):
|
|
158
|
+
"""In-memory role store for development/testing.
|
|
159
|
+
|
|
160
|
+
WARNING: Data is lost on restart. Use a persistent store
|
|
161
|
+
for production.
|
|
162
|
+
|
|
163
|
+
This implementation is thread-safe using a reentrant lock.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self) -> None:
|
|
167
|
+
"""Initialize with built-in roles."""
|
|
168
|
+
self._lock = threading.RLock()
|
|
169
|
+
# role_name -> Role
|
|
170
|
+
self._roles: dict[str, Role] = dict(BUILTIN_ROLES)
|
|
171
|
+
# principal_id -> scope -> set of role names
|
|
172
|
+
self._assignments: dict[str, dict[str, set[str]]] = {}
|
|
173
|
+
|
|
174
|
+
def get_role(self, role_name: str) -> Role | None:
|
|
175
|
+
"""Get role by name.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
role_name: Name of the role to retrieve.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Role if found, None otherwise.
|
|
182
|
+
"""
|
|
183
|
+
with self._lock:
|
|
184
|
+
return self._roles.get(role_name)
|
|
185
|
+
|
|
186
|
+
def add_role(self, role: Role) -> None:
|
|
187
|
+
"""Add a custom role.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
role: Role to add.
|
|
191
|
+
"""
|
|
192
|
+
with self._lock:
|
|
193
|
+
self._roles[role.name] = role
|
|
194
|
+
logger.info("role_added", role_name=role.name)
|
|
195
|
+
|
|
196
|
+
def get_roles_for_principal(
|
|
197
|
+
self,
|
|
198
|
+
principal_id: str,
|
|
199
|
+
scope: str = "*",
|
|
200
|
+
) -> list[Role]:
|
|
201
|
+
"""Get all roles assigned to a principal.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
principal_id: ID of the principal.
|
|
205
|
+
scope: Filter by scope ('*' for all, 'global' for global only,
|
|
206
|
+
'tenant:X' for that specific scope only, etc.).
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of roles assigned to the principal for the specified scope.
|
|
210
|
+
|
|
211
|
+
Note:
|
|
212
|
+
When scope is specific (not '*'), only roles in that exact scope
|
|
213
|
+
are returned. The caller is responsible for aggregating roles
|
|
214
|
+
from multiple scopes if needed.
|
|
215
|
+
"""
|
|
216
|
+
with self._lock:
|
|
217
|
+
if principal_id not in self._assignments:
|
|
218
|
+
return []
|
|
219
|
+
|
|
220
|
+
principal_assignments = self._assignments[principal_id]
|
|
221
|
+
role_names: set[str] = set()
|
|
222
|
+
|
|
223
|
+
if scope == "*":
|
|
224
|
+
# All scopes
|
|
225
|
+
for scope_roles in principal_assignments.values():
|
|
226
|
+
role_names.update(scope_roles)
|
|
227
|
+
else:
|
|
228
|
+
# Specific scope only - no automatic global inclusion
|
|
229
|
+
role_names.update(principal_assignments.get(scope, set()))
|
|
230
|
+
|
|
231
|
+
# Convert role names to Role objects
|
|
232
|
+
return [self._roles[name] for name in role_names if name in self._roles]
|
|
233
|
+
|
|
234
|
+
def assign_role(
|
|
235
|
+
self,
|
|
236
|
+
principal_id: str,
|
|
237
|
+
role_name: str,
|
|
238
|
+
scope: str = "global",
|
|
239
|
+
assigned_by: str = "system",
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Assign a role to a principal.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
principal_id: ID of the principal receiving the role.
|
|
245
|
+
role_name: Name of the role to assign.
|
|
246
|
+
scope: Scope of the assignment.
|
|
247
|
+
assigned_by: Principal making the assignment.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
ValueError: If role_name doesn't exist.
|
|
251
|
+
"""
|
|
252
|
+
with self._lock:
|
|
253
|
+
if role_name not in self._roles:
|
|
254
|
+
raise ValueError(f"Unknown role: {role_name}")
|
|
255
|
+
|
|
256
|
+
if principal_id not in self._assignments:
|
|
257
|
+
self._assignments[principal_id] = {}
|
|
258
|
+
|
|
259
|
+
if scope not in self._assignments[principal_id]:
|
|
260
|
+
self._assignments[principal_id][scope] = set()
|
|
261
|
+
|
|
262
|
+
self._assignments[principal_id][scope].add(role_name)
|
|
263
|
+
|
|
264
|
+
logger.info(
|
|
265
|
+
"role_assigned",
|
|
266
|
+
principal_id=principal_id,
|
|
267
|
+
role_name=role_name,
|
|
268
|
+
scope=scope,
|
|
269
|
+
assigned_by=assigned_by,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def revoke_role(
|
|
273
|
+
self,
|
|
274
|
+
principal_id: str,
|
|
275
|
+
role_name: str,
|
|
276
|
+
scope: str = "global",
|
|
277
|
+
revoked_by: str = "system",
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Revoke a role from a principal.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
principal_id: ID of the principal losing the role.
|
|
283
|
+
role_name: Name of the role to revoke.
|
|
284
|
+
scope: Scope from which to revoke.
|
|
285
|
+
revoked_by: Principal making the revocation.
|
|
286
|
+
"""
|
|
287
|
+
with self._lock:
|
|
288
|
+
if principal_id in self._assignments:
|
|
289
|
+
if scope in self._assignments[principal_id]:
|
|
290
|
+
self._assignments[principal_id][scope].discard(role_name)
|
|
291
|
+
logger.info(
|
|
292
|
+
"role_revoked",
|
|
293
|
+
principal_id=principal_id,
|
|
294
|
+
role_name=role_name,
|
|
295
|
+
scope=scope,
|
|
296
|
+
revoked_by=revoked_by,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def list_assignments(self, principal_id: str) -> dict[str, list[str]]:
|
|
300
|
+
"""List all role assignments for a principal.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
principal_id: ID of the principal.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Dict of scope -> list of role names.
|
|
307
|
+
"""
|
|
308
|
+
with self._lock:
|
|
309
|
+
if principal_id not in self._assignments:
|
|
310
|
+
return {}
|
|
311
|
+
|
|
312
|
+
return {scope: list(roles) for scope, roles in self._assignments[principal_id].items()}
|
|
313
|
+
|
|
314
|
+
def clear_assignments(self, principal_id: str) -> None:
|
|
315
|
+
"""Clear all role assignments for a principal.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
principal_id: ID of the principal.
|
|
319
|
+
"""
|
|
320
|
+
with self._lock:
|
|
321
|
+
if principal_id in self._assignments:
|
|
322
|
+
del self._assignments[principal_id]
|
|
323
|
+
logger.info("assignments_cleared", principal_id=principal_id)
|