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.
Files changed (160) hide show
  1. mcp_hangar/__init__.py +139 -0
  2. mcp_hangar/application/__init__.py +1 -0
  3. mcp_hangar/application/commands/__init__.py +67 -0
  4. mcp_hangar/application/commands/auth_commands.py +118 -0
  5. mcp_hangar/application/commands/auth_handlers.py +296 -0
  6. mcp_hangar/application/commands/commands.py +59 -0
  7. mcp_hangar/application/commands/handlers.py +189 -0
  8. mcp_hangar/application/discovery/__init__.py +21 -0
  9. mcp_hangar/application/discovery/discovery_metrics.py +283 -0
  10. mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
  11. mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
  12. mcp_hangar/application/discovery/security_validator.py +414 -0
  13. mcp_hangar/application/event_handlers/__init__.py +50 -0
  14. mcp_hangar/application/event_handlers/alert_handler.py +191 -0
  15. mcp_hangar/application/event_handlers/audit_handler.py +203 -0
  16. mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
  17. mcp_hangar/application/event_handlers/logging_handler.py +69 -0
  18. mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
  19. mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
  20. mcp_hangar/application/event_handlers/security_handler.py +604 -0
  21. mcp_hangar/application/mcp/tooling.py +158 -0
  22. mcp_hangar/application/ports/__init__.py +9 -0
  23. mcp_hangar/application/ports/observability.py +237 -0
  24. mcp_hangar/application/queries/__init__.py +52 -0
  25. mcp_hangar/application/queries/auth_handlers.py +237 -0
  26. mcp_hangar/application/queries/auth_queries.py +118 -0
  27. mcp_hangar/application/queries/handlers.py +227 -0
  28. mcp_hangar/application/read_models/__init__.py +11 -0
  29. mcp_hangar/application/read_models/provider_views.py +139 -0
  30. mcp_hangar/application/sagas/__init__.py +11 -0
  31. mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
  32. mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
  33. mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
  34. mcp_hangar/application/services/__init__.py +9 -0
  35. mcp_hangar/application/services/provider_service.py +208 -0
  36. mcp_hangar/application/services/traced_provider_service.py +211 -0
  37. mcp_hangar/bootstrap/runtime.py +328 -0
  38. mcp_hangar/context.py +178 -0
  39. mcp_hangar/domain/__init__.py +117 -0
  40. mcp_hangar/domain/contracts/__init__.py +57 -0
  41. mcp_hangar/domain/contracts/authentication.py +225 -0
  42. mcp_hangar/domain/contracts/authorization.py +229 -0
  43. mcp_hangar/domain/contracts/event_store.py +178 -0
  44. mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
  45. mcp_hangar/domain/contracts/persistence.py +383 -0
  46. mcp_hangar/domain/contracts/provider_runtime.py +146 -0
  47. mcp_hangar/domain/discovery/__init__.py +20 -0
  48. mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
  49. mcp_hangar/domain/discovery/discovered_provider.py +185 -0
  50. mcp_hangar/domain/discovery/discovery_service.py +412 -0
  51. mcp_hangar/domain/discovery/discovery_source.py +192 -0
  52. mcp_hangar/domain/events.py +433 -0
  53. mcp_hangar/domain/exceptions.py +525 -0
  54. mcp_hangar/domain/model/__init__.py +70 -0
  55. mcp_hangar/domain/model/aggregate.py +58 -0
  56. mcp_hangar/domain/model/circuit_breaker.py +152 -0
  57. mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
  58. mcp_hangar/domain/model/event_sourced_provider.py +423 -0
  59. mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
  60. mcp_hangar/domain/model/health_tracker.py +183 -0
  61. mcp_hangar/domain/model/load_balancer.py +185 -0
  62. mcp_hangar/domain/model/provider.py +810 -0
  63. mcp_hangar/domain/model/provider_group.py +656 -0
  64. mcp_hangar/domain/model/tool_catalog.py +105 -0
  65. mcp_hangar/domain/policies/__init__.py +19 -0
  66. mcp_hangar/domain/policies/provider_health.py +187 -0
  67. mcp_hangar/domain/repository.py +249 -0
  68. mcp_hangar/domain/security/__init__.py +85 -0
  69. mcp_hangar/domain/security/input_validator.py +710 -0
  70. mcp_hangar/domain/security/rate_limiter.py +387 -0
  71. mcp_hangar/domain/security/roles.py +237 -0
  72. mcp_hangar/domain/security/sanitizer.py +387 -0
  73. mcp_hangar/domain/security/secrets.py +501 -0
  74. mcp_hangar/domain/services/__init__.py +20 -0
  75. mcp_hangar/domain/services/audit_service.py +376 -0
  76. mcp_hangar/domain/services/image_builder.py +328 -0
  77. mcp_hangar/domain/services/provider_launcher.py +1046 -0
  78. mcp_hangar/domain/value_objects.py +1138 -0
  79. mcp_hangar/errors.py +818 -0
  80. mcp_hangar/fastmcp_server.py +1105 -0
  81. mcp_hangar/gc.py +134 -0
  82. mcp_hangar/infrastructure/__init__.py +79 -0
  83. mcp_hangar/infrastructure/async_executor.py +133 -0
  84. mcp_hangar/infrastructure/auth/__init__.py +37 -0
  85. mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
  86. mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
  87. mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
  88. mcp_hangar/infrastructure/auth/middleware.py +340 -0
  89. mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
  90. mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
  91. mcp_hangar/infrastructure/auth/projections.py +366 -0
  92. mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
  93. mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
  94. mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
  95. mcp_hangar/infrastructure/command_bus.py +112 -0
  96. mcp_hangar/infrastructure/discovery/__init__.py +110 -0
  97. mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
  98. mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
  99. mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
  100. mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
  101. mcp_hangar/infrastructure/event_bus.py +260 -0
  102. mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
  103. mcp_hangar/infrastructure/event_store.py +396 -0
  104. mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
  105. mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
  106. mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
  107. mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
  108. mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
  109. mcp_hangar/infrastructure/metrics_publisher.py +36 -0
  110. mcp_hangar/infrastructure/observability/__init__.py +10 -0
  111. mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
  112. mcp_hangar/infrastructure/persistence/__init__.py +33 -0
  113. mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
  114. mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
  115. mcp_hangar/infrastructure/persistence/database.py +333 -0
  116. mcp_hangar/infrastructure/persistence/database_common.py +330 -0
  117. mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
  118. mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
  119. mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
  120. mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
  121. mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
  122. mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
  123. mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
  124. mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
  125. mcp_hangar/infrastructure/query_bus.py +153 -0
  126. mcp_hangar/infrastructure/saga_manager.py +401 -0
  127. mcp_hangar/logging_config.py +209 -0
  128. mcp_hangar/metrics.py +1007 -0
  129. mcp_hangar/models.py +31 -0
  130. mcp_hangar/observability/__init__.py +54 -0
  131. mcp_hangar/observability/health.py +487 -0
  132. mcp_hangar/observability/metrics.py +319 -0
  133. mcp_hangar/observability/tracing.py +433 -0
  134. mcp_hangar/progress.py +542 -0
  135. mcp_hangar/retry.py +613 -0
  136. mcp_hangar/server/__init__.py +120 -0
  137. mcp_hangar/server/__main__.py +6 -0
  138. mcp_hangar/server/auth_bootstrap.py +340 -0
  139. mcp_hangar/server/auth_cli.py +335 -0
  140. mcp_hangar/server/auth_config.py +305 -0
  141. mcp_hangar/server/bootstrap.py +735 -0
  142. mcp_hangar/server/cli.py +161 -0
  143. mcp_hangar/server/config.py +224 -0
  144. mcp_hangar/server/context.py +215 -0
  145. mcp_hangar/server/http_auth_middleware.py +165 -0
  146. mcp_hangar/server/lifecycle.py +467 -0
  147. mcp_hangar/server/state.py +117 -0
  148. mcp_hangar/server/tools/__init__.py +16 -0
  149. mcp_hangar/server/tools/discovery.py +186 -0
  150. mcp_hangar/server/tools/groups.py +75 -0
  151. mcp_hangar/server/tools/health.py +301 -0
  152. mcp_hangar/server/tools/provider.py +939 -0
  153. mcp_hangar/server/tools/registry.py +320 -0
  154. mcp_hangar/server/validation.py +113 -0
  155. mcp_hangar/stdio_client.py +229 -0
  156. mcp_hangar-0.2.0.dist-info/METADATA +347 -0
  157. mcp_hangar-0.2.0.dist-info/RECORD +160 -0
  158. mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
  159. mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
  160. mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,120 @@
1
+ """MCP Hangar Server.
2
+
3
+ Production-grade MCP provider registry with lazy loading, health monitoring,
4
+ auto-discovery, and container support.
5
+
6
+ Usage:
7
+ # CLI
8
+ mcp-hangar --config config.yaml
9
+ mcp-hangar --config config.yaml --http --port 8000
10
+
11
+ # Programmatic
12
+ from mcp_hangar.server import main
13
+ main()
14
+
15
+ # Or with more control
16
+ from mcp_hangar.server import parse_args, run_server
17
+ cli_config = parse_args(["--http", "--port", "9000"])
18
+ run_server(cli_config)
19
+ """
20
+
21
+ from mcp.server.fastmcp import FastMCP
22
+
23
+ # Public API imports
24
+ from .bootstrap import ( # Internal functions exported for backward compatibility / testing
25
+ _auto_add_volumes,
26
+ _create_background_workers,
27
+ _create_discovery_source,
28
+ _ensure_data_dir,
29
+ _init_cqrs,
30
+ _init_event_handlers,
31
+ _init_knowledge_base,
32
+ _init_retry_config,
33
+ _init_saga,
34
+ _register_all_tools,
35
+ ApplicationContext,
36
+ bootstrap,
37
+ GC_WORKER_INTERVAL_SECONDS,
38
+ HEALTH_CHECK_INTERVAL_SECONDS,
39
+ )
40
+ from .cli import CLIConfig, parse_args
41
+ from .config import load_config, load_config_from_file, load_configuration
42
+ from .lifecycle import run_server, ServerLifecycle
43
+ from .state import COMMAND_BUS, EVENT_BUS, get_runtime, GROUPS, PROVIDER_REPOSITORY, PROVIDERS, QUERY_BUS
44
+ from .tools import registry_list
45
+
46
+ # Backward compatibility: expose _parse_args as alias
47
+ _parse_args = parse_args
48
+
49
+
50
+ # Backward compatibility: expose _start_background_workers
51
+ def _start_background_workers() -> None:
52
+ """Start GC and health check background workers.
53
+
54
+ DEPRECATED: Use bootstrap() and ServerLifecycle instead.
55
+ This function is kept for backward compatibility only.
56
+ """
57
+ workers = _create_background_workers()
58
+ for worker in workers:
59
+ worker.start()
60
+
61
+
62
+ def main():
63
+ """CLI entry point for the registry server.
64
+
65
+ Parses command line arguments and runs the server.
66
+ """
67
+ cli_config = parse_args()
68
+ run_server(cli_config)
69
+
70
+
71
+ # FastMCP server instance for backward compatibility
72
+ # Note: This is lazily created by bootstrap() now
73
+ mcp = FastMCP("mcp-registry")
74
+
75
+
76
+ __all__ = [
77
+ # Entry points
78
+ "main",
79
+ "run_server",
80
+ # Bootstrap
81
+ "bootstrap",
82
+ "ApplicationContext",
83
+ # CLI
84
+ "parse_args",
85
+ "CLIConfig",
86
+ # Config
87
+ "load_config",
88
+ "load_config_from_file",
89
+ "load_configuration",
90
+ # Lifecycle
91
+ "ServerLifecycle",
92
+ # State (backward compatibility)
93
+ "PROVIDERS",
94
+ "GROUPS",
95
+ "PROVIDER_REPOSITORY",
96
+ "COMMAND_BUS",
97
+ "QUERY_BUS",
98
+ "EVENT_BUS",
99
+ "get_runtime",
100
+ # Tools
101
+ "registry_list",
102
+ # MCP server instance
103
+ "mcp",
104
+ # Constants
105
+ "GC_WORKER_INTERVAL_SECONDS",
106
+ "HEALTH_CHECK_INTERVAL_SECONDS",
107
+ # Internal functions (backward compatibility / testing)
108
+ "_parse_args",
109
+ "_ensure_data_dir",
110
+ "_init_event_handlers",
111
+ "_init_cqrs",
112
+ "_init_saga",
113
+ "_init_retry_config",
114
+ "_init_knowledge_base",
115
+ "_auto_add_volumes",
116
+ "_create_discovery_source",
117
+ "_create_background_workers",
118
+ "_start_background_workers",
119
+ "_register_all_tools",
120
+ ]
@@ -0,0 +1,6 @@
1
+ """Allow running server as module: python -m mcp_hangar.server"""
2
+
3
+ from . import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,340 @@
1
+ """Authentication and Authorization bootstrap.
2
+
3
+ Initializes auth components based on configuration and wires them together.
4
+ This is the composition root for auth infrastructure.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Callable
9
+
10
+ import structlog
11
+
12
+ from ..domain.contracts.authentication import IApiKeyStore
13
+ from ..domain.contracts.authorization import IAuthorizer, IRoleStore
14
+ from ..infrastructure.auth.api_key_authenticator import ApiKeyAuthenticator, InMemoryApiKeyStore
15
+ from ..infrastructure.auth.jwt_authenticator import JWKSTokenValidator, JWTAuthenticator, OIDCConfig
16
+ from ..infrastructure.auth.middleware import AuthenticationMiddleware, AuthorizationMiddleware
17
+ from ..infrastructure.auth.opa_authorizer import CombinedAuthorizer, OPAAuthorizer
18
+ from ..infrastructure.auth.rate_limiter import AuthRateLimitConfig, AuthRateLimiter
19
+ from ..infrastructure.auth.rbac_authorizer import InMemoryRoleStore, RBACAuthorizer
20
+ from .auth_config import AuthConfig
21
+
22
+ logger = structlog.get_logger(__name__)
23
+
24
+
25
+ def _create_storage_backends(
26
+ config: AuthConfig,
27
+ event_publisher: Callable | None = None,
28
+ event_store=None,
29
+ event_bus=None,
30
+ ) -> tuple[IApiKeyStore, IRoleStore]:
31
+ """Create storage backends based on configuration.
32
+
33
+ Args:
34
+ config: Auth configuration with storage settings.
35
+ event_publisher: Optional callback for publishing domain events.
36
+ For CQRS integration, pass EventBus.publish.
37
+ event_store: Optional event store for event_sourcing driver.
38
+ event_bus: Optional event bus for event_sourcing driver.
39
+
40
+ Returns:
41
+ Tuple of (api_key_store, role_store).
42
+
43
+ Raises:
44
+ ValueError: If unknown storage driver is specified.
45
+ """
46
+ driver = config.storage.driver.lower()
47
+
48
+ if driver == "memory":
49
+ logger.info("auth_storage_memory", warning="Data will be lost on restart")
50
+ api_key_store = InMemoryApiKeyStore()
51
+ role_store = InMemoryRoleStore()
52
+
53
+ elif driver == "event_sourcing":
54
+ from ..infrastructure.auth.event_sourced_store import EventSourcedApiKeyStore, EventSourcedRoleStore
55
+
56
+ if event_store is None:
57
+ raise ValueError("event_sourcing driver requires event_store to be provided")
58
+
59
+ logger.info("auth_storage_event_sourcing")
60
+
61
+ api_key_store = EventSourcedApiKeyStore(
62
+ event_store=event_store,
63
+ event_publisher=event_bus,
64
+ )
65
+ role_store = EventSourcedRoleStore(
66
+ event_store=event_store,
67
+ event_publisher=event_bus,
68
+ )
69
+
70
+ elif driver == "sqlite":
71
+ from ..infrastructure.auth.sqlite_store import SQLiteApiKeyStore, SQLiteRoleStore
72
+
73
+ # Ensure directory exists
74
+ db_path = Path(config.storage.path)
75
+ db_path.parent.mkdir(parents=True, exist_ok=True)
76
+
77
+ logger.info("auth_storage_sqlite", path=str(db_path))
78
+
79
+ api_key_store = SQLiteApiKeyStore(db_path, event_publisher=event_publisher)
80
+ api_key_store.initialize()
81
+
82
+ role_store = SQLiteRoleStore(db_path, event_publisher=event_publisher)
83
+ role_store.initialize()
84
+
85
+ elif driver == "postgresql" or driver == "postgres":
86
+ from ..infrastructure.auth.postgres_store import (
87
+ create_postgres_connection_factory,
88
+ PostgresApiKeyStore,
89
+ PostgresRoleStore,
90
+ )
91
+
92
+ logger.info(
93
+ "auth_storage_postgresql",
94
+ host=config.storage.host,
95
+ port=config.storage.port,
96
+ database=config.storage.database,
97
+ )
98
+
99
+ connection_factory = create_postgres_connection_factory(
100
+ host=config.storage.host,
101
+ port=config.storage.port,
102
+ database=config.storage.database,
103
+ user=config.storage.user,
104
+ password=config.storage.password,
105
+ min_connections=config.storage.min_connections,
106
+ max_connections=config.storage.max_connections,
107
+ )
108
+
109
+ api_key_store = PostgresApiKeyStore(connection_factory, event_publisher=event_publisher)
110
+ api_key_store.initialize()
111
+
112
+ role_store = PostgresRoleStore(connection_factory, event_publisher=event_publisher)
113
+ role_store.initialize()
114
+
115
+ else:
116
+ raise ValueError(
117
+ f"Unknown auth storage driver: {driver}. " "Use 'memory', 'event_sourcing', 'sqlite', or 'postgresql'."
118
+ )
119
+
120
+ return api_key_store, role_store
121
+
122
+
123
+ class AuthComponents:
124
+ """Container for initialized auth components.
125
+
126
+ Provides access to all auth infrastructure for use by the application.
127
+
128
+ Attributes:
129
+ authn_middleware: Authentication middleware.
130
+ authz_middleware: Authorization middleware.
131
+ api_key_store: API key storage (for key management).
132
+ role_store: Role storage (for role management).
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ authn_middleware: AuthenticationMiddleware,
138
+ authz_middleware: AuthorizationMiddleware,
139
+ api_key_store: IApiKeyStore | None = None,
140
+ role_store: IRoleStore | None = None,
141
+ ):
142
+ self.authn_middleware = authn_middleware
143
+ self.authz_middleware = authz_middleware
144
+ self.api_key_store = api_key_store
145
+ self.role_store = role_store
146
+
147
+ @property
148
+ def enabled(self) -> bool:
149
+ """Check if auth is enabled (has any authenticators)."""
150
+ return len(self.authn_middleware._authenticators) > 0 or not self.authn_middleware._allow_anonymous
151
+
152
+
153
+ class NullAuthComponents(AuthComponents):
154
+ """Null auth components for when auth is disabled.
155
+
156
+ All authentication succeeds with system principal.
157
+ All authorization is granted.
158
+ """
159
+
160
+ def __init__(self):
161
+ from ..domain.value_objects import Principal
162
+
163
+ class NullAuthenticator:
164
+ def supports(self, request):
165
+ return True
166
+
167
+ def authenticate(self, request):
168
+ return Principal.system()
169
+
170
+ class NullAuthorizer:
171
+ def authorize(self, request):
172
+ from ..domain.contracts.authorization import AuthorizationResult
173
+
174
+ return AuthorizationResult.allow(reason="auth_disabled")
175
+
176
+ super().__init__(
177
+ authn_middleware=AuthenticationMiddleware([NullAuthenticator()], allow_anonymous=True),
178
+ authz_middleware=AuthorizationMiddleware(NullAuthorizer()),
179
+ )
180
+
181
+ @property
182
+ def enabled(self) -> bool:
183
+ return False
184
+
185
+
186
+ def bootstrap_auth(
187
+ config: AuthConfig,
188
+ event_publisher: Callable | None = None,
189
+ event_store=None,
190
+ event_bus=None,
191
+ ) -> AuthComponents:
192
+ """Bootstrap authentication and authorization components.
193
+
194
+ Creates and configures all auth infrastructure based on configuration.
195
+
196
+ Args:
197
+ config: Auth configuration.
198
+ event_publisher: Optional function to publish domain events.
199
+ event_store: Optional event store for event_sourcing driver.
200
+ event_bus: Optional event bus for event_sourcing driver.
201
+
202
+ Returns:
203
+ AuthComponents with initialized middleware and stores.
204
+ """
205
+ if not config.enabled:
206
+ logger.info("auth_disabled", allow_anonymous=config.allow_anonymous)
207
+ return NullAuthComponents()
208
+
209
+ # Initialize storage backends based on configuration
210
+ # Pass event_publisher for CQRS integration - stores will emit domain events
211
+ api_key_store, role_store = _create_storage_backends(
212
+ config,
213
+ event_publisher=event_publisher,
214
+ event_store=event_store,
215
+ event_bus=event_bus,
216
+ )
217
+
218
+ authenticators = []
219
+
220
+ # Initialize API Key authentication
221
+ if config.api_key.enabled:
222
+ authenticators.append(
223
+ ApiKeyAuthenticator(
224
+ key_store=api_key_store,
225
+ header_name=config.api_key.header_name,
226
+ )
227
+ )
228
+ logger.info("api_key_auth_enabled", header_name=config.api_key.header_name)
229
+
230
+ # Initialize OIDC/JWT authentication
231
+ if config.oidc.enabled:
232
+ if not config.oidc.issuer or not config.oidc.audience:
233
+ logger.warning("oidc_config_incomplete", issuer=config.oidc.issuer, audience=config.oidc.audience)
234
+ else:
235
+ oidc_config = OIDCConfig(
236
+ issuer=config.oidc.issuer,
237
+ audience=config.oidc.audience,
238
+ jwks_uri=config.oidc.jwks_uri,
239
+ client_id=config.oidc.client_id,
240
+ subject_claim=config.oidc.subject_claim,
241
+ groups_claim=config.oidc.groups_claim,
242
+ tenant_claim=config.oidc.tenant_claim,
243
+ email_claim=config.oidc.email_claim,
244
+ )
245
+ token_validator = JWKSTokenValidator(oidc_config)
246
+ authenticators.append(JWTAuthenticator(oidc_config, token_validator))
247
+ logger.info("oidc_auth_enabled", issuer=config.oidc.issuer)
248
+
249
+ # Initialize rate limiter for brute-force protection
250
+ rate_limiter = AuthRateLimiter(
251
+ AuthRateLimitConfig(
252
+ enabled=config.rate_limit.enabled,
253
+ max_attempts=config.rate_limit.max_attempts,
254
+ window_seconds=config.rate_limit.window_seconds,
255
+ lockout_seconds=config.rate_limit.lockout_seconds,
256
+ )
257
+ )
258
+ if config.rate_limit.enabled:
259
+ logger.info(
260
+ "auth_rate_limiter_enabled",
261
+ max_attempts=config.rate_limit.max_attempts,
262
+ window_seconds=config.rate_limit.window_seconds,
263
+ )
264
+
265
+ # Create authentication middleware
266
+ authn_middleware = AuthenticationMiddleware(
267
+ authenticators=authenticators,
268
+ allow_anonymous=config.allow_anonymous,
269
+ event_publisher=event_publisher,
270
+ rate_limiter=rate_limiter if config.rate_limit.enabled else None,
271
+ )
272
+
273
+ # Apply static role assignments from config
274
+ for assignment in config.role_assignments:
275
+ if not assignment.principal or not assignment.role:
276
+ logger.warning(
277
+ "skipping_invalid_role_assignment",
278
+ principal=assignment.principal,
279
+ role=assignment.role,
280
+ )
281
+ continue
282
+
283
+ try:
284
+ role_store.assign_role(
285
+ principal_id=assignment.principal,
286
+ role_name=assignment.role,
287
+ scope=assignment.scope,
288
+ )
289
+ logger.debug(
290
+ "role_assigned_from_config",
291
+ principal=assignment.principal,
292
+ role=assignment.role,
293
+ scope=assignment.scope,
294
+ )
295
+ except ValueError as e:
296
+ logger.warning(
297
+ "role_assignment_failed",
298
+ principal=assignment.principal,
299
+ role=assignment.role,
300
+ error=str(e),
301
+ )
302
+
303
+ # Initialize authorizer
304
+ rbac_authorizer = RBACAuthorizer(role_store)
305
+ authorizer: IAuthorizer = rbac_authorizer
306
+
307
+ # Optionally wrap with OPA
308
+ if config.opa.enabled:
309
+ opa_authorizer = OPAAuthorizer(
310
+ opa_url=config.opa.url,
311
+ policy_path=config.opa.policy_path,
312
+ timeout=config.opa.timeout,
313
+ )
314
+ authorizer = CombinedAuthorizer(
315
+ rbac_authorizer=rbac_authorizer,
316
+ opa_authorizer=opa_authorizer,
317
+ require_both=False, # RBAC first, OPA as fallback
318
+ )
319
+ logger.info("opa_auth_enabled", url=config.opa.url)
320
+
321
+ # Create authorization middleware
322
+ authz_middleware = AuthorizationMiddleware(
323
+ authorizer=authorizer,
324
+ event_publisher=event_publisher,
325
+ )
326
+
327
+ logger.info(
328
+ "auth_bootstrap_complete",
329
+ authenticators_count=len(authenticators),
330
+ allow_anonymous=config.allow_anonymous,
331
+ role_assignments_count=len(config.role_assignments),
332
+ opa_enabled=config.opa.enabled,
333
+ )
334
+
335
+ return AuthComponents(
336
+ authn_middleware=authn_middleware,
337
+ authz_middleware=authz_middleware,
338
+ api_key_store=api_key_store,
339
+ role_store=role_store,
340
+ )