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,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,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
|
+
)
|