tba-app-backend-core 0.4.0.dev2__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.
- tba_app_backend_core/__init__.py +15 -0
- tba_app_backend_core/app.py +146 -0
- tba_app_backend_core/plugin.py +36 -0
- tba_app_backend_core/product.py +55 -0
- tba_app_backend_core/py.typed +0 -0
- tba_app_backend_core/surfaces/__init__.py +3 -0
- tba_app_backend_core/surfaces/admin_routes.py +353 -0
- tba_app_backend_core/surfaces/advanced_protocols.py +170 -0
- tba_app_backend_core/surfaces/auth_flows.py +16 -0
- tba_app_backend_core/surfaces/authorization_server_metadata_surface.py +37 -0
- tba_app_backend_core/surfaces/authorization_surface.py +15 -0
- tba_app_backend_core/surfaces/authz_surface.py +14 -0
- tba_app_backend_core/surfaces/client_registration_surface.py +366 -0
- tba_app_backend_core/surfaces/device_authorization_surface.py +44 -0
- tba_app_backend_core/surfaces/introspection_surface.py +247 -0
- tba_app_backend_core/surfaces/login_surface.py +25 -0
- tba_app_backend_core/surfaces/logout_surface.py +38 -0
- tba_app_backend_core/surfaces/oidc_discovery_surface.py +79 -0
- tba_app_backend_core/surfaces/par_surface.py +394 -0
- tba_app_backend_core/surfaces/protected_resource_metadata_surface.py +34 -0
- tba_app_backend_core/surfaces/resource_validation_metadata_surface.py +33 -0
- tba_app_backend_core/surfaces/revocation_surface.py +39 -0
- tba_app_backend_core/surfaces/standards_manifest.py +24 -0
- tba_app_backend_core/surfaces/surface_api.py +284 -0
- tba_app_backend_core/surfaces/token_exchange_surface.py +34 -0
- tba_app_backend_core/surfaces/token_surface.py +12 -0
- tba_app_backend_core/surfaces/userinfo_surface.py +34 -0
- tba_app_backend_core-0.4.0.dev2.dist-info/METADATA +60 -0
- tba_app_backend_core-0.4.0.dev2.dist-info/RECORD +30 -0
- tba_app_backend_core-0.4.0.dev2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Shared backend-application composition APIs."""
|
|
2
|
+
|
|
3
|
+
from .app import app, build_app, build_application_runtime_plan
|
|
4
|
+
from .plugin import TbaBackendAppPlugin, install
|
|
5
|
+
from .product import build_product_app, validate_backend_app_contract
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"app",
|
|
9
|
+
"build_app",
|
|
10
|
+
"build_application_runtime_plan",
|
|
11
|
+
"TbaBackendAppPlugin",
|
|
12
|
+
"build_product_app",
|
|
13
|
+
"install",
|
|
14
|
+
"validate_backend_app_contract",
|
|
15
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from importlib.metadata import (
|
|
5
|
+
PackageNotFoundError,
|
|
6
|
+
version as installed_package_version,
|
|
7
|
+
)
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from tba_runtime_identity.deployment import ResolvedDeployment, resolve_deployment
|
|
12
|
+
from tba_operator_identity.repo_truth import (
|
|
13
|
+
package_version as repository_package_version,
|
|
14
|
+
)
|
|
15
|
+
from tba_runtime_identity import LazyASGIApplication
|
|
16
|
+
from tba_server_identity.composition.app import (
|
|
17
|
+
_load_default_settings,
|
|
18
|
+
build_application_runtime_plan as _build_application_runtime_plan,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from tigrbl import TigrblApp
|
|
23
|
+
from tba_server_identity.composition.lifecycle import AssemblyFactory
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _runtime_package_version() -> str:
|
|
27
|
+
try:
|
|
28
|
+
return installed_package_version("tba-app-backend-core")
|
|
29
|
+
except PackageNotFoundError:
|
|
30
|
+
return repository_package_version(Path(__file__).resolve().parents[2])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _path_has_prefix(path: str, prefix: str) -> bool:
|
|
34
|
+
return path == prefix or path.startswith(f"{prefix}/")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _hide_disabled_control_plane_docs(
|
|
38
|
+
app: object,
|
|
39
|
+
deployment: ResolvedDeployment,
|
|
40
|
+
*,
|
|
41
|
+
diagnostics_prefix: str,
|
|
42
|
+
) -> None:
|
|
43
|
+
routes = getattr(app, "_routes", None)
|
|
44
|
+
if not isinstance(routes, list):
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
hidden = []
|
|
48
|
+
for route in routes:
|
|
49
|
+
path = str(getattr(route, "path_template", None) or getattr(route, "path", ""))
|
|
50
|
+
hide = not deployment.flag_enabled(
|
|
51
|
+
"surface_diagnostics_enabled"
|
|
52
|
+
) and _path_has_prefix(path, diagnostics_prefix)
|
|
53
|
+
if hide and hasattr(route, "include_in_schema"):
|
|
54
|
+
hidden.append(replace(route, include_in_schema=False))
|
|
55
|
+
else:
|
|
56
|
+
hidden.append(route)
|
|
57
|
+
routes[:] = hidden
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_app(
|
|
61
|
+
settings_obj: object | None = None,
|
|
62
|
+
*,
|
|
63
|
+
deployment: ResolvedDeployment | None = None,
|
|
64
|
+
runtime_assembly_factory: AssemblyFactory | None = None,
|
|
65
|
+
) -> "TigrblApp":
|
|
66
|
+
resolved_settings = (
|
|
67
|
+
settings_obj if settings_obj is not None else _load_default_settings()
|
|
68
|
+
)
|
|
69
|
+
resolved_deployment = deployment or resolve_deployment(resolved_settings)
|
|
70
|
+
if runtime_assembly_factory is None:
|
|
71
|
+
from tba_server_identity.composition.runtime_assembly import (
|
|
72
|
+
build_server_runtime_assembly_async,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def default_runtime_assembly_factory():
|
|
76
|
+
return await build_server_runtime_assembly_async(resolved_settings)
|
|
77
|
+
|
|
78
|
+
runtime_assembly_factory = default_runtime_assembly_factory
|
|
79
|
+
|
|
80
|
+
from tba_contracts_identity.protocol_configuration import (
|
|
81
|
+
ProtocolSettingsOverlay,
|
|
82
|
+
bind_protocol_settings,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
protocol_settings = ProtocolSettingsOverlay(
|
|
86
|
+
resolved_settings, _load_default_settings()
|
|
87
|
+
)
|
|
88
|
+
bind_protocol_settings(protocol_settings)
|
|
89
|
+
|
|
90
|
+
from tigrbl import TigrblApp
|
|
91
|
+
|
|
92
|
+
from tba_server_identity.composition.lifecycle import register_lifecycle
|
|
93
|
+
from tba_app_backend_core.surfaces import (
|
|
94
|
+
admin_resource_path_prefixes,
|
|
95
|
+
attach_runtime_surfaces,
|
|
96
|
+
)
|
|
97
|
+
from tba_router_administration_gate import AdminGate
|
|
98
|
+
from tba_runtime_identity_storage.engine import dsn
|
|
99
|
+
|
|
100
|
+
app = TigrblApp(
|
|
101
|
+
title="tba_auth",
|
|
102
|
+
version=_runtime_package_version(),
|
|
103
|
+
openapi_url="/openapi.json",
|
|
104
|
+
docs_url="/docs",
|
|
105
|
+
engine=dsn,
|
|
106
|
+
)
|
|
107
|
+
state = getattr(app, "state", None)
|
|
108
|
+
if state is not None:
|
|
109
|
+
state.tba_auth_deployment = resolved_deployment
|
|
110
|
+
state.tba_auth_settings = resolved_settings
|
|
111
|
+
surface_router = attach_runtime_surfaces(
|
|
112
|
+
app,
|
|
113
|
+
resolved_settings,
|
|
114
|
+
deployment=resolved_deployment,
|
|
115
|
+
diagnostics_prefix="/system",
|
|
116
|
+
)
|
|
117
|
+
_hide_disabled_control_plane_docs(
|
|
118
|
+
app,
|
|
119
|
+
resolved_deployment,
|
|
120
|
+
diagnostics_prefix="/system",
|
|
121
|
+
)
|
|
122
|
+
register_lifecycle(
|
|
123
|
+
app,
|
|
124
|
+
assembly_factory=runtime_assembly_factory,
|
|
125
|
+
surface_initializer=surface_router.initialize,
|
|
126
|
+
)
|
|
127
|
+
return AdminGate(
|
|
128
|
+
app,
|
|
129
|
+
deployment=resolved_deployment,
|
|
130
|
+
settings_obj=resolved_settings,
|
|
131
|
+
admin_path_prefixes=admin_resource_path_prefixes(resolved_deployment),
|
|
132
|
+
diagnostics_prefix="/system",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def build_application_runtime_plan(*args: object, **kwargs: object):
|
|
137
|
+
kwargs.setdefault(
|
|
138
|
+
"app_factory",
|
|
139
|
+
"tba_app_backend_core.app.build_app",
|
|
140
|
+
)
|
|
141
|
+
return _build_application_runtime_plan(*args, **kwargs)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
app = LazyASGIApplication(build_app)
|
|
145
|
+
|
|
146
|
+
__all__ = ["app", "build_app", "build_application_runtime_plan"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Layer-90 host-application plugin with HTTP surface ownership."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from tba_plugin_tigrbl import TbaPlugin as RuntimePlugin
|
|
8
|
+
|
|
9
|
+
from .surfaces import attach_runtime_surfaces
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from tigrbl import TigrblApp
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TbaBackendAppPlugin(RuntimePlugin):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
diagnostics_prefix: str = "/system",
|
|
20
|
+
settings: object | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(
|
|
23
|
+
surface_attacher=attach_runtime_surfaces,
|
|
24
|
+
diagnostics_prefix=diagnostics_prefix,
|
|
25
|
+
settings=settings,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def install(app: "TigrblApp", settings: object | None = None) -> "TigrblApp":
|
|
30
|
+
return TbaBackendAppPlugin(settings=settings).install(
|
|
31
|
+
app,
|
|
32
|
+
settings=settings,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["TbaBackendAppPlugin", "install"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Product-specific backend application assembly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from tba_runtime_identity.deployment import (
|
|
8
|
+
PRODUCT_SURFACE_REGISTRY,
|
|
9
|
+
resolve_deployment,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .app import build_app
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from tigrbl import TigrblApp
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_backend_app_contract(contract: object, deployment: object) -> None:
|
|
19
|
+
expected_surface = getattr(contract, "product_surface", None)
|
|
20
|
+
actual_surface = getattr(deployment, "product_surface", None)
|
|
21
|
+
if expected_surface != actual_surface:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
"backend-app contract product surface does not match deployment: "
|
|
24
|
+
f"{expected_surface!r} != {actual_surface!r}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
declared_routers = tuple(getattr(contract, "mounted_router_packages", ()))
|
|
28
|
+
resolved_routers = tuple(getattr(deployment, "router_packages", ()))
|
|
29
|
+
if declared_routers != resolved_routers:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"backend-app contract router closure does not match deployment: "
|
|
32
|
+
f"{declared_routers!r} != {resolved_routers!r}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_product_app(
|
|
37
|
+
product_surface: str,
|
|
38
|
+
settings_obj: object | None = None,
|
|
39
|
+
*,
|
|
40
|
+
contract: object | None = None,
|
|
41
|
+
) -> "TigrblApp":
|
|
42
|
+
"""Build the shared ASGI app constrained to one declared product surface."""
|
|
43
|
+
if product_surface not in PRODUCT_SURFACE_REGISTRY:
|
|
44
|
+
raise ValueError(f"unknown tba_auth product surface: {product_surface}")
|
|
45
|
+
deployment = resolve_deployment(
|
|
46
|
+
settings_obj,
|
|
47
|
+
product_surface=product_surface,
|
|
48
|
+
runtime_style="standalone",
|
|
49
|
+
)
|
|
50
|
+
if contract is not None:
|
|
51
|
+
validate_backend_app_contract(contract, deployment)
|
|
52
|
+
return build_app(settings_obj, deployment=deployment)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
__all__ = ["build_product_app", "validate_backend_app_contract"]
|
|
File without changes
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Executable release-path surface composition for Tigrbl-auth.
|
|
2
|
+
|
|
3
|
+
The capability identifiers used here must stay synchronized with
|
|
4
|
+
``tba_auth.config.surfaces.PUBLIC_CAPABILITIES`` so deployment resolution,
|
|
5
|
+
runtime mounting, contract generation, and verification all observe the same
|
|
6
|
+
installed surface shape.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import replace
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Final
|
|
14
|
+
|
|
15
|
+
from tigrbl import TigrblRouter
|
|
16
|
+
from tba_server_identity.admin_auth import admin_api as admin_auth_api
|
|
17
|
+
from tba_runtime_identity.deployment import ResolvedDeployment, resolve_deployment
|
|
18
|
+
from tba_router_administration_gate import ADMIN_OPENAPI_SECURITY_DEPENDENCIES
|
|
19
|
+
from tba_storage_core import ensure_identity_storage_importable
|
|
20
|
+
from tba_storage_core.tables import (
|
|
21
|
+
AuditEvent,
|
|
22
|
+
AuthCode,
|
|
23
|
+
AuthSession,
|
|
24
|
+
Client,
|
|
25
|
+
ClientRegistration,
|
|
26
|
+
Consent,
|
|
27
|
+
CredentialApiKey,
|
|
28
|
+
CredentialServiceKey,
|
|
29
|
+
CryptoKey,
|
|
30
|
+
CryptoKeyVersion,
|
|
31
|
+
KeyAttestationEvidence,
|
|
32
|
+
KeyEnvelope,
|
|
33
|
+
KeyRotationEvent,
|
|
34
|
+
LogoutState,
|
|
35
|
+
PrincipalKeyBinding,
|
|
36
|
+
PushedAuthorizationRequest,
|
|
37
|
+
Realm,
|
|
38
|
+
RevokedToken,
|
|
39
|
+
ServiceIdentity,
|
|
40
|
+
Tenant,
|
|
41
|
+
TokenRecord,
|
|
42
|
+
User,
|
|
43
|
+
)
|
|
44
|
+
from tba_runtime_identity_storage.engine import dsn
|
|
45
|
+
|
|
46
|
+
ensure_identity_storage_importable()
|
|
47
|
+
|
|
48
|
+
TABLE_RESOURCES = [
|
|
49
|
+
Realm,
|
|
50
|
+
Tenant,
|
|
51
|
+
User,
|
|
52
|
+
Client,
|
|
53
|
+
ClientRegistration,
|
|
54
|
+
CredentialApiKey,
|
|
55
|
+
ServiceIdentity,
|
|
56
|
+
CredentialServiceKey,
|
|
57
|
+
CryptoKey,
|
|
58
|
+
CryptoKeyVersion,
|
|
59
|
+
PrincipalKeyBinding,
|
|
60
|
+
KeyEnvelope,
|
|
61
|
+
KeyAttestationEvidence,
|
|
62
|
+
AuthSession,
|
|
63
|
+
AuthCode,
|
|
64
|
+
TokenRecord,
|
|
65
|
+
RevokedToken,
|
|
66
|
+
Consent,
|
|
67
|
+
LogoutState,
|
|
68
|
+
KeyRotationEvent,
|
|
69
|
+
AuditEvent,
|
|
70
|
+
PushedAuthorizationRequest,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
ADMIN_ROUTER_BINDINGS: Final[tuple[dict[str, Any], ...]] = (
|
|
74
|
+
{"mount_group": "admin_auth", "router": admin_auth_api},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TableInitializationScopeError(ValueError):
|
|
79
|
+
"""Raised when a product surface selects table resources outside its contract."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _admin_table_resources(
|
|
83
|
+
deployment: ResolvedDeployment | None = None,
|
|
84
|
+
) -> tuple[type[Any], ...]:
|
|
85
|
+
if deployment is None or getattr(deployment, "product_surface", None) is None:
|
|
86
|
+
return tuple(TABLE_RESOURCES)
|
|
87
|
+
required = getattr(deployment, "required_table_resources", ())
|
|
88
|
+
if required:
|
|
89
|
+
resources_by_name = {
|
|
90
|
+
resource.__name__: resource for resource in TABLE_RESOURCES
|
|
91
|
+
}
|
|
92
|
+
return tuple(
|
|
93
|
+
resources_by_name[name]
|
|
94
|
+
for name in tuple(str(item) for item in required)
|
|
95
|
+
if name in resources_by_name
|
|
96
|
+
)
|
|
97
|
+
return tuple(
|
|
98
|
+
resource
|
|
99
|
+
for resource in TABLE_RESOURCES
|
|
100
|
+
if deployment.admin_resource_enabled(resource.__name__)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _resource_name(resource: type[Any] | str) -> str:
|
|
105
|
+
return resource if isinstance(resource, str) else resource.__name__
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def required_table_resource_names(
|
|
109
|
+
deployment: ResolvedDeployment | None = None,
|
|
110
|
+
) -> tuple[str, ...]:
|
|
111
|
+
return tuple(resource.__name__ for resource in _admin_table_resources(deployment))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def table_initialization_manifest(
|
|
115
|
+
deployment: ResolvedDeployment | None = None,
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
return {
|
|
118
|
+
"product_surface": getattr(deployment, "product_surface", None),
|
|
119
|
+
"required_table_resources": list(required_table_resource_names(deployment)),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def assert_table_initialization_scope(
|
|
124
|
+
deployment: ResolvedDeployment,
|
|
125
|
+
selected_resources: tuple[type[Any] | str, ...],
|
|
126
|
+
) -> None:
|
|
127
|
+
if deployment.product_surface is None:
|
|
128
|
+
return
|
|
129
|
+
expected = set(required_table_resource_names(deployment))
|
|
130
|
+
selected = set(_resource_name(resource) for resource in selected_resources)
|
|
131
|
+
unexpected = sorted(selected - expected)
|
|
132
|
+
missing = sorted(expected - selected)
|
|
133
|
+
if unexpected or missing:
|
|
134
|
+
raise TableInitializationScopeError(
|
|
135
|
+
"product surface table initialization mismatch: "
|
|
136
|
+
f"unexpected={unexpected}, missing={missing}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _admin_resource_path(
|
|
141
|
+
resource: type[Any], deployment: ResolvedDeployment | None = None
|
|
142
|
+
) -> str:
|
|
143
|
+
if getattr(deployment, "product_surface", None) == "platform-admin-app":
|
|
144
|
+
if resource.__name__ == "Realm":
|
|
145
|
+
return "/admin/realm"
|
|
146
|
+
if resource.__name__ == "Tenant":
|
|
147
|
+
return "/admin/tenant"
|
|
148
|
+
if resource.__name__ == "User":
|
|
149
|
+
return "/admin/identity"
|
|
150
|
+
return f"/{resource.__name__.lower()}"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def admin_resource_path_prefixes(
|
|
154
|
+
deployment: ResolvedDeployment | None = None,
|
|
155
|
+
) -> tuple[str, ...]:
|
|
156
|
+
return tuple(
|
|
157
|
+
_admin_resource_path(resource, deployment)
|
|
158
|
+
for resource in _admin_table_resources(deployment)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _path_has_prefix(path: str, prefix: str) -> bool:
|
|
163
|
+
return path == prefix or path.startswith(f"{prefix}/")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _requires_admin_security_metadata(
|
|
167
|
+
path: str,
|
|
168
|
+
deployment: ResolvedDeployment,
|
|
169
|
+
*,
|
|
170
|
+
diagnostics_prefix: str,
|
|
171
|
+
) -> bool:
|
|
172
|
+
if deployment.flag_enabled("surface_admin_enabled") and any(
|
|
173
|
+
_path_has_prefix(path, prefix)
|
|
174
|
+
for prefix in admin_resource_path_prefixes(deployment)
|
|
175
|
+
):
|
|
176
|
+
return True
|
|
177
|
+
if deployment.flag_enabled("surface_diagnostics_enabled") and _path_has_prefix(
|
|
178
|
+
path, diagnostics_prefix
|
|
179
|
+
):
|
|
180
|
+
return True
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _attach_admin_security_metadata(
|
|
185
|
+
router: TigrblRouter,
|
|
186
|
+
deployment: ResolvedDeployment,
|
|
187
|
+
*,
|
|
188
|
+
diagnostics_prefix: str,
|
|
189
|
+
) -> None:
|
|
190
|
+
secured_routes = []
|
|
191
|
+
for route in getattr(router, "_routes", []):
|
|
192
|
+
route_path = getattr(route, "path_template", None) or getattr(route, "path", "")
|
|
193
|
+
if not _requires_admin_security_metadata(
|
|
194
|
+
str(route_path),
|
|
195
|
+
deployment,
|
|
196
|
+
diagnostics_prefix=diagnostics_prefix,
|
|
197
|
+
):
|
|
198
|
+
secured_routes.append(route)
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
existing = list(getattr(route, "dependencies", None) or [])
|
|
202
|
+
secured_routes.append(
|
|
203
|
+
replace(
|
|
204
|
+
route,
|
|
205
|
+
dependencies=existing + list(ADMIN_OPENAPI_SECURITY_DEPENDENCIES),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
router._routes[:] = secured_routes
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _route_pattern(path_template: str, param_names: tuple[str, ...]) -> re.Pattern[str]:
|
|
212
|
+
pattern = re.escape(path_template)
|
|
213
|
+
for name in param_names:
|
|
214
|
+
pattern = pattern.replace(r"\{" + name + r"\}", f"(?P<{name}>[^/]+)")
|
|
215
|
+
return re.compile(f"^{pattern}/?$")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _rewrite_model_rest_bindings(
|
|
219
|
+
model: type[Any],
|
|
220
|
+
*,
|
|
221
|
+
old_prefix: str,
|
|
222
|
+
new_prefix: str,
|
|
223
|
+
) -> None:
|
|
224
|
+
ops = getattr(model, "ops", None)
|
|
225
|
+
by_alias = getattr(ops, "by_alias", None)
|
|
226
|
+
if not isinstance(by_alias, dict):
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
for alias, specs in list(by_alias.items()):
|
|
230
|
+
rewritten_specs = []
|
|
231
|
+
changed = False
|
|
232
|
+
for spec in tuple(specs or ()):
|
|
233
|
+
next_bindings = []
|
|
234
|
+
spec_changed = False
|
|
235
|
+
for binding in tuple(getattr(spec, "bindings", ()) or ()):
|
|
236
|
+
path = getattr(binding, "path", None)
|
|
237
|
+
if isinstance(path, str) and path.startswith(old_prefix):
|
|
238
|
+
next_path = f"{new_prefix}{path[len(old_prefix) :]}"
|
|
239
|
+
next_bindings.append(replace(binding, path=next_path))
|
|
240
|
+
spec_changed = True
|
|
241
|
+
else:
|
|
242
|
+
next_bindings.append(binding)
|
|
243
|
+
if spec_changed:
|
|
244
|
+
rewritten_specs.append(replace(spec, bindings=tuple(next_bindings)))
|
|
245
|
+
changed = True
|
|
246
|
+
else:
|
|
247
|
+
rewritten_specs.append(spec)
|
|
248
|
+
if changed:
|
|
249
|
+
try:
|
|
250
|
+
by_alias[alias] = type(specs)(tuple(rewritten_specs))
|
|
251
|
+
except TypeError:
|
|
252
|
+
by_alias[alias] = tuple(rewritten_specs)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _rewrite_admin_table_routes(
|
|
256
|
+
router: TigrblRouter,
|
|
257
|
+
deployment: ResolvedDeployment,
|
|
258
|
+
) -> None:
|
|
259
|
+
if deployment.product_surface != "platform-admin-app":
|
|
260
|
+
return
|
|
261
|
+
rewrites = {
|
|
262
|
+
"Tenant": ("/tenant", "/admin/tenant"),
|
|
263
|
+
"User": ("/user", "/admin/identity"),
|
|
264
|
+
}
|
|
265
|
+
for resource in _admin_table_resources(deployment):
|
|
266
|
+
rewrite = rewrites.get(resource.__name__)
|
|
267
|
+
if rewrite is not None:
|
|
268
|
+
old_prefix, new_prefix = rewrite
|
|
269
|
+
_rewrite_model_rest_bindings(
|
|
270
|
+
resource,
|
|
271
|
+
old_prefix=old_prefix,
|
|
272
|
+
new_prefix=new_prefix,
|
|
273
|
+
)
|
|
274
|
+
rewritten = []
|
|
275
|
+
for route in getattr(router, "_routes", []):
|
|
276
|
+
model = getattr(route, "tigrbl_model", None)
|
|
277
|
+
resource_name = getattr(model, "__name__", "")
|
|
278
|
+
rewrite = rewrites.get(resource_name)
|
|
279
|
+
path_template = str(getattr(route, "path_template", "") or "")
|
|
280
|
+
if rewrite is None or not path_template.startswith(rewrite[0]):
|
|
281
|
+
rewritten.append(route)
|
|
282
|
+
continue
|
|
283
|
+
old_prefix, new_prefix = rewrite
|
|
284
|
+
next_path = f"{new_prefix}{path_template[len(old_prefix) :]}"
|
|
285
|
+
binding = getattr(route, "tigrbl_binding", None)
|
|
286
|
+
if binding is not None and hasattr(binding, "path"):
|
|
287
|
+
binding = replace(binding, path=next_path)
|
|
288
|
+
_rewrite_model_rest_bindings(
|
|
289
|
+
model,
|
|
290
|
+
old_prefix=old_prefix,
|
|
291
|
+
new_prefix=new_prefix,
|
|
292
|
+
)
|
|
293
|
+
rewritten.append(
|
|
294
|
+
replace(
|
|
295
|
+
route,
|
|
296
|
+
path_template=next_path,
|
|
297
|
+
pattern=_route_pattern(
|
|
298
|
+
next_path, tuple(getattr(route, "param_names", ()) or ())
|
|
299
|
+
),
|
|
300
|
+
tigrbl_binding=binding,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
router._routes[:] = rewritten
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _as_deployment(
|
|
307
|
+
settings_obj: object | None = None,
|
|
308
|
+
*,
|
|
309
|
+
deployment: ResolvedDeployment | None = None,
|
|
310
|
+
) -> ResolvedDeployment:
|
|
311
|
+
return deployment or resolve_deployment(settings_obj)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def include_admin_routes(
|
|
315
|
+
router: TigrblRouter,
|
|
316
|
+
settings_obj: object | None = None,
|
|
317
|
+
*,
|
|
318
|
+
deployment: ResolvedDeployment | None = None,
|
|
319
|
+
) -> tuple[type[Any], ...]:
|
|
320
|
+
deployment = _as_deployment(settings_obj, deployment=deployment)
|
|
321
|
+
selected_table_resources: tuple[type[Any], ...] = ()
|
|
322
|
+
if deployment.flag_enabled("surface_admin_enabled"):
|
|
323
|
+
selected_table_resources = _admin_table_resources(deployment)
|
|
324
|
+
if selected_table_resources:
|
|
325
|
+
if deployment.product_surface == "platform-admin-app":
|
|
326
|
+
for resource in selected_table_resources:
|
|
327
|
+
if resource.__name__ in {"Realm", "Tenant"}:
|
|
328
|
+
continue
|
|
329
|
+
router.include_table(resource)
|
|
330
|
+
else:
|
|
331
|
+
router.include_tables(selected_table_resources)
|
|
332
|
+
_rewrite_admin_table_routes(router, deployment)
|
|
333
|
+
for entry in ADMIN_ROUTER_BINDINGS:
|
|
334
|
+
if deployment.admin_rest_group_enabled(str(entry["mount_group"])):
|
|
335
|
+
router.include_router(entry["router"])
|
|
336
|
+
return selected_table_resources
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def build_admin_router(
|
|
340
|
+
settings_obj: object | None = None,
|
|
341
|
+
*,
|
|
342
|
+
deployment: ResolvedDeployment | None = None,
|
|
343
|
+
) -> TigrblRouter:
|
|
344
|
+
deployment = _as_deployment(settings_obj, deployment=deployment)
|
|
345
|
+
router = TigrblRouter(engine=dsn)
|
|
346
|
+
selected_table_resources = include_admin_routes(router, deployment=deployment)
|
|
347
|
+
assert_table_initialization_scope(deployment, selected_table_resources)
|
|
348
|
+
return router
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
AdminRouter = build_admin_router(
|
|
352
|
+
deployment=resolve_deployment(plugin_mode="admin-only")
|
|
353
|
+
)
|