dominus-sdk-python-dev 2.1.8__py3-none-any.whl → 2.3.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.
- dominus/__init__.py +80 -19
- dominus/config/__init__.py +4 -4
- dominus/config/endpoints.py +18 -5
- dominus/namespaces/admin.py +58 -0
- dominus/namespaces/auth.py +539 -10
- dominus/namespaces/db.py +26 -1
- dominus/namespaces/ddl.py +222 -0
- dominus/namespaces/fastapi.py +252 -0
- dominus/namespaces/files.py +180 -0
- dominus/namespaces/secure.py +256 -0
- dominus/start.py +22 -0
- {dominus_sdk_python_dev-2.1.8.dist-info → dominus_sdk_python_dev-2.3.0.dist-info}/METADATA +215 -28
- {dominus_sdk_python_dev-2.1.8.dist-info → dominus_sdk_python_dev-2.3.0.dist-info}/RECORD +15 -12
- {dominus_sdk_python_dev-2.1.8.dist-info → dominus_sdk_python_dev-2.3.0.dist-info}/WHEEL +0 -0
- {dominus_sdk_python_dev-2.1.8.dist-info → dominus_sdk_python_dev-2.3.0.dist-info}/top_level.txt +0 -0
dominus/namespaces/db.py
CHANGED
|
@@ -45,6 +45,24 @@ class DbNamespace:
|
|
|
45
45
|
def __init__(self, client: "Dominus"):
|
|
46
46
|
self._client = client
|
|
47
47
|
|
|
48
|
+
async def schemas(self) -> List[str]:
|
|
49
|
+
"""
|
|
50
|
+
List accessible schemas.
|
|
51
|
+
|
|
52
|
+
Returns schemas that the current user can access (public, tenant_*, etc.)
|
|
53
|
+
Excludes system schemas (auth, logs, meta, object, etc.)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of schema names
|
|
57
|
+
"""
|
|
58
|
+
result = await self._client._request(
|
|
59
|
+
endpoint="/api/scribe/schema/list",
|
|
60
|
+
method="GET"
|
|
61
|
+
)
|
|
62
|
+
if isinstance(result, list):
|
|
63
|
+
return result
|
|
64
|
+
return result.get("schemas", [])
|
|
65
|
+
|
|
48
66
|
async def tables(self, schema: str = "public") -> List[Dict[str, Any]]:
|
|
49
67
|
"""
|
|
50
68
|
List tables in a schema.
|
|
@@ -123,11 +141,18 @@ class DbNamespace:
|
|
|
123
141
|
if actor:
|
|
124
142
|
body["actor"] = actor
|
|
125
143
|
|
|
126
|
-
|
|
144
|
+
# Backend returns {items, total, limit, offset} but we normalize to {rows, total}
|
|
145
|
+
result = await self._client._request(
|
|
127
146
|
endpoint=f"/api/scribe/data/{schema}/{table}/query",
|
|
128
147
|
body=body
|
|
129
148
|
)
|
|
130
149
|
|
|
150
|
+
# Normalize response - map 'items' to 'rows' for consistent interface
|
|
151
|
+
return {
|
|
152
|
+
"rows": result.get("items", result.get("rows", [])),
|
|
153
|
+
"total": result.get("total", 0)
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
async def insert(
|
|
132
157
|
self,
|
|
133
158
|
table: str,
|
dominus/namespaces/ddl.py
CHANGED
|
@@ -308,6 +308,22 @@ class DdlNamespace:
|
|
|
308
308
|
)
|
|
309
309
|
return result if isinstance(result, list) else []
|
|
310
310
|
|
|
311
|
+
async def apply_migration(self, schema: str, migration_id: str) -> Dict[str, Any]:
|
|
312
|
+
"""
|
|
313
|
+
Apply a specific migration to a schema.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
schema: Schema name (e.g., "tenant_acme")
|
|
317
|
+
migration_id: Migration version ID to apply
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Dict with application result
|
|
321
|
+
"""
|
|
322
|
+
return await self._client._request(
|
|
323
|
+
endpoint=f"/api/smith/migrations/apply/{schema}",
|
|
324
|
+
body={"migration_id": migration_id}
|
|
325
|
+
)
|
|
326
|
+
|
|
311
327
|
async def get_current_version(self, schema: str) -> Dict[str, Any]:
|
|
312
328
|
"""Get current migration version for a schema."""
|
|
313
329
|
return await self._client._request(
|
|
@@ -588,3 +604,209 @@ class DdlNamespace:
|
|
|
588
604
|
endpoint=f"/api/smith/provision/tenants-by-category/{category_slug}",
|
|
589
605
|
method="GET"
|
|
590
606
|
)
|
|
607
|
+
|
|
608
|
+
# ========================================
|
|
609
|
+
# SCHEMA BUILDER (UI SUPPORT)
|
|
610
|
+
# ========================================
|
|
611
|
+
|
|
612
|
+
async def get_category_structure(self, category_slug: str) -> Dict[str, Any]:
|
|
613
|
+
"""
|
|
614
|
+
Get schema structure for a category (for Schema Builder UI).
|
|
615
|
+
|
|
616
|
+
Returns tables, columns, indexes, constraints for all tables.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Dict with category, tenant_count, tables, current_version, migration_count
|
|
620
|
+
"""
|
|
621
|
+
return await self._client._request(
|
|
622
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/structure",
|
|
623
|
+
method="GET"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
async def get_category_migration_history(self, category_slug: str) -> Dict[str, Any]:
|
|
627
|
+
"""
|
|
628
|
+
Get migration history for a category.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
Dict with migrations, stored_versions, current_version, total
|
|
632
|
+
"""
|
|
633
|
+
return await self._client._request(
|
|
634
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/migrations",
|
|
635
|
+
method="GET"
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
async def preview_migration(
|
|
639
|
+
self,
|
|
640
|
+
category_slug: str,
|
|
641
|
+
operation: str,
|
|
642
|
+
params: Dict[str, Any],
|
|
643
|
+
migration_name: str
|
|
644
|
+
) -> Dict[str, Any]:
|
|
645
|
+
"""
|
|
646
|
+
Preview a migration before applying.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
category_slug: Category identifier
|
|
650
|
+
operation: Operation type (create_table, add_column, etc.)
|
|
651
|
+
params: Operation parameters
|
|
652
|
+
migration_name: Human-readable migration name
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Dict with affected_tenants, migration_preview, sql_preview
|
|
656
|
+
"""
|
|
657
|
+
return await self._client._request(
|
|
658
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/preview-migration",
|
|
659
|
+
body={
|
|
660
|
+
"operation": operation,
|
|
661
|
+
"params": params,
|
|
662
|
+
"migration_name": migration_name
|
|
663
|
+
}
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
async def apply_builder_migration(
|
|
667
|
+
self,
|
|
668
|
+
category_slug: str,
|
|
669
|
+
operation: str,
|
|
670
|
+
params: Dict[str, Any],
|
|
671
|
+
migration_name: str
|
|
672
|
+
) -> Dict[str, Any]:
|
|
673
|
+
"""
|
|
674
|
+
Apply a migration from Schema Builder.
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
category_slug: Category identifier
|
|
678
|
+
operation: Operation type (create_table, add_column, etc.)
|
|
679
|
+
params: Operation parameters
|
|
680
|
+
migration_name: Human-readable migration name
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Dict with success, migration_id, b2_key, applied_schemas, tenant_count, versions_recorded
|
|
684
|
+
"""
|
|
685
|
+
return await self._client._request(
|
|
686
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/apply-migration",
|
|
687
|
+
body={
|
|
688
|
+
"operation": operation,
|
|
689
|
+
"params": params,
|
|
690
|
+
"migration_name": migration_name
|
|
691
|
+
},
|
|
692
|
+
timeout=120000 # 2 minutes for migration operations
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
async def get_builder_migration_file(
|
|
696
|
+
self,
|
|
697
|
+
category_slug: str,
|
|
698
|
+
version_id: str
|
|
699
|
+
) -> Dict[str, Any]:
|
|
700
|
+
"""
|
|
701
|
+
Get a specific migration file content from B2 storage.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
category_slug: Category identifier
|
|
705
|
+
version_id: Migration version ID
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
Dict with version_id, content, migration_name, b2_key
|
|
709
|
+
"""
|
|
710
|
+
return await self._client._request(
|
|
711
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/migrations/{version_id}",
|
|
712
|
+
method="GET"
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
async def get_tenant_sync_status(self, category_slug: str) -> Dict[str, Any]:
|
|
716
|
+
"""
|
|
717
|
+
Get sync status for all tenants in a category.
|
|
718
|
+
|
|
719
|
+
Shows which tenants are on which migration version.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
Dict with category_slug, latest_version, total_tenants, synced_count,
|
|
723
|
+
pending_count, tenants
|
|
724
|
+
"""
|
|
725
|
+
return await self._client._request(
|
|
726
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/sync-status",
|
|
727
|
+
method="GET"
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
async def sync_tenants_to_latest(self, category_slug: str) -> Dict[str, Any]:
|
|
731
|
+
"""
|
|
732
|
+
Sync all tenants in a category to the latest migration version.
|
|
733
|
+
|
|
734
|
+
Applies any pending migrations to tenants that are behind.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
Dict with success, message, synced_count, failed_count, results
|
|
738
|
+
"""
|
|
739
|
+
return await self._client._request(
|
|
740
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/sync-tenants",
|
|
741
|
+
method="POST",
|
|
742
|
+
timeout=120000 # 2 minutes for sync operations
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# ========================================
|
|
746
|
+
# RESET / DELETE OPERATIONS
|
|
747
|
+
# ========================================
|
|
748
|
+
|
|
749
|
+
async def reset_category_schema(self, category_slug: str) -> Dict[str, Any]:
|
|
750
|
+
"""
|
|
751
|
+
Factory reset a category's schema.
|
|
752
|
+
|
|
753
|
+
WARNING: This is a destructive operation that:
|
|
754
|
+
- Drops ALL tenant schemas in this category (CASCADE)
|
|
755
|
+
- Clears ALL migration history from meta.alembic_version
|
|
756
|
+
- Recreates empty schemas for each tenant
|
|
757
|
+
- Deletes ALL migration files from B2 storage
|
|
758
|
+
|
|
759
|
+
After reset, the category is back to a blank slate.
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
Dict with success, message, reset_count, failed_count, migrations_deleted, results
|
|
763
|
+
"""
|
|
764
|
+
return await self._client._request(
|
|
765
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/reset",
|
|
766
|
+
method="DELETE",
|
|
767
|
+
timeout=120000 # 2 minutes for reset operations
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
async def reset_tenant_schema(
|
|
771
|
+
self,
|
|
772
|
+
category_slug: str,
|
|
773
|
+
tenant_slug: str
|
|
774
|
+
) -> Dict[str, Any]:
|
|
775
|
+
"""
|
|
776
|
+
Reset a single tenant's schema to blank.
|
|
777
|
+
|
|
778
|
+
WARNING: This is a destructive operation that:
|
|
779
|
+
- Clears the tenant's migration history from meta.alembic_version
|
|
780
|
+
- Drops the tenant's schema with CASCADE
|
|
781
|
+
- Recreates an empty schema
|
|
782
|
+
|
|
783
|
+
The tenant can then re-sync to apply current category migrations.
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
Dict with success, message, tenant_slug, schema, versions_cleared
|
|
787
|
+
"""
|
|
788
|
+
return await self._client._request(
|
|
789
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/tenants/{tenant_slug}/reset",
|
|
790
|
+
method="DELETE",
|
|
791
|
+
timeout=60000 # 1 minute for single tenant reset
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
async def delete_migration(
|
|
795
|
+
self,
|
|
796
|
+
category_slug: str,
|
|
797
|
+
version_id: str
|
|
798
|
+
) -> Dict[str, Any]:
|
|
799
|
+
"""
|
|
800
|
+
Delete a single migration file from B2 storage.
|
|
801
|
+
|
|
802
|
+
WARNING: This does NOT clean up meta.alembic_version records.
|
|
803
|
+
Tenants that have already applied this migration keep their history.
|
|
804
|
+
Use with caution - typically only for cleaning up test migrations.
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
Dict with success, message, version_id, category_slug
|
|
808
|
+
"""
|
|
809
|
+
return await self._client._request(
|
|
810
|
+
endpoint=f"/api/smith/builder/category/{category_slug}/migrations/{version_id}",
|
|
811
|
+
method="DELETE"
|
|
812
|
+
)
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Namespace - Decorators for route authentication.
|
|
3
|
+
|
|
4
|
+
Provides decorator-style auth for FastAPI routes:
|
|
5
|
+
|
|
6
|
+
from dominus import dominus
|
|
7
|
+
|
|
8
|
+
@router.get("/protected")
|
|
9
|
+
@dominus.fastapi.jwt
|
|
10
|
+
async def route(claims: dict):
|
|
11
|
+
# claims contains JWT payload
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
@router.get("/scoped")
|
|
15
|
+
@dominus.fastapi.scopes(["api:read", "data:read"])
|
|
16
|
+
async def route(claims: dict):
|
|
17
|
+
# claims validated to have at least one scope
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
@router.get("/psk-protected")
|
|
21
|
+
@dominus.fastapi.psk
|
|
22
|
+
async def route(client: dict):
|
|
23
|
+
# client contains {id, name, tenant_id, scopes, roles, status}
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
Parameter naming conventions:
|
|
27
|
+
- @jwt and @scopes: function must have a 'claims' parameter
|
|
28
|
+
- @psk: function must have a 'client' parameter
|
|
29
|
+
"""
|
|
30
|
+
from functools import wraps
|
|
31
|
+
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
|
|
32
|
+
import inspect
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from ..start import Dominus
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FastAPINamespace:
|
|
39
|
+
"""
|
|
40
|
+
FastAPI authentication decorators namespace.
|
|
41
|
+
|
|
42
|
+
Usage:
|
|
43
|
+
from dominus import dominus
|
|
44
|
+
|
|
45
|
+
@router.get("/protected")
|
|
46
|
+
@dominus.fastapi.jwt
|
|
47
|
+
async def my_route(claims: dict):
|
|
48
|
+
return {"user": claims.get("sub")}
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, client: "Dominus"):
|
|
52
|
+
self._client = client
|
|
53
|
+
|
|
54
|
+
def jwt(self, func: Callable) -> Callable:
|
|
55
|
+
"""
|
|
56
|
+
Decorator: Require valid JWT in Authorization header.
|
|
57
|
+
|
|
58
|
+
Injects JWT claims into the 'claims' parameter.
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
@router.get("/protected")
|
|
62
|
+
@dominus.fastapi.jwt
|
|
63
|
+
async def route(claims: dict):
|
|
64
|
+
return {"user": claims.get("sub")}
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
HTTPException 401: Missing/invalid Authorization header or token
|
|
68
|
+
"""
|
|
69
|
+
return self._create_auth_decorator(func, auth_type="jwt")
|
|
70
|
+
|
|
71
|
+
def psk(self, func: Callable) -> Callable:
|
|
72
|
+
"""
|
|
73
|
+
Decorator: Require valid PSK in Authorization: Bearer header.
|
|
74
|
+
|
|
75
|
+
Injects client info into the 'client' parameter.
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
@router.get("/protected")
|
|
79
|
+
@dominus.fastapi.psk
|
|
80
|
+
async def route(client: dict):
|
|
81
|
+
return {"client_name": client.get("name")}
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
HTTPException 401: Missing/invalid Authorization header or PSK
|
|
85
|
+
"""
|
|
86
|
+
return self._create_auth_decorator(func, auth_type="psk")
|
|
87
|
+
|
|
88
|
+
def scopes(self, allowed_scopes: List[str]) -> Callable:
|
|
89
|
+
"""
|
|
90
|
+
Decorator factory: Require JWT with specific scopes.
|
|
91
|
+
|
|
92
|
+
Access granted if client has ANY of the allowed scopes.
|
|
93
|
+
Injects JWT claims into the 'claims' parameter.
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
@router.get("/admin")
|
|
97
|
+
@dominus.fastapi.scopes(["admin:read", "admin:write"])
|
|
98
|
+
async def route(claims: dict):
|
|
99
|
+
return {"admin": True}
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
allowed_scopes: List of scope strings, any one grants access
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
HTTPException 401: Missing/invalid Authorization header or token
|
|
106
|
+
HTTPException 403: Token valid but missing required scopes
|
|
107
|
+
"""
|
|
108
|
+
def decorator(func: Callable) -> Callable:
|
|
109
|
+
return self._create_auth_decorator(func, auth_type="jwt", required_scopes=allowed_scopes)
|
|
110
|
+
return decorator
|
|
111
|
+
|
|
112
|
+
def _create_auth_decorator(
|
|
113
|
+
self,
|
|
114
|
+
func: Callable,
|
|
115
|
+
auth_type: str,
|
|
116
|
+
required_scopes: Optional[List[str]] = None
|
|
117
|
+
) -> Callable:
|
|
118
|
+
"""
|
|
119
|
+
Internal: Create an auth decorator for the given function.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
func: The route function to wrap
|
|
123
|
+
auth_type: "jwt" or "psk"
|
|
124
|
+
required_scopes: Optional list of scopes to require (jwt only)
|
|
125
|
+
"""
|
|
126
|
+
# Import here to avoid circular imports and make fastapi optional
|
|
127
|
+
from fastapi import Header, HTTPException
|
|
128
|
+
|
|
129
|
+
# Determine the parameter name to inject
|
|
130
|
+
param_name = "claims" if auth_type == "jwt" else "client"
|
|
131
|
+
|
|
132
|
+
# Validate the function has the required parameter
|
|
133
|
+
sig = inspect.signature(func)
|
|
134
|
+
if param_name not in sig.parameters:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"@dominus.fastapi.{auth_type} requires a '{param_name}' parameter. "
|
|
137
|
+
f"Add '{param_name}: dict' to your function signature."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@wraps(func)
|
|
141
|
+
async def wrapper(*args, **kwargs):
|
|
142
|
+
# Get Authorization header from the request
|
|
143
|
+
# FastAPI injects this via the wrapper's signature
|
|
144
|
+
authorization = kwargs.pop("_dominus_authorization", None)
|
|
145
|
+
|
|
146
|
+
if not authorization:
|
|
147
|
+
raise HTTPException(
|
|
148
|
+
status_code=401,
|
|
149
|
+
detail="Missing Authorization header",
|
|
150
|
+
headers={"WWW-Authenticate": "Bearer"}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Extract Bearer token
|
|
154
|
+
token = self._extract_bearer(authorization)
|
|
155
|
+
if not token:
|
|
156
|
+
raise HTTPException(
|
|
157
|
+
status_code=401,
|
|
158
|
+
detail="Invalid Authorization header format. Expected: Bearer <token>",
|
|
159
|
+
headers={"WWW-Authenticate": "Bearer"}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Validate based on auth type
|
|
163
|
+
if auth_type == "jwt":
|
|
164
|
+
auth_data = await self._validate_jwt(token)
|
|
165
|
+
if not auth_data:
|
|
166
|
+
raise HTTPException(
|
|
167
|
+
status_code=401,
|
|
168
|
+
detail="Invalid or expired token",
|
|
169
|
+
headers={"WWW-Authenticate": "Bearer"}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Check scopes if required
|
|
173
|
+
if required_scopes:
|
|
174
|
+
client_scopes = auth_data.get("scopes", [])
|
|
175
|
+
if not any(scope in client_scopes for scope in required_scopes):
|
|
176
|
+
raise HTTPException(
|
|
177
|
+
status_code=403,
|
|
178
|
+
detail=f"Missing required scope. Need one of: {required_scopes}"
|
|
179
|
+
)
|
|
180
|
+
else: # psk
|
|
181
|
+
auth_data = await self._validate_psk(token)
|
|
182
|
+
if not auth_data:
|
|
183
|
+
raise HTTPException(
|
|
184
|
+
status_code=401,
|
|
185
|
+
detail="Invalid API key",
|
|
186
|
+
headers={"WWW-Authenticate": "Bearer"}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Inject the auth data into the function
|
|
190
|
+
kwargs[param_name] = auth_data
|
|
191
|
+
|
|
192
|
+
# Call the original function
|
|
193
|
+
return await func(*args, **kwargs)
|
|
194
|
+
|
|
195
|
+
# Modify the wrapper signature to include Authorization header
|
|
196
|
+
# This allows FastAPI to inject it automatically
|
|
197
|
+
old_sig = inspect.signature(func)
|
|
198
|
+
old_params = list(old_sig.parameters.values())
|
|
199
|
+
|
|
200
|
+
# Remove the claims/client param (we'll inject it manually)
|
|
201
|
+
new_params = [p for p in old_params if p.name != param_name]
|
|
202
|
+
|
|
203
|
+
# Add the Authorization header param
|
|
204
|
+
auth_param = inspect.Parameter(
|
|
205
|
+
"_dominus_authorization",
|
|
206
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
207
|
+
default=Header(None, alias="Authorization"),
|
|
208
|
+
annotation=Optional[str]
|
|
209
|
+
)
|
|
210
|
+
new_params.append(auth_param)
|
|
211
|
+
|
|
212
|
+
# Create new signature
|
|
213
|
+
wrapper.__signature__ = old_sig.replace(parameters=new_params)
|
|
214
|
+
|
|
215
|
+
return wrapper
|
|
216
|
+
|
|
217
|
+
def _extract_bearer(self, auth_header: str) -> Optional[str]:
|
|
218
|
+
"""Extract token from Authorization: Bearer <token> header."""
|
|
219
|
+
if not auth_header or not auth_header.startswith("Bearer "):
|
|
220
|
+
return None
|
|
221
|
+
return auth_header[7:]
|
|
222
|
+
|
|
223
|
+
async def _validate_jwt(self, token: str) -> Optional[Dict[str, Any]]:
|
|
224
|
+
"""Validate JWT and return claims."""
|
|
225
|
+
try:
|
|
226
|
+
return await self._client.validate_jwt(token)
|
|
227
|
+
except (ValueError, Exception):
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
async def _validate_psk(self, psk: str) -> Optional[Dict[str, Any]]:
|
|
231
|
+
"""
|
|
232
|
+
Validate PSK and return client info with scopes/roles.
|
|
233
|
+
|
|
234
|
+
Uses portal.login_client to exchange PSK for JWT, then decodes
|
|
235
|
+
the JWT to get the client's claims (scopes, roles, tenants, etc.).
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
# Exchange PSK for JWT via portal.login_client
|
|
239
|
+
# This uses the SDK's project JWT + the subsidiary client's PSK
|
|
240
|
+
result = await self._client.portal.login_client(psk=psk)
|
|
241
|
+
if not result:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
jwt = result.get("access_token")
|
|
245
|
+
if not jwt:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
# Decode JWT to get claims (includes scopes, roles, tenants, etc.)
|
|
249
|
+
claims = await self._client.validate_jwt(jwt)
|
|
250
|
+
return claims
|
|
251
|
+
except Exception:
|
|
252
|
+
return None
|