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,243 @@
|
|
|
1
|
+
"""OPA (Open Policy Agent) authorization implementation.
|
|
2
|
+
|
|
3
|
+
Provides integration with OPA for complex policy-based authorization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import structlog
|
|
11
|
+
|
|
12
|
+
from ...domain.contracts.authorization import AuthorizationRequest, AuthorizationResult, IPolicyEngine
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .rbac_authorizer import RBACAuthorizer
|
|
16
|
+
|
|
17
|
+
logger = structlog.get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OPAAuthorizer(IPolicyEngine):
|
|
21
|
+
"""Open Policy Agent integration for complex authorization policies.
|
|
22
|
+
|
|
23
|
+
Evaluates authorization requests against OPA policies via HTTP API.
|
|
24
|
+
Fails closed (denies access) on any errors to ensure security.
|
|
25
|
+
|
|
26
|
+
Example OPA policy (policies/mcp_authz.rego):
|
|
27
|
+
|
|
28
|
+
package mcp.authz
|
|
29
|
+
|
|
30
|
+
default allow := false
|
|
31
|
+
|
|
32
|
+
# Admins can do anything
|
|
33
|
+
allow if {
|
|
34
|
+
"admin" in input.principal.groups
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Developers can invoke tools
|
|
38
|
+
allow if {
|
|
39
|
+
"developer" in input.principal.groups
|
|
40
|
+
input.resource.type == "tool"
|
|
41
|
+
input.action == "invoke"
|
|
42
|
+
}
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
opa_url: str,
|
|
48
|
+
policy_path: str = "v1/data/mcp/authz/allow",
|
|
49
|
+
timeout: float = 5.0,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize the OPA authorizer.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
opa_url: Base URL of the OPA server (e.g., http://localhost:8181).
|
|
55
|
+
policy_path: Path to the policy decision endpoint.
|
|
56
|
+
timeout: HTTP request timeout in seconds.
|
|
57
|
+
"""
|
|
58
|
+
self._opa_url = opa_url.rstrip("/")
|
|
59
|
+
self._policy_path = policy_path.lstrip("/")
|
|
60
|
+
self._timeout = timeout
|
|
61
|
+
self._client = None # Lazy initialization
|
|
62
|
+
|
|
63
|
+
def evaluate(self, input_data: dict[str, Any]) -> AuthorizationResult:
|
|
64
|
+
"""Evaluate OPA policy with given input.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
input_data: Policy input including principal, action, resource, context.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
AuthorizationResult from policy evaluation.
|
|
71
|
+
Returns deny on any errors (fail closed).
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
import httpx
|
|
75
|
+
except ImportError:
|
|
76
|
+
logger.error("opa_httpx_not_installed")
|
|
77
|
+
return AuthorizationResult.deny(reason="opa_error:httpx_not_installed")
|
|
78
|
+
|
|
79
|
+
url = f"{self._opa_url}/{self._policy_path}"
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Lazy initialize client
|
|
83
|
+
if self._client is None:
|
|
84
|
+
self._client = httpx.Client(timeout=self._timeout)
|
|
85
|
+
|
|
86
|
+
response = self._client.post(url, json={"input": input_data})
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
|
|
89
|
+
result = response.json()
|
|
90
|
+
allowed = result.get("result", False)
|
|
91
|
+
|
|
92
|
+
logger.debug(
|
|
93
|
+
"opa_evaluation_complete",
|
|
94
|
+
url=url,
|
|
95
|
+
allowed=allowed,
|
|
96
|
+
principal_id=input_data.get("principal", {}).get("id"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if allowed:
|
|
100
|
+
return AuthorizationResult.allow(reason="opa_policy")
|
|
101
|
+
return AuthorizationResult.deny(reason="opa_denied")
|
|
102
|
+
|
|
103
|
+
except httpx.ConnectError as e:
|
|
104
|
+
logger.error("opa_connection_failed", url=url, error=str(e))
|
|
105
|
+
return AuthorizationResult.deny(reason="opa_error:connection_failed")
|
|
106
|
+
|
|
107
|
+
except httpx.TimeoutException as e:
|
|
108
|
+
logger.error("opa_timeout", url=url, error=str(e))
|
|
109
|
+
return AuthorizationResult.deny(reason="opa_error:timeout")
|
|
110
|
+
|
|
111
|
+
except httpx.HTTPStatusError as e:
|
|
112
|
+
logger.error(
|
|
113
|
+
"opa_http_error",
|
|
114
|
+
url=url,
|
|
115
|
+
status_code=e.response.status_code,
|
|
116
|
+
error=str(e),
|
|
117
|
+
)
|
|
118
|
+
return AuthorizationResult.deny(reason=f"opa_error:http_{e.response.status_code}")
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error("opa_evaluation_failed", url=url, error=str(e))
|
|
122
|
+
return AuthorizationResult.deny(reason=f"opa_error:{type(e).__name__}")
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def build_input(request: AuthorizationRequest) -> dict[str, Any]:
|
|
126
|
+
"""Build OPA input from authorization request.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
request: The authorization request.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dictionary formatted for OPA policy input.
|
|
133
|
+
"""
|
|
134
|
+
return {
|
|
135
|
+
"principal": {
|
|
136
|
+
"id": request.principal.id.value,
|
|
137
|
+
"type": request.principal.type.value,
|
|
138
|
+
"tenant_id": request.principal.tenant_id,
|
|
139
|
+
"groups": list(request.principal.groups),
|
|
140
|
+
"metadata": request.principal.metadata or {},
|
|
141
|
+
},
|
|
142
|
+
"action": request.action,
|
|
143
|
+
"resource": {
|
|
144
|
+
"type": request.resource_type,
|
|
145
|
+
"id": request.resource_id,
|
|
146
|
+
},
|
|
147
|
+
"context": request.context or {},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def authorize(self, request: AuthorizationRequest) -> AuthorizationResult:
|
|
151
|
+
"""Authorize using OPA policy.
|
|
152
|
+
|
|
153
|
+
Convenience method that builds input and evaluates policy.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
request: The authorization request.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
AuthorizationResult from policy evaluation.
|
|
160
|
+
"""
|
|
161
|
+
input_data = self.build_input(request)
|
|
162
|
+
return self.evaluate(input_data)
|
|
163
|
+
|
|
164
|
+
def close(self) -> None:
|
|
165
|
+
"""Close the HTTP client."""
|
|
166
|
+
if self._client is not None:
|
|
167
|
+
self._client.close()
|
|
168
|
+
self._client = None
|
|
169
|
+
|
|
170
|
+
def __enter__(self) -> "OPAAuthorizer":
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
174
|
+
self.close()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CombinedAuthorizer:
|
|
178
|
+
"""Combines RBAC with optional OPA for hybrid authorization.
|
|
179
|
+
|
|
180
|
+
Checks RBAC first for performance, then OPA for additional policies.
|
|
181
|
+
Useful when you want fast RBAC for most cases with OPA for complex rules.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
rbac_authorizer: "RBACAuthorizer",
|
|
187
|
+
opa_authorizer: OPAAuthorizer | None = None,
|
|
188
|
+
require_both: bool = False,
|
|
189
|
+
):
|
|
190
|
+
"""Initialize combined authorizer.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
rbac_authorizer: Primary RBAC authorizer.
|
|
194
|
+
opa_authorizer: Optional OPA authorizer for additional checks.
|
|
195
|
+
require_both: If True, both RBAC and OPA must allow.
|
|
196
|
+
If False, RBAC is checked first and OPA only if RBAC denies.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
self._rbac = rbac_authorizer
|
|
200
|
+
self._opa = opa_authorizer
|
|
201
|
+
self._require_both = require_both
|
|
202
|
+
|
|
203
|
+
def authorize(self, request: AuthorizationRequest) -> AuthorizationResult:
|
|
204
|
+
"""Check authorization with combined strategy.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
request: The authorization request.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
AuthorizationResult based on combined evaluation.
|
|
211
|
+
"""
|
|
212
|
+
rbac_result = self._rbac.authorize(request)
|
|
213
|
+
|
|
214
|
+
if self._opa is None:
|
|
215
|
+
return rbac_result
|
|
216
|
+
|
|
217
|
+
if self._require_both:
|
|
218
|
+
# Both must allow
|
|
219
|
+
if not rbac_result.allowed:
|
|
220
|
+
return rbac_result
|
|
221
|
+
|
|
222
|
+
opa_result = self._opa.authorize(request)
|
|
223
|
+
if not opa_result.allowed:
|
|
224
|
+
return AuthorizationResult.deny(reason=f"rbac_allowed_but_{opa_result.reason}")
|
|
225
|
+
|
|
226
|
+
return AuthorizationResult.allow(
|
|
227
|
+
reason=f"rbac_and_opa_allowed:{rbac_result.matched_role}",
|
|
228
|
+
permission=rbac_result.matched_permission,
|
|
229
|
+
role=rbac_result.matched_role,
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
# RBAC first, OPA as fallback
|
|
233
|
+
if rbac_result.allowed:
|
|
234
|
+
return rbac_result
|
|
235
|
+
|
|
236
|
+
# RBAC denied, check OPA
|
|
237
|
+
opa_result = self._opa.authorize(request)
|
|
238
|
+
if opa_result.allowed:
|
|
239
|
+
return AuthorizationResult.allow(
|
|
240
|
+
reason="opa_override",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return rbac_result # Return original RBAC denial
|