tba-app-backend-platform-administration 0.4.0.dev2__tar.gz
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_platform_administration-0.4.0.dev2/PKG-INFO +67 -0
- tba_app_backend_platform_administration-0.4.0.dev2/README.md +42 -0
- tba_app_backend_platform_administration-0.4.0.dev2/pyproject.toml +28 -0
- tba_app_backend_platform_administration-0.4.0.dev2/src/tba_app_backend_platform_administration/__init__.py +33 -0
- tba_app_backend_platform_administration-0.4.0.dev2/src/tba_app_backend_platform_administration/app.py +138 -0
- tba_app_backend_platform_administration-0.4.0.dev2/src/tba_app_backend_platform_administration/contract.py +82 -0
- tba_app_backend_platform_administration-0.4.0.dev2/src/tba_app_backend_platform_administration/identities.py +213 -0
- tba_app_backend_platform_administration-0.4.0.dev2/src/tba_app_backend_platform_administration/py.typed +0 -0
- tba_app_backend_platform_administration-0.4.0.dev2/src/tba_app_backend_platform_administration/realms.py +267 -0
- tba_app_backend_platform_administration-0.4.0.dev2/src/tba_app_backend_platform_administration/tenants.py +195 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tba-app-backend-platform-administration
|
|
3
|
+
Version: 0.4.0.dev2
|
|
4
|
+
Summary: Platform owner and operator control-plane backend application for TBA.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10,<3.15
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: pydantic (==2.12.5)
|
|
14
|
+
Requires-Dist: tba-app-backend-core (==0.4.0.dev2)
|
|
15
|
+
Requires-Dist: tba-auth (==0.4.0.dev2)
|
|
16
|
+
Requires-Dist: tba-contracts-identity (==0.4.0.dev2)
|
|
17
|
+
Requires-Dist: tba-router-administration-gate (==0.4.0.dev2)
|
|
18
|
+
Requires-Dist: tba-server-identity (==0.4.0.dev2)
|
|
19
|
+
Requires-Dist: tigrbl (==0.4.5.dev8)
|
|
20
|
+
Project-URL: Homepage, https://github.com/tigrbl/tigrbl_auth
|
|
21
|
+
Project-URL: Repository, https://github.com/tigrbl/tigrbl_auth
|
|
22
|
+
Project-URL: Source, https://github.com/tigrbl/tigrbl_auth/tree/master/pkgs/90-backend-apps/tba-app-backend-platform-administration
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# tba-app-backend-platform-administration
|
|
26
|
+
|
|
27
|
+
Platform-scoped tenant lifecycle and authority-assignment API front door.
|
|
28
|
+
|
|
29
|
+
## Surface
|
|
30
|
+
|
|
31
|
+
| Property | Value |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| Product surface | `platform-admin-app` |
|
|
34
|
+
|
|
35
|
+
The backend app owns HTTP schemas and routes only. Tenant, realm, and identity
|
|
36
|
+
administration are invoked through request-scoped layer-40 capabilities composed
|
|
37
|
+
by `tba-server-identity`; durable table operations and secret hashing remain
|
|
38
|
+
in the layer-60 runtime adapters and their lower-layer providers.
|
|
39
|
+
| Intended UIX | `@tba/ui-platform-administration` |
|
|
40
|
+
| Exposes | Tenant lifecycle, platform authority assignment, admin identity lookup, audit/session/profile RPC |
|
|
41
|
+
| Excludes | Public login/consent pages, tenant self-service-only workflows, developer app registration surface |
|
|
42
|
+
|
|
43
|
+
## Entrypoint
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from tba_app_backend_platform_administration import app, build_app
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Local dev deployment
|
|
50
|
+
|
|
51
|
+
Run the isolated platform control-plane front door on port `8015`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
docker compose -f docker/docker-compose.platform-admin-app.yml up -d --build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The dev API key defaults to `dev-platform-admin-key`.
|
|
58
|
+
|
|
59
|
+
Useful endpoints:
|
|
60
|
+
|
|
61
|
+
| Endpoint | Purpose |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `/openapi.json` | Platform-admin REST/control-plane contract. |
|
|
64
|
+
| `/openrpc.json` | Platform-admin JSON-RPC method contract. |
|
|
65
|
+
| `/rpc` | API-key-protected JSON-RPC dispatch. |
|
|
66
|
+
| `/tenant` | API-key-protected platform tenant lifecycle resource. |
|
|
67
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# tba-app-backend-platform-administration
|
|
2
|
+
|
|
3
|
+
Platform-scoped tenant lifecycle and authority-assignment API front door.
|
|
4
|
+
|
|
5
|
+
## Surface
|
|
6
|
+
|
|
7
|
+
| Property | Value |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Product surface | `platform-admin-app` |
|
|
10
|
+
|
|
11
|
+
The backend app owns HTTP schemas and routes only. Tenant, realm, and identity
|
|
12
|
+
administration are invoked through request-scoped layer-40 capabilities composed
|
|
13
|
+
by `tba-server-identity`; durable table operations and secret hashing remain
|
|
14
|
+
in the layer-60 runtime adapters and their lower-layer providers.
|
|
15
|
+
| Intended UIX | `@tba/ui-platform-administration` |
|
|
16
|
+
| Exposes | Tenant lifecycle, platform authority assignment, admin identity lookup, audit/session/profile RPC |
|
|
17
|
+
| Excludes | Public login/consent pages, tenant self-service-only workflows, developer app registration surface |
|
|
18
|
+
|
|
19
|
+
## Entrypoint
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from tba_app_backend_platform_administration import app, build_app
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Local dev deployment
|
|
26
|
+
|
|
27
|
+
Run the isolated platform control-plane front door on port `8015`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
docker compose -f docker/docker-compose.platform-admin-app.yml up -d --build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The dev API key defaults to `dev-platform-admin-key`.
|
|
34
|
+
|
|
35
|
+
Useful endpoints:
|
|
36
|
+
|
|
37
|
+
| Endpoint | Purpose |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| `/openapi.json` | Platform-admin REST/control-plane contract. |
|
|
40
|
+
| `/openrpc.json` | Platform-admin JSON-RPC method contract. |
|
|
41
|
+
| `/rpc` | API-key-protected JSON-RPC dispatch. |
|
|
42
|
+
| `/tenant` | API-key-protected platform tenant lifecycle resource. |
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tba-app-backend-platform-administration"
|
|
3
|
+
version = "0.4.0.dev2"
|
|
4
|
+
description = "Platform owner and operator control-plane backend application for TBA."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10,<3.15"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"pydantic==2.12.5",
|
|
10
|
+
"tigrbl==0.4.5.dev8",
|
|
11
|
+
"tba-auth==0.4.0.dev2",
|
|
12
|
+
"tba-app-backend-core==0.4.0.dev2",
|
|
13
|
+
"tba-router-administration-gate==0.4.0.dev2",
|
|
14
|
+
"tba-contracts-identity==0.4.0.dev2",
|
|
15
|
+
"tba-server-identity==0.4.0.dev2",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/tigrbl/tigrbl_auth"
|
|
20
|
+
Repository = "https://github.com/tigrbl/tigrbl_auth"
|
|
21
|
+
Source = "https://github.com/tigrbl/tigrbl_auth/tree/master/pkgs/90-backend-apps/tba-app-backend-platform-administration"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["poetry-core>=1.0.0"]
|
|
25
|
+
build-backend = "poetry.core.masonry.api"
|
|
26
|
+
|
|
27
|
+
[tool.poetry]
|
|
28
|
+
packages = [{ include = "tba_app_backend_platform_administration", from = "src" }]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Platform admin backend application package for TBA."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .app import PRODUCT_SURFACE, app, build_app
|
|
6
|
+
from .contract import (
|
|
7
|
+
PLATFORM_ADMIN_BACKEND_APP_CONTRACT,
|
|
8
|
+
PlatformAdminBackendAppContract,
|
|
9
|
+
)
|
|
10
|
+
from .identities import (
|
|
11
|
+
AdminIdentityOut,
|
|
12
|
+
AdminIdentityProvisionIn,
|
|
13
|
+
AdminIdentityUpdateIn,
|
|
14
|
+
)
|
|
15
|
+
from .realms import AdminRealmOut, AdminRealmProvisionIn, AdminRealmUpdateIn
|
|
16
|
+
from .tenants import AdminTenantOut, AdminTenantProvisionIn, AdminTenantUpdateIn
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AdminIdentityOut",
|
|
20
|
+
"AdminIdentityProvisionIn",
|
|
21
|
+
"AdminIdentityUpdateIn",
|
|
22
|
+
"AdminRealmOut",
|
|
23
|
+
"AdminRealmProvisionIn",
|
|
24
|
+
"AdminRealmUpdateIn",
|
|
25
|
+
"AdminTenantOut",
|
|
26
|
+
"AdminTenantProvisionIn",
|
|
27
|
+
"AdminTenantUpdateIn",
|
|
28
|
+
"PRODUCT_SURFACE",
|
|
29
|
+
"PLATFORM_ADMIN_BACKEND_APP_CONTRACT",
|
|
30
|
+
"PlatformAdminBackendAppContract",
|
|
31
|
+
"app",
|
|
32
|
+
"build_app",
|
|
33
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""ASGI entrypoint for the platform admin TBA backend application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
from tba_app_backend_core import build_product_app
|
|
9
|
+
from tba_app_backend_platform_administration.contract import (
|
|
10
|
+
PLATFORM_ADMIN_BACKEND_APP_CONTRACT,
|
|
11
|
+
)
|
|
12
|
+
from tba_auth.runtime import LazyASGIApplication
|
|
13
|
+
from tba_app_backend_platform_administration.identities import router as identity_router
|
|
14
|
+
from tba_app_backend_platform_administration.realms import router as realm_router
|
|
15
|
+
from tba_app_backend_platform_administration.tenants import router as tenant_router
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from tigrbl import TigrblApp
|
|
19
|
+
|
|
20
|
+
PRODUCT_SURFACE = "platform-admin-app"
|
|
21
|
+
DEFAULT_CORS_ORIGINS = ("http://localhost:3011", "http://127.0.0.1:3011")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlatformAdminCors:
|
|
25
|
+
"""Small ASGI CORS wrapper for the standalone platform admin UIX demo."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, app: Any, origins: tuple[str, ...] = DEFAULT_CORS_ORIGINS
|
|
29
|
+
) -> None:
|
|
30
|
+
self.app = app
|
|
31
|
+
self.origins = origins
|
|
32
|
+
|
|
33
|
+
def __getattr__(self, name: str) -> Any:
|
|
34
|
+
return getattr(self.app, name)
|
|
35
|
+
|
|
36
|
+
async def __call__(
|
|
37
|
+
self,
|
|
38
|
+
scope: dict[str, Any],
|
|
39
|
+
receive: Callable[[], Awaitable[dict[str, Any]]],
|
|
40
|
+
send: Callable[[dict[str, Any]], Awaitable[None]],
|
|
41
|
+
) -> None:
|
|
42
|
+
if scope.get("type") != "http":
|
|
43
|
+
await self.app(scope, receive, send)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
headers = {
|
|
47
|
+
key.decode("latin-1").lower(): value.decode("latin-1")
|
|
48
|
+
for key, value in scope.get("headers", [])
|
|
49
|
+
}
|
|
50
|
+
origin = headers.get("origin")
|
|
51
|
+
allowed_origin = origin if origin in self.origins else None
|
|
52
|
+
|
|
53
|
+
if str(scope.get("method", "")).upper() == "OPTIONS" and allowed_origin:
|
|
54
|
+
await send(
|
|
55
|
+
{
|
|
56
|
+
"type": "http.response.start",
|
|
57
|
+
"status": 204,
|
|
58
|
+
"headers": self._cors_headers(allowed_origin)
|
|
59
|
+
+ [
|
|
60
|
+
(
|
|
61
|
+
b"access-control-allow-methods",
|
|
62
|
+
b"GET,POST,PATCH,DELETE,OPTIONS",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
b"access-control-allow-headers",
|
|
66
|
+
headers.get(
|
|
67
|
+
"access-control-request-headers",
|
|
68
|
+
"content-type,authorization,x-api-key",
|
|
69
|
+
).encode("latin-1"),
|
|
70
|
+
),
|
|
71
|
+
(b"content-length", b"0"),
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
await send({"type": "http.response.body", "body": b""})
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
async def cors_send(message: dict[str, Any]) -> None:
|
|
79
|
+
if message.get("type") == "http.response.start" and allowed_origin:
|
|
80
|
+
message = dict(message)
|
|
81
|
+
response_headers = list(message.get("headers", []))
|
|
82
|
+
response_headers.extend(self._cors_headers(allowed_origin))
|
|
83
|
+
message["headers"] = response_headers
|
|
84
|
+
await send(message)
|
|
85
|
+
|
|
86
|
+
await self.app(scope, receive, cors_send)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _cors_headers(origin: str) -> list[tuple[bytes, bytes]]:
|
|
90
|
+
return [
|
|
91
|
+
(b"access-control-allow-origin", origin.encode("latin-1")),
|
|
92
|
+
(b"access-control-allow-credentials", b"true"),
|
|
93
|
+
(b"vary", b"Origin"),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _default_settings() -> object:
|
|
98
|
+
from tba_auth.config.settings import Settings
|
|
99
|
+
|
|
100
|
+
settings = Settings()
|
|
101
|
+
env_overrides = {
|
|
102
|
+
"issuer": os.environ.get("AUTHN_ISSUER"),
|
|
103
|
+
"protected_resource_identifier": os.environ.get(
|
|
104
|
+
"TBA_AUTH_PROTECTED_RESOURCE_IDENTIFIER"
|
|
105
|
+
),
|
|
106
|
+
"deployment_profile": os.environ.get("TBA_AUTH_PROFILE"),
|
|
107
|
+
"require_tls": os.environ.get("TBA_AUTH_REQUIRE_TLS"),
|
|
108
|
+
"admin_api_key": os.environ.get("TBA_AUTH_ADMIN_API_KEY"),
|
|
109
|
+
"admin_api_key_dir": os.environ.get("TBA_AUTH_ADMIN_API_KEY_DIR"),
|
|
110
|
+
}
|
|
111
|
+
for name, value in env_overrides.items():
|
|
112
|
+
if value in {None, ""}:
|
|
113
|
+
continue
|
|
114
|
+
current = getattr(settings, name, None)
|
|
115
|
+
if isinstance(current, bool):
|
|
116
|
+
value = str(value).lower() in {"1", "true", "yes", "on"}
|
|
117
|
+
setattr(settings, name, value)
|
|
118
|
+
return settings
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def build_app(settings_obj: object | None = None) -> "TigrblApp":
|
|
122
|
+
if settings_obj is None:
|
|
123
|
+
settings_obj = _default_settings()
|
|
124
|
+
app = build_product_app(
|
|
125
|
+
PRODUCT_SURFACE, settings_obj, contract=PLATFORM_ADMIN_BACKEND_APP_CONTRACT
|
|
126
|
+
)
|
|
127
|
+
app.include_router(tenant_router)
|
|
128
|
+
app.include_router(realm_router)
|
|
129
|
+
app.include_router(identity_router)
|
|
130
|
+
app.admin_path_prefixes = tuple(
|
|
131
|
+
dict.fromkeys((*app.admin_path_prefixes, "/admin/identities"))
|
|
132
|
+
)
|
|
133
|
+
return PlatformAdminCors(app)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
app = LazyASGIApplication(build_app)
|
|
137
|
+
|
|
138
|
+
__all__ = ["PRODUCT_SURFACE", "app", "build_app"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Platform-admin backend-app contract for TBA."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class PlatformAdminBackendAppContract:
|
|
10
|
+
"""Machine-readable boundary for the platform control-plane backend app."""
|
|
11
|
+
|
|
12
|
+
product_surface: str
|
|
13
|
+
intended_uix: str
|
|
14
|
+
admin_resources: tuple[str, ...]
|
|
15
|
+
admin_rest_groups: tuple[str, ...]
|
|
16
|
+
forbidden_route_prefixes: tuple[str, ...]
|
|
17
|
+
forbidden_exact_routes: tuple[str, ...]
|
|
18
|
+
consumed_packages: tuple[str, ...]
|
|
19
|
+
mounted_router_packages: tuple[str, ...]
|
|
20
|
+
app_local_route_groups: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
PLATFORM_ADMIN_BACKEND_APP_CONTRACT = PlatformAdminBackendAppContract(
|
|
24
|
+
product_surface="platform-admin-app",
|
|
25
|
+
intended_uix="@tba/ui-platform-administration",
|
|
26
|
+
admin_resources=(
|
|
27
|
+
"Realm",
|
|
28
|
+
"Tenant",
|
|
29
|
+
"User",
|
|
30
|
+
"AuditEvent",
|
|
31
|
+
"CryptoKey",
|
|
32
|
+
"CryptoKeyVersion",
|
|
33
|
+
"PrincipalKeyBinding",
|
|
34
|
+
"KeyEnvelope",
|
|
35
|
+
"KeyAttestationEvidence",
|
|
36
|
+
"KeyRotationEvent",
|
|
37
|
+
),
|
|
38
|
+
admin_rest_groups=("admin_auth", "admin_realms", "admin_identities"),
|
|
39
|
+
forbidden_route_prefixes=(
|
|
40
|
+
"/client",
|
|
41
|
+
"/clientregistration",
|
|
42
|
+
"/consent",
|
|
43
|
+
"/serviceidentity",
|
|
44
|
+
"/credentialservicekey",
|
|
45
|
+
"/credentialapikey",
|
|
46
|
+
"/tokenrecord",
|
|
47
|
+
"/revokedtoken",
|
|
48
|
+
"/authsession",
|
|
49
|
+
"/tenant",
|
|
50
|
+
"/user",
|
|
51
|
+
),
|
|
52
|
+
forbidden_exact_routes=(
|
|
53
|
+
"/login",
|
|
54
|
+
"/authorize",
|
|
55
|
+
"/token",
|
|
56
|
+
"/register",
|
|
57
|
+
"/logout",
|
|
58
|
+
"/revoke",
|
|
59
|
+
"/userinfo",
|
|
60
|
+
"/introspect",
|
|
61
|
+
"/rpc",
|
|
62
|
+
"/openrpc.json",
|
|
63
|
+
"/.well-known/jwks.json",
|
|
64
|
+
"/.well-known/openid-configuration",
|
|
65
|
+
),
|
|
66
|
+
consumed_packages=(
|
|
67
|
+
"tba-auth",
|
|
68
|
+
"tba-server-identity",
|
|
69
|
+
"tba-runtime-identity",
|
|
70
|
+
"tba-runtime-identity-administration",
|
|
71
|
+
"tba-concrete-identity-principals",
|
|
72
|
+
"tba-runtime-authorization-policy",
|
|
73
|
+
),
|
|
74
|
+
mounted_router_packages=("tba-router-administration-gate",),
|
|
75
|
+
app_local_route_groups=(
|
|
76
|
+
"realms",
|
|
77
|
+
"tenants",
|
|
78
|
+
"identities",
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
__all__ = ["PLATFORM_ADMIN_BACKEND_APP_CONTRACT", "PlatformAdminBackendAppContract"]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Platform identity-administration HTTP routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field, constr
|
|
8
|
+
from tigrbl import TigrblRouter
|
|
9
|
+
from tigrbl.requests import Request
|
|
10
|
+
from tigrbl.runtime.status import HTTPException
|
|
11
|
+
from tigrbl.security import Depends
|
|
12
|
+
from tba_contracts_identity.admin_identities import (
|
|
13
|
+
AdminIdentity,
|
|
14
|
+
AdminIdentityCreate,
|
|
15
|
+
AdminIdentityUpdate,
|
|
16
|
+
IdentityAdministrationConflictError,
|
|
17
|
+
IdentityAdministrationNotFoundError,
|
|
18
|
+
IdentityAdministrationPolicyError,
|
|
19
|
+
IdentityAdministrationValidationError,
|
|
20
|
+
)
|
|
21
|
+
from tba_contracts_identity.admin_tenants import (
|
|
22
|
+
TenantAdministratorAuthenticationError,
|
|
23
|
+
)
|
|
24
|
+
from tba_server_identity.platform_identity_administration import (
|
|
25
|
+
get_db,
|
|
26
|
+
identity_administration_for_request,
|
|
27
|
+
require_identity_administrator,
|
|
28
|
+
)
|
|
29
|
+
from tba_router_administration_gate import ADMIN_OPENAPI_SECURITY_DEPENDENCIES
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_email = constr(strip_whitespace=True, min_length=3, max_length=120)
|
|
33
|
+
_password = constr(min_length=8, max_length=256)
|
|
34
|
+
_username = constr(strip_whitespace=True, min_length=3, max_length=80)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AdminIdentityOut(BaseModel):
|
|
38
|
+
id: str
|
|
39
|
+
tenant_id: str
|
|
40
|
+
username: str
|
|
41
|
+
email: str
|
|
42
|
+
is_active: bool = True
|
|
43
|
+
is_admin: bool = False
|
|
44
|
+
is_superuser: bool = False
|
|
45
|
+
must_change_password: bool = False
|
|
46
|
+
roles: list[str] = Field(default_factory=list)
|
|
47
|
+
created_at: str | None = None
|
|
48
|
+
updated_at: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AdminIdentityProvisionIn(BaseModel):
|
|
52
|
+
tenant_id: str
|
|
53
|
+
username: _username
|
|
54
|
+
email: _email
|
|
55
|
+
password: _password
|
|
56
|
+
is_admin: bool = False
|
|
57
|
+
is_superuser: bool = False
|
|
58
|
+
must_change_password: bool = True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AdminIdentityUpdateIn(BaseModel):
|
|
62
|
+
username: _username | None = None
|
|
63
|
+
email: _email | None = None
|
|
64
|
+
password: _password | None = None
|
|
65
|
+
is_active: bool | None = None
|
|
66
|
+
is_admin: bool | None = None
|
|
67
|
+
is_superuser: bool | None = None
|
|
68
|
+
must_change_password: bool | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _identity_payload(row: AdminIdentity) -> AdminIdentityOut:
|
|
72
|
+
return AdminIdentityOut(
|
|
73
|
+
id=row.identity_id,
|
|
74
|
+
tenant_id=row.tenant_id,
|
|
75
|
+
username=row.username,
|
|
76
|
+
email=row.email,
|
|
77
|
+
is_active=row.is_active,
|
|
78
|
+
is_admin=row.is_admin,
|
|
79
|
+
is_superuser=row.is_superuser,
|
|
80
|
+
must_change_password=row.must_change_password,
|
|
81
|
+
roles=list(row.roles),
|
|
82
|
+
created_at=row.created_at,
|
|
83
|
+
updated_at=row.updated_at,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def _invoke(
|
|
88
|
+
request: Request,
|
|
89
|
+
db: Any,
|
|
90
|
+
operation: str,
|
|
91
|
+
*args: object,
|
|
92
|
+
) -> object:
|
|
93
|
+
try:
|
|
94
|
+
actor = await require_identity_administrator(request, db)
|
|
95
|
+
capability = identity_administration_for_request(request, db)
|
|
96
|
+
return (await capability.call(operation, actor, *args)).value
|
|
97
|
+
except TenantAdministratorAuthenticationError as exc:
|
|
98
|
+
raise HTTPException(401, str(exc)) from exc
|
|
99
|
+
except IdentityAdministrationPolicyError as exc:
|
|
100
|
+
raise HTTPException(403, str(exc)) from exc
|
|
101
|
+
except IdentityAdministrationNotFoundError as exc:
|
|
102
|
+
raise HTTPException(404, str(exc)) from exc
|
|
103
|
+
except IdentityAdministrationConflictError as exc:
|
|
104
|
+
raise HTTPException(409, str(exc)) from exc
|
|
105
|
+
except IdentityAdministrationValidationError as exc:
|
|
106
|
+
raise HTTPException(400, str(exc)) from exc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
router = TigrblRouter()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.route(
|
|
113
|
+
"/admin/identities",
|
|
114
|
+
methods=["GET"],
|
|
115
|
+
response_model=list[AdminIdentityOut],
|
|
116
|
+
dependencies=list(ADMIN_OPENAPI_SECURITY_DEPENDENCIES),
|
|
117
|
+
)
|
|
118
|
+
async def admin_list_identities(
|
|
119
|
+
request: Request,
|
|
120
|
+
tenant_id: str | None = None,
|
|
121
|
+
db: Any = Depends(get_db),
|
|
122
|
+
) -> list[AdminIdentityOut]:
|
|
123
|
+
rows = await _invoke(request, db, "list_identities", tenant_id)
|
|
124
|
+
return [_identity_payload(row) for row in rows]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@router.route(
|
|
128
|
+
"/admin/identities",
|
|
129
|
+
methods=["POST"],
|
|
130
|
+
response_model=AdminIdentityOut,
|
|
131
|
+
dependencies=list(ADMIN_OPENAPI_SECURITY_DEPENDENCIES),
|
|
132
|
+
)
|
|
133
|
+
async def admin_create_identity(
|
|
134
|
+
request: Request,
|
|
135
|
+
payload: AdminIdentityProvisionIn | None = None,
|
|
136
|
+
db: Any = Depends(get_db),
|
|
137
|
+
) -> AdminIdentityOut:
|
|
138
|
+
if payload is None:
|
|
139
|
+
payload = AdminIdentityProvisionIn.model_validate(await request.json() or {})
|
|
140
|
+
row = await _invoke(
|
|
141
|
+
request,
|
|
142
|
+
db,
|
|
143
|
+
"create_identity",
|
|
144
|
+
AdminIdentityCreate(
|
|
145
|
+
tenant_id=payload.tenant_id,
|
|
146
|
+
username=payload.username,
|
|
147
|
+
email=str(payload.email),
|
|
148
|
+
password=payload.password,
|
|
149
|
+
is_admin=payload.is_admin,
|
|
150
|
+
is_superuser=payload.is_superuser,
|
|
151
|
+
must_change_password=payload.must_change_password,
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
return _identity_payload(row)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@router.route(
|
|
158
|
+
"/admin/identities/{user_id}",
|
|
159
|
+
methods=["PATCH"],
|
|
160
|
+
response_model=AdminIdentityOut,
|
|
161
|
+
dependencies=list(ADMIN_OPENAPI_SECURITY_DEPENDENCIES),
|
|
162
|
+
)
|
|
163
|
+
async def admin_update_identity(
|
|
164
|
+
request: Request,
|
|
165
|
+
user_id: str,
|
|
166
|
+
payload: AdminIdentityUpdateIn | None = None,
|
|
167
|
+
db: Any = Depends(get_db),
|
|
168
|
+
) -> AdminIdentityOut:
|
|
169
|
+
if payload is None:
|
|
170
|
+
payload = AdminIdentityUpdateIn.model_validate(await request.json() or {})
|
|
171
|
+
row = await _invoke(
|
|
172
|
+
request,
|
|
173
|
+
db,
|
|
174
|
+
"update_identity",
|
|
175
|
+
user_id,
|
|
176
|
+
AdminIdentityUpdate(
|
|
177
|
+
username=payload.username,
|
|
178
|
+
email=str(payload.email) if payload.email is not None else None,
|
|
179
|
+
password=payload.password,
|
|
180
|
+
is_active=payload.is_active,
|
|
181
|
+
is_admin=payload.is_admin,
|
|
182
|
+
is_superuser=payload.is_superuser,
|
|
183
|
+
must_change_password=payload.must_change_password,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
return _identity_payload(row)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@router.route(
|
|
190
|
+
"/admin/identities/{user_id}",
|
|
191
|
+
methods=["DELETE"],
|
|
192
|
+
response_model=AdminIdentityOut,
|
|
193
|
+
dependencies=list(ADMIN_OPENAPI_SECURITY_DEPENDENCIES),
|
|
194
|
+
)
|
|
195
|
+
async def admin_delete_identity(
|
|
196
|
+
request: Request,
|
|
197
|
+
user_id: str,
|
|
198
|
+
db: Any = Depends(get_db),
|
|
199
|
+
) -> AdminIdentityOut:
|
|
200
|
+
row = await _invoke(request, db, "delete_identity", user_id)
|
|
201
|
+
return _identity_payload(row)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
__all__ = [
|
|
205
|
+
"AdminIdentityOut",
|
|
206
|
+
"AdminIdentityProvisionIn",
|
|
207
|
+
"AdminIdentityUpdateIn",
|
|
208
|
+
"admin_create_identity",
|
|
209
|
+
"admin_delete_identity",
|
|
210
|
+
"admin_list_identities",
|
|
211
|
+
"admin_update_identity",
|
|
212
|
+
"router",
|
|
213
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Platform realm-administration HTTP routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, constr
|
|
8
|
+
from tigrbl import TigrblRouter
|
|
9
|
+
from tigrbl.requests import Request
|
|
10
|
+
from tigrbl.runtime.status import HTTPException
|
|
11
|
+
from tigrbl.security import Depends
|
|
12
|
+
from tba_router_administration_gate import ADMIN_OPENAPI_SECURITY_DEPENDENCIES
|
|
13
|
+
from tba_contracts_identity.admin_realms import (
|
|
14
|
+
AdminRealm,
|
|
15
|
+
AdminRealmCreate,
|
|
16
|
+
AdminRealmUpdate,
|
|
17
|
+
RealmAdministrationConflictError,
|
|
18
|
+
RealmAdministrationNotFoundError,
|
|
19
|
+
RealmAdministrationValidationError,
|
|
20
|
+
)
|
|
21
|
+
from tba_contracts_identity.admin_tenants import (
|
|
22
|
+
AdminTenantCreate,
|
|
23
|
+
TenantAdministrationConflictError,
|
|
24
|
+
TenantAdministrationNotFoundError,
|
|
25
|
+
TenantAdministrationPolicyError,
|
|
26
|
+
TenantAdministrationValidationError,
|
|
27
|
+
TenantAdministratorAuthenticationError,
|
|
28
|
+
)
|
|
29
|
+
from tba_server_identity.platform_realm_administration import (
|
|
30
|
+
get_db,
|
|
31
|
+
realm_administration_for_request,
|
|
32
|
+
require_realm_administrator,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from .tenants import AdminTenantOut, AdminTenantProvisionIn, tenant_payload
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_description = constr(strip_whitespace=True, max_length=255)
|
|
39
|
+
_issuer_path_value = constr(strip_whitespace=True, max_length=255)
|
|
40
|
+
_name = constr(strip_whitespace=True, min_length=1, max_length=120)
|
|
41
|
+
_slug = constr(strip_whitespace=True, min_length=3, max_length=120)
|
|
42
|
+
_SECURITY = list(ADMIN_OPENAPI_SECURITY_DEPENDENCIES)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AdminRealmOut(BaseModel):
|
|
46
|
+
id: str
|
|
47
|
+
slug: str
|
|
48
|
+
name: str
|
|
49
|
+
issuer_path: str = ""
|
|
50
|
+
description: str | None = None
|
|
51
|
+
created_at: str | None = None
|
|
52
|
+
updated_at: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AdminRealmProvisionIn(BaseModel):
|
|
56
|
+
slug: _slug
|
|
57
|
+
name: _name
|
|
58
|
+
issuer_path: _issuer_path_value | None = None
|
|
59
|
+
description: _description | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AdminRealmUpdateIn(BaseModel):
|
|
63
|
+
slug: _slug | None = None
|
|
64
|
+
name: _name | None = None
|
|
65
|
+
issuer_path: _issuer_path_value | None = None
|
|
66
|
+
description: _description | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _realm_payload(row: AdminRealm) -> AdminRealmOut:
|
|
70
|
+
return AdminRealmOut(
|
|
71
|
+
id=row.realm_id,
|
|
72
|
+
slug=row.slug,
|
|
73
|
+
name=row.name,
|
|
74
|
+
issuer_path=row.issuer_path,
|
|
75
|
+
description=row.description,
|
|
76
|
+
created_at=row.created_at,
|
|
77
|
+
updated_at=row.updated_at,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _invoke(
|
|
82
|
+
request: Request,
|
|
83
|
+
db: Any,
|
|
84
|
+
operation: str,
|
|
85
|
+
*args: object,
|
|
86
|
+
) -> object:
|
|
87
|
+
try:
|
|
88
|
+
actor = await require_realm_administrator(request, db)
|
|
89
|
+
capability = realm_administration_for_request(request, db)
|
|
90
|
+
return (await capability.call(operation, actor, *args)).value
|
|
91
|
+
except TenantAdministratorAuthenticationError as exc:
|
|
92
|
+
raise HTTPException(401, str(exc)) from exc
|
|
93
|
+
except TenantAdministrationPolicyError as exc:
|
|
94
|
+
raise HTTPException(403, str(exc)) from exc
|
|
95
|
+
except (RealmAdministrationNotFoundError, TenantAdministrationNotFoundError) as exc:
|
|
96
|
+
raise HTTPException(404, str(exc)) from exc
|
|
97
|
+
except (RealmAdministrationConflictError, TenantAdministrationConflictError) as exc:
|
|
98
|
+
raise HTTPException(409, str(exc)) from exc
|
|
99
|
+
except (
|
|
100
|
+
RealmAdministrationValidationError,
|
|
101
|
+
TenantAdministrationValidationError,
|
|
102
|
+
) as exc:
|
|
103
|
+
raise HTTPException(400, str(exc)) from exc
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
router = TigrblRouter()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@router.route(
|
|
110
|
+
"/admin/realm",
|
|
111
|
+
methods=["GET"],
|
|
112
|
+
response_model=list[AdminRealmOut],
|
|
113
|
+
dependencies=_SECURITY,
|
|
114
|
+
)
|
|
115
|
+
async def admin_list_realms(
|
|
116
|
+
request: Request,
|
|
117
|
+
db: Any = Depends(get_db),
|
|
118
|
+
) -> list[AdminRealmOut]:
|
|
119
|
+
rows = await _invoke(request, db, "list_realms")
|
|
120
|
+
return [_realm_payload(row) for row in rows]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@router.route(
|
|
124
|
+
"/admin/realm",
|
|
125
|
+
methods=["POST"],
|
|
126
|
+
response_model=AdminRealmOut,
|
|
127
|
+
dependencies=_SECURITY,
|
|
128
|
+
)
|
|
129
|
+
async def admin_create_realm(
|
|
130
|
+
request: Request,
|
|
131
|
+
payload: AdminRealmProvisionIn | None = None,
|
|
132
|
+
db: Any = Depends(get_db),
|
|
133
|
+
) -> AdminRealmOut:
|
|
134
|
+
if payload is None:
|
|
135
|
+
payload = AdminRealmProvisionIn.model_validate(await request.json() or {})
|
|
136
|
+
row = await _invoke(
|
|
137
|
+
request,
|
|
138
|
+
db,
|
|
139
|
+
"create_realm",
|
|
140
|
+
AdminRealmCreate(
|
|
141
|
+
slug=payload.slug,
|
|
142
|
+
name=payload.name,
|
|
143
|
+
issuer_path=payload.issuer_path,
|
|
144
|
+
description=payload.description,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
return _realm_payload(row)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@router.route(
|
|
151
|
+
"/admin/realm/{realm_id}",
|
|
152
|
+
methods=["GET"],
|
|
153
|
+
response_model=AdminRealmOut,
|
|
154
|
+
dependencies=_SECURITY,
|
|
155
|
+
)
|
|
156
|
+
async def admin_get_realm(
|
|
157
|
+
request: Request,
|
|
158
|
+
realm_id: str,
|
|
159
|
+
db: Any = Depends(get_db),
|
|
160
|
+
) -> AdminRealmOut:
|
|
161
|
+
row = await _invoke(request, db, "read_realm", realm_id)
|
|
162
|
+
if row is None:
|
|
163
|
+
raise HTTPException(404, "realm not found")
|
|
164
|
+
return _realm_payload(row)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@router.route(
|
|
168
|
+
"/admin/realm/{realm_id}",
|
|
169
|
+
methods=["PATCH"],
|
|
170
|
+
response_model=AdminRealmOut,
|
|
171
|
+
dependencies=_SECURITY,
|
|
172
|
+
)
|
|
173
|
+
async def admin_update_realm(
|
|
174
|
+
request: Request,
|
|
175
|
+
realm_id: str,
|
|
176
|
+
payload: AdminRealmUpdateIn | None = None,
|
|
177
|
+
db: Any = Depends(get_db),
|
|
178
|
+
) -> AdminRealmOut:
|
|
179
|
+
if payload is None:
|
|
180
|
+
payload = AdminRealmUpdateIn.model_validate(await request.json() or {})
|
|
181
|
+
row = await _invoke(
|
|
182
|
+
request,
|
|
183
|
+
db,
|
|
184
|
+
"update_realm",
|
|
185
|
+
realm_id,
|
|
186
|
+
AdminRealmUpdate(
|
|
187
|
+
slug=payload.slug,
|
|
188
|
+
name=payload.name,
|
|
189
|
+
issuer_path=payload.issuer_path,
|
|
190
|
+
description=payload.description,
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
return _realm_payload(row)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@router.route(
|
|
197
|
+
"/admin/realm/{realm_id}",
|
|
198
|
+
methods=["DELETE"],
|
|
199
|
+
response_model=AdminRealmOut,
|
|
200
|
+
dependencies=_SECURITY,
|
|
201
|
+
)
|
|
202
|
+
async def admin_delete_realm(
|
|
203
|
+
request: Request,
|
|
204
|
+
realm_id: str,
|
|
205
|
+
db: Any = Depends(get_db),
|
|
206
|
+
) -> AdminRealmOut:
|
|
207
|
+
row = await _invoke(request, db, "delete_realm", realm_id)
|
|
208
|
+
return _realm_payload(row)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@router.route(
|
|
212
|
+
"/admin/realm/{realm_id}/tenant",
|
|
213
|
+
methods=["GET"],
|
|
214
|
+
response_model=list[AdminTenantOut],
|
|
215
|
+
dependencies=_SECURITY,
|
|
216
|
+
)
|
|
217
|
+
async def admin_list_realm_tenants(
|
|
218
|
+
request: Request,
|
|
219
|
+
realm_id: str,
|
|
220
|
+
db: Any = Depends(get_db),
|
|
221
|
+
) -> list[AdminTenantOut]:
|
|
222
|
+
rows = await _invoke(request, db, "list_realm_tenants", realm_id)
|
|
223
|
+
return [tenant_payload(row) for row in rows]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@router.route(
|
|
227
|
+
"/admin/realm/{realm_id}/tenant",
|
|
228
|
+
methods=["POST"],
|
|
229
|
+
response_model=AdminTenantOut,
|
|
230
|
+
dependencies=_SECURITY,
|
|
231
|
+
)
|
|
232
|
+
async def admin_create_realm_tenant(
|
|
233
|
+
request: Request,
|
|
234
|
+
realm_id: str,
|
|
235
|
+
payload: AdminTenantProvisionIn | None = None,
|
|
236
|
+
db: Any = Depends(get_db),
|
|
237
|
+
) -> AdminTenantOut:
|
|
238
|
+
if payload is None:
|
|
239
|
+
payload = AdminTenantProvisionIn.model_validate(await request.json() or {})
|
|
240
|
+
row = await _invoke(
|
|
241
|
+
request,
|
|
242
|
+
db,
|
|
243
|
+
"create_realm_tenant",
|
|
244
|
+
realm_id,
|
|
245
|
+
AdminTenantCreate(
|
|
246
|
+
realm_id=realm_id,
|
|
247
|
+
slug=payload.slug,
|
|
248
|
+
name=payload.name,
|
|
249
|
+
email=payload.email,
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
return tenant_payload(row)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = [
|
|
256
|
+
"AdminRealmOut",
|
|
257
|
+
"AdminRealmProvisionIn",
|
|
258
|
+
"AdminRealmUpdateIn",
|
|
259
|
+
"admin_create_realm",
|
|
260
|
+
"admin_create_realm_tenant",
|
|
261
|
+
"admin_delete_realm",
|
|
262
|
+
"admin_get_realm",
|
|
263
|
+
"admin_list_realm_tenants",
|
|
264
|
+
"admin_list_realms",
|
|
265
|
+
"admin_update_realm",
|
|
266
|
+
"router",
|
|
267
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Platform tenant-administration HTTP routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, constr
|
|
8
|
+
from tigrbl import TigrblRouter
|
|
9
|
+
from tigrbl.requests import Request
|
|
10
|
+
from tigrbl.runtime.status import HTTPException
|
|
11
|
+
from tigrbl.security import Depends
|
|
12
|
+
from tba_router_administration_gate import ADMIN_OPENAPI_SECURITY_DEPENDENCIES
|
|
13
|
+
from tba_contracts_identity.admin_tenants import (
|
|
14
|
+
AdminTenant,
|
|
15
|
+
AdminTenantCreate,
|
|
16
|
+
AdminTenantUpdate,
|
|
17
|
+
TenantAdministrationConflictError,
|
|
18
|
+
TenantAdministrationNotFoundError,
|
|
19
|
+
TenantAdministrationPolicyError,
|
|
20
|
+
TenantAdministrationValidationError,
|
|
21
|
+
TenantAdministratorAuthenticationError,
|
|
22
|
+
)
|
|
23
|
+
from tba_server_identity.platform_tenant_administration import (
|
|
24
|
+
get_db,
|
|
25
|
+
require_tenant_administrator,
|
|
26
|
+
tenant_administration_for_request,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_email = constr(strip_whitespace=True, min_length=3, max_length=120)
|
|
31
|
+
_name = constr(strip_whitespace=True, min_length=1, max_length=120)
|
|
32
|
+
_slug = constr(strip_whitespace=True, min_length=3, max_length=120)
|
|
33
|
+
_SECURITY = list(ADMIN_OPENAPI_SECURITY_DEPENDENCIES)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AdminTenantOut(BaseModel):
|
|
37
|
+
id: str
|
|
38
|
+
realm_id: str | None = None
|
|
39
|
+
slug: str
|
|
40
|
+
name: str
|
|
41
|
+
email: str
|
|
42
|
+
created_at: str | None = None
|
|
43
|
+
updated_at: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AdminTenantProvisionIn(BaseModel):
|
|
47
|
+
realm_id: str | None = None
|
|
48
|
+
slug: _slug
|
|
49
|
+
name: _name
|
|
50
|
+
email: _email
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AdminTenantUpdateIn(BaseModel):
|
|
54
|
+
realm_id: str | None = None
|
|
55
|
+
slug: _slug | None = None
|
|
56
|
+
name: _name | None = None
|
|
57
|
+
email: _email | None = None
|
|
58
|
+
is_active: bool | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tenant_payload(row: AdminTenant) -> AdminTenantOut:
|
|
62
|
+
return AdminTenantOut(
|
|
63
|
+
id=row.tenant_id,
|
|
64
|
+
realm_id=row.realm_id,
|
|
65
|
+
slug=row.slug,
|
|
66
|
+
name=row.name,
|
|
67
|
+
email=row.email,
|
|
68
|
+
created_at=row.created_at,
|
|
69
|
+
updated_at=row.updated_at,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def _invoke(
|
|
74
|
+
request: Request,
|
|
75
|
+
db: Any,
|
|
76
|
+
operation: str,
|
|
77
|
+
*args: object,
|
|
78
|
+
) -> object:
|
|
79
|
+
try:
|
|
80
|
+
actor = await require_tenant_administrator(request, db)
|
|
81
|
+
capability = tenant_administration_for_request(request, db)
|
|
82
|
+
return (await capability.call(operation, actor, *args)).value
|
|
83
|
+
except TenantAdministratorAuthenticationError as exc:
|
|
84
|
+
raise HTTPException(401, str(exc)) from exc
|
|
85
|
+
except TenantAdministrationPolicyError as exc:
|
|
86
|
+
raise HTTPException(403, str(exc)) from exc
|
|
87
|
+
except TenantAdministrationNotFoundError as exc:
|
|
88
|
+
raise HTTPException(404, str(exc)) from exc
|
|
89
|
+
except TenantAdministrationConflictError as exc:
|
|
90
|
+
raise HTTPException(409, str(exc)) from exc
|
|
91
|
+
except TenantAdministrationValidationError as exc:
|
|
92
|
+
raise HTTPException(400, str(exc)) from exc
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
admin_router = router = TigrblRouter()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@router.route(
|
|
99
|
+
"/admin/tenant",
|
|
100
|
+
methods=["GET"],
|
|
101
|
+
response_model=list[AdminTenantOut],
|
|
102
|
+
dependencies=_SECURITY,
|
|
103
|
+
)
|
|
104
|
+
async def admin_list_tenants(
|
|
105
|
+
request: Request,
|
|
106
|
+
db: Any = Depends(get_db),
|
|
107
|
+
) -> list[AdminTenantOut]:
|
|
108
|
+
rows = await _invoke(request, db, "list_tenants")
|
|
109
|
+
return [tenant_payload(row) for row in rows]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.route(
|
|
113
|
+
"/admin/tenant",
|
|
114
|
+
methods=["POST"],
|
|
115
|
+
response_model=AdminTenantOut,
|
|
116
|
+
dependencies=_SECURITY,
|
|
117
|
+
)
|
|
118
|
+
async def admin_create_tenant(
|
|
119
|
+
request: Request,
|
|
120
|
+
payload: AdminTenantProvisionIn | None = None,
|
|
121
|
+
db: Any = Depends(get_db),
|
|
122
|
+
) -> AdminTenantOut:
|
|
123
|
+
if payload is None:
|
|
124
|
+
payload = AdminTenantProvisionIn.model_validate(await request.json() or {})
|
|
125
|
+
row = await _invoke(
|
|
126
|
+
request,
|
|
127
|
+
db,
|
|
128
|
+
"create_tenant",
|
|
129
|
+
AdminTenantCreate(
|
|
130
|
+
realm_id=payload.realm_id,
|
|
131
|
+
slug=payload.slug,
|
|
132
|
+
name=payload.name,
|
|
133
|
+
email=payload.email,
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
return tenant_payload(row)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@router.route(
|
|
140
|
+
"/admin/tenant/{tenant_id}",
|
|
141
|
+
methods=["DELETE"],
|
|
142
|
+
response_model=AdminTenantOut,
|
|
143
|
+
dependencies=_SECURITY,
|
|
144
|
+
)
|
|
145
|
+
async def admin_delete_tenant(
|
|
146
|
+
request: Request,
|
|
147
|
+
tenant_id: str,
|
|
148
|
+
db: Any = Depends(get_db),
|
|
149
|
+
) -> AdminTenantOut:
|
|
150
|
+
row = await _invoke(request, db, "delete_tenant", tenant_id)
|
|
151
|
+
return tenant_payload(row)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@router.route(
|
|
155
|
+
"/admin/tenant/{tenant_id}",
|
|
156
|
+
methods=["PATCH"],
|
|
157
|
+
response_model=AdminTenantOut,
|
|
158
|
+
dependencies=_SECURITY,
|
|
159
|
+
)
|
|
160
|
+
async def admin_update_tenant(
|
|
161
|
+
request: Request,
|
|
162
|
+
tenant_id: str,
|
|
163
|
+
payload: AdminTenantUpdateIn | None = None,
|
|
164
|
+
db: Any = Depends(get_db),
|
|
165
|
+
) -> AdminTenantOut:
|
|
166
|
+
if payload is None:
|
|
167
|
+
payload = AdminTenantUpdateIn.model_validate(await request.json() or {})
|
|
168
|
+
row = await _invoke(
|
|
169
|
+
request,
|
|
170
|
+
db,
|
|
171
|
+
"update_tenant",
|
|
172
|
+
tenant_id,
|
|
173
|
+
AdminTenantUpdate(
|
|
174
|
+
realm_id=payload.realm_id,
|
|
175
|
+
slug=payload.slug,
|
|
176
|
+
name=payload.name,
|
|
177
|
+
email=payload.email,
|
|
178
|
+
is_active=payload.is_active,
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
return tenant_payload(row)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
__all__ = [
|
|
185
|
+
"AdminTenantOut",
|
|
186
|
+
"AdminTenantProvisionIn",
|
|
187
|
+
"AdminTenantUpdateIn",
|
|
188
|
+
"admin_create_tenant",
|
|
189
|
+
"admin_delete_tenant",
|
|
190
|
+
"admin_list_tenants",
|
|
191
|
+
"admin_router",
|
|
192
|
+
"admin_update_tenant",
|
|
193
|
+
"router",
|
|
194
|
+
"tenant_payload",
|
|
195
|
+
]
|