dominus-sdk-python 2.0.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 +47 -0
- dominus/config/__init__.py +24 -0
- dominus/config/endpoints.py +43 -0
- dominus/errors.py +201 -0
- dominus/helpers/__init__.py +2 -0
- dominus/helpers/auth.py +49 -0
- dominus/helpers/cache.py +192 -0
- dominus/helpers/core.py +603 -0
- dominus/helpers/crypto.py +118 -0
- dominus/namespaces/__init__.py +26 -0
- dominus/namespaces/_deprecated_crossover.py +10 -0
- dominus/namespaces/_deprecated_sql.py +1341 -0
- dominus/namespaces/auth.py +1025 -0
- dominus/namespaces/courier.py +251 -0
- dominus/namespaces/db.py +267 -0
- dominus/namespaces/ddl.py +590 -0
- dominus/namespaces/files.py +299 -0
- dominus/namespaces/health.py +59 -0
- dominus/namespaces/logs.py +367 -0
- dominus/namespaces/open.py +72 -0
- dominus/namespaces/portal.py +437 -0
- dominus/namespaces/redis.py +357 -0
- dominus/namespaces/secrets.py +126 -0
- dominus/services/__init__.py +2 -0
- dominus/services/_deprecated_architect.py +323 -0
- dominus/services/_deprecated_sovereign.py +93 -0
- dominus/start.py +942 -0
- dominus_sdk_python-2.0.0.dist-info/METADATA +381 -0
- dominus_sdk_python-2.0.0.dist-info/RECORD +31 -0
- dominus_sdk_python-2.0.0.dist-info/WHEEL +5 -0
- dominus_sdk_python-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
"""SQL operations namespace - Role-based database operations"""
|
|
2
|
+
from typing import Dict, Any, List, Optional
|
|
3
|
+
|
|
4
|
+
from ..helpers.crypto import hash_password, hash_psk, hash_token, generate_token
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SQLNamespace:
|
|
8
|
+
"""SQL operations namespace"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, _execute_command, _execute_sovereign_command=None):
|
|
11
|
+
"""
|
|
12
|
+
Initialize SQL namespace.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
_execute_command: Internal function to execute commands (routes to Architect)
|
|
16
|
+
_execute_sovereign_command: Internal function for Sovereign commands (optional)
|
|
17
|
+
"""
|
|
18
|
+
self._execute = _execute_command
|
|
19
|
+
self._execute_sovereign = _execute_sovereign_command
|
|
20
|
+
self.app = SQLRoleNamespace("app", _execute_command)
|
|
21
|
+
self.secure = SQLRoleNamespace("secure", _execute_command)
|
|
22
|
+
self.secure_machine = SQLRoleNamespace("secure_machine", _execute_command)
|
|
23
|
+
self.auth = SQLAuthNamespace(_execute_command, _execute_sovereign_command)
|
|
24
|
+
self.schema = SQLSchemaNamespace(_execute_command)
|
|
25
|
+
self.open = SQLOpenNamespace(_execute_command)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SQLRoleNamespace:
|
|
29
|
+
"""Role-specific SQL operations (app_user, secure_user, secure_machine_user)"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, role: str, _execute_command):
|
|
32
|
+
"""
|
|
33
|
+
Initialize role namespace.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
role: Role name ("app", "secure", "secure_machine")
|
|
37
|
+
_execute_command: Internal function to execute commands
|
|
38
|
+
"""
|
|
39
|
+
self._role = role
|
|
40
|
+
self._command_prefix = f"sql.{role}"
|
|
41
|
+
self._execute = _execute_command
|
|
42
|
+
self._default_schema = "app" if role == "app" else "secure"
|
|
43
|
+
|
|
44
|
+
async def list_tables(self, schema: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
45
|
+
"""
|
|
46
|
+
List tables in a schema.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
schema: Schema name (default: "app" for app_user, "secure" for secure_user)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of table information dictionaries
|
|
53
|
+
"""
|
|
54
|
+
if schema is None:
|
|
55
|
+
schema = self._default_schema
|
|
56
|
+
command = f"{self._command_prefix}.list_tables"
|
|
57
|
+
return await self._execute(command, schema=schema)
|
|
58
|
+
|
|
59
|
+
async def query_table(
|
|
60
|
+
self,
|
|
61
|
+
table_name: str,
|
|
62
|
+
schema: Optional[str] = None,
|
|
63
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
64
|
+
sort_by: Optional[str] = None,
|
|
65
|
+
sort_order: str = "ASC",
|
|
66
|
+
limit: int = 100,
|
|
67
|
+
offset: int = 0
|
|
68
|
+
) -> Dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Query table data with pagination and filters.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
table_name: Table name
|
|
74
|
+
schema: Schema name (default: "app" for app_user, "secure" for secure_user)
|
|
75
|
+
filters: Optional dictionary of column:value filters
|
|
76
|
+
sort_by: Optional column name to sort by
|
|
77
|
+
sort_order: Sort order (ASC or DESC, default: ASC)
|
|
78
|
+
limit: Maximum number of rows to return (default: 100)
|
|
79
|
+
offset: Number of rows to skip (default: 0)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary with 'rows' and 'total' keys
|
|
83
|
+
"""
|
|
84
|
+
if schema is None:
|
|
85
|
+
schema = self._default_schema
|
|
86
|
+
command = f"{self._command_prefix}.query_table"
|
|
87
|
+
return await self._execute(
|
|
88
|
+
command,
|
|
89
|
+
table_name=table_name,
|
|
90
|
+
schema=schema,
|
|
91
|
+
filters=filters,
|
|
92
|
+
sort_by=sort_by,
|
|
93
|
+
sort_order=sort_order,
|
|
94
|
+
limit=limit,
|
|
95
|
+
offset=offset
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def insert_row(
|
|
99
|
+
self,
|
|
100
|
+
table_name: str,
|
|
101
|
+
data: Dict[str, Any],
|
|
102
|
+
schema: Optional[str] = None
|
|
103
|
+
) -> Dict[str, Any]:
|
|
104
|
+
"""
|
|
105
|
+
Insert a single row into a table.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
table_name: Table name
|
|
109
|
+
data: Dictionary of column:value pairs
|
|
110
|
+
schema: Schema name (default: "app" for app_user, "secure" for secure_user)
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary with inserted row data
|
|
114
|
+
"""
|
|
115
|
+
if schema is None:
|
|
116
|
+
schema = self._default_schema
|
|
117
|
+
command = f"{self._command_prefix}.insert_row"
|
|
118
|
+
return await self._execute(
|
|
119
|
+
command,
|
|
120
|
+
table_name=table_name,
|
|
121
|
+
data=data,
|
|
122
|
+
schema=schema
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def update_rows(
|
|
126
|
+
self,
|
|
127
|
+
table_name: str,
|
|
128
|
+
data: Dict[str, Any],
|
|
129
|
+
filters: Dict[str, Any],
|
|
130
|
+
schema: Optional[str] = None
|
|
131
|
+
) -> Dict[str, Any]:
|
|
132
|
+
"""
|
|
133
|
+
Update rows matching filters.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
table_name: Table name
|
|
137
|
+
data: Dictionary of column:value pairs to update
|
|
138
|
+
filters: Dictionary of column:value pairs for WHERE clause
|
|
139
|
+
schema: Schema name (default: "app" for app_user, "secure" for secure_user)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dictionary with 'affected_rows' key
|
|
143
|
+
"""
|
|
144
|
+
if schema is None:
|
|
145
|
+
schema = self._default_schema
|
|
146
|
+
command = f"{self._command_prefix}.update_rows"
|
|
147
|
+
return await self._execute(
|
|
148
|
+
command,
|
|
149
|
+
table_name=table_name,
|
|
150
|
+
data=data,
|
|
151
|
+
filters=filters,
|
|
152
|
+
schema=schema
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def delete_rows(
|
|
156
|
+
self,
|
|
157
|
+
table_name: str,
|
|
158
|
+
filters: Dict[str, Any],
|
|
159
|
+
schema: Optional[str] = None
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Delete rows matching filters.
|
|
163
|
+
|
|
164
|
+
Note: For secure_user and secure_machine_user, DELETE only works on app schema,
|
|
165
|
+
not on secure schema (PHI data uses soft deletes only).
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
table_name: Table name
|
|
169
|
+
filters: Dictionary of column:value pairs for WHERE clause
|
|
170
|
+
schema: Schema name (default: "app" - secure roles can only delete from app schema)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dictionary with 'affected_rows' key
|
|
174
|
+
"""
|
|
175
|
+
if schema is None:
|
|
176
|
+
schema = "app" # Always app for delete operations (secure schema doesn't allow DELETE)
|
|
177
|
+
command = f"{self._command_prefix}.delete_rows"
|
|
178
|
+
return await self._execute(
|
|
179
|
+
command,
|
|
180
|
+
table_name=table_name,
|
|
181
|
+
filters=filters,
|
|
182
|
+
schema=schema
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def list_columns(
|
|
186
|
+
self,
|
|
187
|
+
table_name: str,
|
|
188
|
+
schema: Optional[str] = None
|
|
189
|
+
) -> List[Dict[str, Any]]:
|
|
190
|
+
"""
|
|
191
|
+
List columns in a table.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
table_name: Table name
|
|
195
|
+
schema: Schema name (default: "app" for app_user, "secure" for secure_user)
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
List of column information dictionaries
|
|
199
|
+
"""
|
|
200
|
+
if schema is None:
|
|
201
|
+
schema = self._default_schema
|
|
202
|
+
command = f"{self._command_prefix}.list_columns"
|
|
203
|
+
return await self._execute(
|
|
204
|
+
command,
|
|
205
|
+
table_name=table_name,
|
|
206
|
+
schema=schema
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def get_table_size(
|
|
210
|
+
self,
|
|
211
|
+
table_name: str,
|
|
212
|
+
schema: Optional[str] = None
|
|
213
|
+
) -> Dict[str, Any]:
|
|
214
|
+
"""
|
|
215
|
+
Get table size information.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
table_name: Table name
|
|
219
|
+
schema: Schema name (default: "app" for app_user, "secure" for secure_user)
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Dictionary with table size information
|
|
223
|
+
"""
|
|
224
|
+
if schema is None:
|
|
225
|
+
schema = self._default_schema
|
|
226
|
+
command = f"{self._command_prefix}.get_table_size"
|
|
227
|
+
return await self._execute(
|
|
228
|
+
command,
|
|
229
|
+
table_name=table_name,
|
|
230
|
+
schema=schema
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class SQLOpenNamespace:
|
|
235
|
+
"""Open user namespace - Returns DSN string for dominus_open database"""
|
|
236
|
+
|
|
237
|
+
def __init__(self, _execute_command):
|
|
238
|
+
"""
|
|
239
|
+
Initialize open namespace.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
_execute_command: Internal function to execute commands
|
|
243
|
+
"""
|
|
244
|
+
self._execute = _execute_command
|
|
245
|
+
|
|
246
|
+
async def dsn(self) -> str:
|
|
247
|
+
"""
|
|
248
|
+
Get the full DSN connection string for open_user role.
|
|
249
|
+
|
|
250
|
+
This returns the complete PostgreSQL connection URI for the dominus_open
|
|
251
|
+
database that can be used directly by clients to connect.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
PostgreSQL connection URI string in format:
|
|
255
|
+
postgresql://{userpwcombo}@{branchtargetstring}/dominus_open?sslmode=require&channel_binding=require
|
|
256
|
+
|
|
257
|
+
Note:
|
|
258
|
+
This is the only role that exposes the DSN string directly.
|
|
259
|
+
All other roles use the DSN internally for operations.
|
|
260
|
+
"""
|
|
261
|
+
return await self._execute("sql.open.get_dsn")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class SQLAuthNamespace:
|
|
265
|
+
"""
|
|
266
|
+
Authentication operations namespace.
|
|
267
|
+
|
|
268
|
+
Provides scoped functions for managing auth schema tables:
|
|
269
|
+
- scopes: Permission definitions
|
|
270
|
+
- roles: Role definitions with assigned scopes
|
|
271
|
+
- users: User accounts with assigned roles
|
|
272
|
+
- clients: Service account PSKs with assigned roles
|
|
273
|
+
- refresh_tokens: Token management
|
|
274
|
+
- JWT operations: Minting and validation
|
|
275
|
+
|
|
276
|
+
DB User: auth_user (CRUD on auth schema only)
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(self, _execute_command, _execute_sovereign_command=None):
|
|
280
|
+
"""
|
|
281
|
+
Initialize auth namespace.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
_execute_command: Internal function to execute commands (Architect)
|
|
285
|
+
_execute_sovereign_command: Internal function for Sovereign commands
|
|
286
|
+
"""
|
|
287
|
+
self._execute = _execute_command
|
|
288
|
+
self._execute_sovereign = _execute_sovereign_command
|
|
289
|
+
self._public_key_cache = None
|
|
290
|
+
|
|
291
|
+
# ========================================
|
|
292
|
+
# SCOPES (auth.scopes table)
|
|
293
|
+
# ========================================
|
|
294
|
+
|
|
295
|
+
async def add_scope(
|
|
296
|
+
self,
|
|
297
|
+
slug: str,
|
|
298
|
+
display_name: str,
|
|
299
|
+
category_id: Optional[str] = None,
|
|
300
|
+
description: Optional[str] = None
|
|
301
|
+
) -> Dict[str, Any]:
|
|
302
|
+
"""
|
|
303
|
+
Add a new scope.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
slug: Unique scope identifier (e.g., "read", "write", "admin")
|
|
307
|
+
display_name: Human-readable name
|
|
308
|
+
category_id: Optional tenant category UUID to link via scope_categories
|
|
309
|
+
description: Optional description
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Created scope record
|
|
313
|
+
"""
|
|
314
|
+
return await self._execute(
|
|
315
|
+
"auth.add_scope",
|
|
316
|
+
slug=slug,
|
|
317
|
+
display_name=display_name,
|
|
318
|
+
category_id=category_id,
|
|
319
|
+
description=description
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
async def delete_scope(self, scope_id: str) -> Dict[str, Any]:
|
|
323
|
+
"""
|
|
324
|
+
Delete a scope by ID.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
scope_id: UUID of scope to delete
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Deletion confirmation
|
|
331
|
+
"""
|
|
332
|
+
return await self._execute("auth.delete_scope", scope_id=scope_id)
|
|
333
|
+
|
|
334
|
+
async def list_scopes(
|
|
335
|
+
self,
|
|
336
|
+
category_id: Optional[str] = None
|
|
337
|
+
) -> List[Dict[str, Any]]:
|
|
338
|
+
"""
|
|
339
|
+
List all scopes, optionally filtered by category.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
category_id: Optional tenant category UUID filter
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
List of scope records
|
|
346
|
+
"""
|
|
347
|
+
return await self._execute(
|
|
348
|
+
"auth.list_scopes",
|
|
349
|
+
category_id=category_id
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
async def get_scope(
|
|
353
|
+
self,
|
|
354
|
+
scope_id: Optional[str] = None,
|
|
355
|
+
slug: Optional[str] = None
|
|
356
|
+
) -> Dict[str, Any]:
|
|
357
|
+
"""
|
|
358
|
+
Get a scope by ID or slug.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
scope_id: UUID of scope (preferred)
|
|
362
|
+
slug: Scope slug (alternative lookup)
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Scope record
|
|
366
|
+
"""
|
|
367
|
+
return await self._execute(
|
|
368
|
+
"auth.get_scope",
|
|
369
|
+
scope_id=scope_id,
|
|
370
|
+
slug=slug
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# ========================================
|
|
374
|
+
# ROLES (auth.roles table)
|
|
375
|
+
# ========================================
|
|
376
|
+
|
|
377
|
+
async def add_role(
|
|
378
|
+
self,
|
|
379
|
+
name: str,
|
|
380
|
+
scope_slugs: Optional[List[str]] = None,
|
|
381
|
+
description: Optional[str] = None,
|
|
382
|
+
tenant_id: Optional[str] = None,
|
|
383
|
+
category_id: Optional[str] = None
|
|
384
|
+
) -> Dict[str, Any]:
|
|
385
|
+
"""
|
|
386
|
+
Add a new role.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
name: Role name (unique per tenant)
|
|
390
|
+
scope_slugs: List of scope slugs to assign
|
|
391
|
+
description: Optional description
|
|
392
|
+
tenant_id: Optional tenant UUID to link via role_tenants
|
|
393
|
+
category_id: Optional category UUID to link via role_categories
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Created role record
|
|
397
|
+
"""
|
|
398
|
+
return await self._execute(
|
|
399
|
+
"auth.add_role",
|
|
400
|
+
name=name,
|
|
401
|
+
scope_slugs=scope_slugs or [],
|
|
402
|
+
description=description,
|
|
403
|
+
tenant_id=tenant_id,
|
|
404
|
+
category_id=category_id
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
async def delete_role(self, role_id: str) -> Dict[str, Any]:
|
|
408
|
+
"""
|
|
409
|
+
Delete a role by ID.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
role_id: UUID of role to delete
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Deletion confirmation
|
|
416
|
+
"""
|
|
417
|
+
return await self._execute("auth.delete_role", role_id=role_id)
|
|
418
|
+
|
|
419
|
+
async def list_roles(self) -> List[Dict[str, Any]]:
|
|
420
|
+
"""
|
|
421
|
+
List all roles for the tenant.
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
List of role records
|
|
425
|
+
"""
|
|
426
|
+
return await self._execute("auth.list_roles")
|
|
427
|
+
|
|
428
|
+
async def get_role(
|
|
429
|
+
self,
|
|
430
|
+
role_id: Optional[str] = None,
|
|
431
|
+
name: Optional[str] = None
|
|
432
|
+
) -> Dict[str, Any]:
|
|
433
|
+
"""
|
|
434
|
+
Get a role by ID or name.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
role_id: UUID of role (preferred)
|
|
438
|
+
name: Role name (alternative lookup)
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Role record
|
|
442
|
+
"""
|
|
443
|
+
return await self._execute(
|
|
444
|
+
"auth.get_role",
|
|
445
|
+
role_id=role_id,
|
|
446
|
+
name=name
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
async def update_role_scopes(
|
|
450
|
+
self,
|
|
451
|
+
role_id: str,
|
|
452
|
+
scope_slugs: List[str]
|
|
453
|
+
) -> Dict[str, Any]:
|
|
454
|
+
"""
|
|
455
|
+
Update the scopes assigned to a role.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
role_id: UUID of role to update
|
|
459
|
+
scope_slugs: New list of scope slugs
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Updated role record
|
|
463
|
+
"""
|
|
464
|
+
return await self._execute(
|
|
465
|
+
"auth.update_role_scopes",
|
|
466
|
+
role_id=role_id,
|
|
467
|
+
scope_slugs=scope_slugs
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# ========================================
|
|
471
|
+
# USERS (auth.users table)
|
|
472
|
+
# ========================================
|
|
473
|
+
|
|
474
|
+
async def add_user(
|
|
475
|
+
self,
|
|
476
|
+
username: str,
|
|
477
|
+
password: str,
|
|
478
|
+
role_id: Optional[str] = None,
|
|
479
|
+
tenant_id: Optional[str] = None,
|
|
480
|
+
email: Optional[str] = None
|
|
481
|
+
) -> Dict[str, Any]:
|
|
482
|
+
"""
|
|
483
|
+
Add a new user.
|
|
484
|
+
|
|
485
|
+
Password is hashed client-side before sending to Architect.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
username: Unique username
|
|
489
|
+
password: Raw password (will be hashed)
|
|
490
|
+
role_id: Optional UUID of role to assign
|
|
491
|
+
tenant_id: Optional UUID of tenant to link (defaults from role if provided)
|
|
492
|
+
email: Optional email address
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Created user record (without password_hash)
|
|
496
|
+
"""
|
|
497
|
+
password_hash = hash_password(password)
|
|
498
|
+
return await self._execute(
|
|
499
|
+
"auth.add_user",
|
|
500
|
+
username=username,
|
|
501
|
+
password_hash=password_hash,
|
|
502
|
+
role_id=role_id,
|
|
503
|
+
tenant_id=tenant_id,
|
|
504
|
+
email=email
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
async def delete_user(self, user_id: str) -> Dict[str, Any]:
|
|
508
|
+
"""
|
|
509
|
+
Delete a user by ID.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
user_id: UUID of user to delete
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Deletion confirmation
|
|
516
|
+
"""
|
|
517
|
+
return await self._execute("auth.delete_user", user_id=user_id)
|
|
518
|
+
|
|
519
|
+
async def list_users(self) -> List[Dict[str, Any]]:
|
|
520
|
+
"""
|
|
521
|
+
List all users for the tenant.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
List of user records (without password_hash)
|
|
525
|
+
"""
|
|
526
|
+
return await self._execute("auth.list_users")
|
|
527
|
+
|
|
528
|
+
async def get_user(
|
|
529
|
+
self,
|
|
530
|
+
user_id: Optional[str] = None,
|
|
531
|
+
username: Optional[str] = None
|
|
532
|
+
) -> Dict[str, Any]:
|
|
533
|
+
"""
|
|
534
|
+
Get a user by ID or username.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
user_id: UUID of user (preferred)
|
|
538
|
+
username: Username (alternative lookup)
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
User record (without password_hash)
|
|
542
|
+
"""
|
|
543
|
+
return await self._execute(
|
|
544
|
+
"auth.get_user",
|
|
545
|
+
user_id=user_id,
|
|
546
|
+
username=username
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
async def update_user_status(
|
|
550
|
+
self,
|
|
551
|
+
user_id: str,
|
|
552
|
+
status: str
|
|
553
|
+
) -> Dict[str, Any]:
|
|
554
|
+
"""
|
|
555
|
+
Update a user's status.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
user_id: UUID of user to update
|
|
559
|
+
status: New status (e.g., "active", "inactive", "suspended")
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
Updated user record
|
|
563
|
+
"""
|
|
564
|
+
return await self._execute(
|
|
565
|
+
"auth.update_user_status",
|
|
566
|
+
user_id=user_id,
|
|
567
|
+
status=status
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
async def update_user_role(
|
|
571
|
+
self,
|
|
572
|
+
user_id: str,
|
|
573
|
+
role_id: str
|
|
574
|
+
) -> Dict[str, Any]:
|
|
575
|
+
"""
|
|
576
|
+
Update a user's assigned role.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
user_id: UUID of user to update
|
|
580
|
+
role_id: UUID of new role
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Updated user record
|
|
584
|
+
"""
|
|
585
|
+
return await self._execute(
|
|
586
|
+
"auth.update_user_role",
|
|
587
|
+
user_id=user_id,
|
|
588
|
+
role_id=role_id
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
async def update_user(
|
|
592
|
+
self,
|
|
593
|
+
user_id: str,
|
|
594
|
+
username: str = None,
|
|
595
|
+
email: str = None
|
|
596
|
+
) -> Dict[str, Any]:
|
|
597
|
+
"""
|
|
598
|
+
Update a user's profile fields (username, email).
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
user_id: UUID of user to update
|
|
602
|
+
username: New username (optional)
|
|
603
|
+
email: New email (optional, can be None to clear)
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Updated user record
|
|
607
|
+
"""
|
|
608
|
+
params = {"user_id": user_id}
|
|
609
|
+
if username is not None:
|
|
610
|
+
params["username"] = username
|
|
611
|
+
if email is not None:
|
|
612
|
+
params["email"] = email
|
|
613
|
+
return await self._execute("auth.update_user", **params)
|
|
614
|
+
|
|
615
|
+
async def update_user_password(
|
|
616
|
+
self,
|
|
617
|
+
user_id: str,
|
|
618
|
+
password: str
|
|
619
|
+
) -> Dict[str, Any]:
|
|
620
|
+
"""
|
|
621
|
+
Update a user's password.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
user_id: UUID of user to update
|
|
625
|
+
password: New raw password (will be hashed by Architect)
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
Updated user record (without password_hash)
|
|
629
|
+
"""
|
|
630
|
+
return await self._execute(
|
|
631
|
+
"auth.update_user_password",
|
|
632
|
+
user_id=user_id,
|
|
633
|
+
password=password
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
async def verify_user_password(
|
|
637
|
+
self,
|
|
638
|
+
username: str,
|
|
639
|
+
password: str
|
|
640
|
+
) -> Dict[str, Any]:
|
|
641
|
+
"""
|
|
642
|
+
Verify a user's password.
|
|
643
|
+
|
|
644
|
+
Raw password is sent to Architect which does bcrypt comparison.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
username: Username to verify
|
|
648
|
+
password: Raw password to check
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
{"valid": True/False, "user": {...}} if valid
|
|
652
|
+
"""
|
|
653
|
+
return await self._execute(
|
|
654
|
+
"auth.verify_user_password",
|
|
655
|
+
username=username,
|
|
656
|
+
password=password
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
# ========================================
|
|
660
|
+
# CLIENTS / PSK (auth.client_psk table)
|
|
661
|
+
# ========================================
|
|
662
|
+
|
|
663
|
+
async def add_client(
|
|
664
|
+
self,
|
|
665
|
+
label: str,
|
|
666
|
+
role_id: str
|
|
667
|
+
) -> Dict[str, Any]:
|
|
668
|
+
"""
|
|
669
|
+
Add a new service client with PSK.
|
|
670
|
+
|
|
671
|
+
Generates a PSK, hashes it, stores the hash, and returns
|
|
672
|
+
the raw PSK (one-time visible).
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
label: Human-readable label for the client
|
|
676
|
+
role_id: UUID of role to assign
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
{"client": {...}, "psk": "raw-psk-one-time-visible"}
|
|
680
|
+
"""
|
|
681
|
+
from ..helpers.crypto import generate_psk_local
|
|
682
|
+
# Generate PSK (prefer Sovereign route if available, fallback to local)
|
|
683
|
+
if self._execute_sovereign:
|
|
684
|
+
try:
|
|
685
|
+
result = await self._execute_sovereign("auth.generate_psk")
|
|
686
|
+
raw_psk = result.get("psk")
|
|
687
|
+
except Exception:
|
|
688
|
+
raw_psk = generate_psk_local()
|
|
689
|
+
else:
|
|
690
|
+
raw_psk = generate_psk_local()
|
|
691
|
+
|
|
692
|
+
psk_hash = hash_psk(raw_psk)
|
|
693
|
+
client_result = await self._execute(
|
|
694
|
+
"auth.add_client",
|
|
695
|
+
label=label,
|
|
696
|
+
role_id=role_id,
|
|
697
|
+
psk_hash=psk_hash
|
|
698
|
+
)
|
|
699
|
+
return {
|
|
700
|
+
"client": client_result,
|
|
701
|
+
"psk": raw_psk # One-time visible
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async def delete_client(self, client_id: str) -> Dict[str, Any]:
|
|
705
|
+
"""
|
|
706
|
+
Delete a client by ID.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
client_id: UUID of client to delete
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
Deletion confirmation
|
|
713
|
+
"""
|
|
714
|
+
return await self._execute("auth.delete_client", client_id=client_id)
|
|
715
|
+
|
|
716
|
+
async def list_clients(self) -> List[Dict[str, Any]]:
|
|
717
|
+
"""
|
|
718
|
+
List all clients for the tenant.
|
|
719
|
+
|
|
720
|
+
Returns:
|
|
721
|
+
List of client records (without psk_hash)
|
|
722
|
+
"""
|
|
723
|
+
return await self._execute("auth.list_clients")
|
|
724
|
+
|
|
725
|
+
async def get_client(self, client_id: str) -> Dict[str, Any]:
|
|
726
|
+
"""
|
|
727
|
+
Get a client by ID.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
client_id: UUID of client
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
Client record (without psk_hash)
|
|
734
|
+
"""
|
|
735
|
+
return await self._execute("auth.get_client", client_id=client_id)
|
|
736
|
+
|
|
737
|
+
async def regenerate_client_psk(self, client_id: str) -> Dict[str, Any]:
|
|
738
|
+
"""
|
|
739
|
+
Regenerate a client's PSK.
|
|
740
|
+
|
|
741
|
+
Generates a new PSK, hashes it, updates the stored hash,
|
|
742
|
+
and returns the new raw PSK (one-time visible).
|
|
743
|
+
|
|
744
|
+
Args:
|
|
745
|
+
client_id: UUID of client
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
{"client": {...}, "psk": "new-raw-psk-one-time-visible"}
|
|
749
|
+
"""
|
|
750
|
+
from ..helpers.crypto import generate_psk_local
|
|
751
|
+
# Generate new PSK
|
|
752
|
+
if self._execute_sovereign:
|
|
753
|
+
try:
|
|
754
|
+
result = await self._execute_sovereign("auth.generate_psk")
|
|
755
|
+
raw_psk = result.get("psk")
|
|
756
|
+
except Exception:
|
|
757
|
+
raw_psk = generate_psk_local()
|
|
758
|
+
else:
|
|
759
|
+
raw_psk = generate_psk_local()
|
|
760
|
+
|
|
761
|
+
psk_hash = hash_psk(raw_psk)
|
|
762
|
+
client_result = await self._execute(
|
|
763
|
+
"auth.regenerate_client_psk",
|
|
764
|
+
client_id=client_id,
|
|
765
|
+
psk_hash=psk_hash
|
|
766
|
+
)
|
|
767
|
+
return {
|
|
768
|
+
"client": client_result,
|
|
769
|
+
"psk": raw_psk # One-time visible
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async def verify_client_psk(
|
|
773
|
+
self,
|
|
774
|
+
client_id: str,
|
|
775
|
+
psk: str
|
|
776
|
+
) -> Dict[str, Any]:
|
|
777
|
+
"""
|
|
778
|
+
Verify a client's PSK.
|
|
779
|
+
|
|
780
|
+
Raw PSK is sent to Architect which does bcrypt comparison.
|
|
781
|
+
|
|
782
|
+
Args:
|
|
783
|
+
client_id: UUID of client
|
|
784
|
+
psk: Raw PSK to verify
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
{"valid": True/False, "client": {...}} if valid
|
|
788
|
+
"""
|
|
789
|
+
return await self._execute(
|
|
790
|
+
"auth.verify_client_psk",
|
|
791
|
+
client_id=client_id,
|
|
792
|
+
psk=psk
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
# ========================================
|
|
796
|
+
# REFRESH TOKENS (auth.refresh_tokens table)
|
|
797
|
+
# ========================================
|
|
798
|
+
|
|
799
|
+
async def add_refresh_token(
|
|
800
|
+
self,
|
|
801
|
+
user_id: Optional[str] = None,
|
|
802
|
+
client_psk_id: Optional[str] = None,
|
|
803
|
+
expires_in_seconds: int = 86400 * 30 # 30 days default
|
|
804
|
+
) -> Dict[str, Any]:
|
|
805
|
+
"""
|
|
806
|
+
Create a new refresh token.
|
|
807
|
+
|
|
808
|
+
Either user_id or client_psk_id must be provided.
|
|
809
|
+
|
|
810
|
+
Args:
|
|
811
|
+
user_id: UUID of user (for user tokens)
|
|
812
|
+
client_psk_id: UUID of client (for service tokens)
|
|
813
|
+
expires_in_seconds: Token lifetime (default: 30 days)
|
|
814
|
+
|
|
815
|
+
Returns:
|
|
816
|
+
{"token_id": "...", "token": "raw-token-one-time-visible"}
|
|
817
|
+
"""
|
|
818
|
+
raw_token = generate_token()
|
|
819
|
+
token_hash = hash_token(raw_token)
|
|
820
|
+
|
|
821
|
+
result = await self._execute(
|
|
822
|
+
"auth.add_refresh_token",
|
|
823
|
+
user_id=user_id,
|
|
824
|
+
client_psk_id=client_psk_id,
|
|
825
|
+
token_hash=token_hash,
|
|
826
|
+
expires_in_seconds=expires_in_seconds
|
|
827
|
+
)
|
|
828
|
+
return {
|
|
829
|
+
"token_id": result.get("id"),
|
|
830
|
+
"token": raw_token, # One-time visible
|
|
831
|
+
"expires_at": result.get("expires_at")
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async def delete_refresh_token(self, token_id: str) -> Dict[str, Any]:
|
|
835
|
+
"""
|
|
836
|
+
Delete/revoke a refresh token.
|
|
837
|
+
|
|
838
|
+
Args:
|
|
839
|
+
token_id: UUID of token to delete
|
|
840
|
+
|
|
841
|
+
Returns:
|
|
842
|
+
Deletion confirmation
|
|
843
|
+
"""
|
|
844
|
+
return await self._execute(
|
|
845
|
+
"auth.delete_refresh_token",
|
|
846
|
+
token_id=token_id
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
async def list_refresh_tokens(
|
|
850
|
+
self,
|
|
851
|
+
user_id: Optional[str] = None,
|
|
852
|
+
client_psk_id: Optional[str] = None
|
|
853
|
+
) -> List[Dict[str, Any]]:
|
|
854
|
+
"""
|
|
855
|
+
List refresh tokens, optionally filtered.
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
user_id: Filter by user
|
|
859
|
+
client_psk_id: Filter by client
|
|
860
|
+
|
|
861
|
+
Returns:
|
|
862
|
+
List of token records (without token_hash)
|
|
863
|
+
"""
|
|
864
|
+
return await self._execute(
|
|
865
|
+
"auth.list_refresh_tokens",
|
|
866
|
+
user_id=user_id,
|
|
867
|
+
client_psk_id=client_psk_id
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# ========================================
|
|
871
|
+
# JWT OPERATIONS (via Sovereign)
|
|
872
|
+
# ========================================
|
|
873
|
+
|
|
874
|
+
async def mint_subsidiary_jwt(
|
|
875
|
+
self,
|
|
876
|
+
user_id: str,
|
|
877
|
+
scope: Optional[List[str]] = None,
|
|
878
|
+
expires_in: int = 900,
|
|
879
|
+
system: str = "user"
|
|
880
|
+
) -> Dict[str, Any]:
|
|
881
|
+
"""
|
|
882
|
+
Mint a JWT for a subsidiary user.
|
|
883
|
+
|
|
884
|
+
Uses DOMINUS_TOKEN to authenticate with Sovereign, then
|
|
885
|
+
requests a JWT with the specified subsidiary user_id.
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
user_id: Subsidiary user identifier to embed in JWT
|
|
889
|
+
scope: List of scopes to include
|
|
890
|
+
expires_in: Token lifetime in seconds (default: 15 min)
|
|
891
|
+
system: System identifier (default: "user")
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
{"access_token": "...", "token_type": "Bearer", "expires_in": ...}
|
|
895
|
+
"""
|
|
896
|
+
if not self._execute_sovereign:
|
|
897
|
+
raise RuntimeError("Sovereign command executor not available")
|
|
898
|
+
|
|
899
|
+
return await self._execute_sovereign(
|
|
900
|
+
"auth.mint_token",
|
|
901
|
+
user_id=user_id,
|
|
902
|
+
scope=scope,
|
|
903
|
+
system=system
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
async def get_public_key(self) -> str:
|
|
907
|
+
"""
|
|
908
|
+
Get Sovereign's public key for JWT validation.
|
|
909
|
+
|
|
910
|
+
Caches the key after first fetch.
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
PEM-encoded public key string
|
|
914
|
+
"""
|
|
915
|
+
if self._public_key_cache:
|
|
916
|
+
return self._public_key_cache
|
|
917
|
+
|
|
918
|
+
if not self._execute_sovereign:
|
|
919
|
+
raise RuntimeError("Sovereign command executor not available")
|
|
920
|
+
|
|
921
|
+
result = await self._execute_sovereign("auth.get_public_key")
|
|
922
|
+
self._public_key_cache = result.get("public_key")
|
|
923
|
+
return self._public_key_cache
|
|
924
|
+
|
|
925
|
+
async def validate_jwt(self, token: str) -> Dict[str, Any]:
|
|
926
|
+
"""
|
|
927
|
+
Validate a JWT and return its claims.
|
|
928
|
+
|
|
929
|
+
Fetches public key from Sovereign (cached) and validates locally.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
token: JWT string to validate
|
|
933
|
+
|
|
934
|
+
Returns:
|
|
935
|
+
Decoded JWT claims if valid
|
|
936
|
+
|
|
937
|
+
Raises:
|
|
938
|
+
ValueError: If token is invalid or expired
|
|
939
|
+
"""
|
|
940
|
+
import jwt as pyjwt
|
|
941
|
+
|
|
942
|
+
public_key = await self.get_public_key()
|
|
943
|
+
|
|
944
|
+
try:
|
|
945
|
+
claims = pyjwt.decode(
|
|
946
|
+
token,
|
|
947
|
+
public_key,
|
|
948
|
+
algorithms=["RS256"],
|
|
949
|
+
options={"verify_exp": True}
|
|
950
|
+
)
|
|
951
|
+
return claims
|
|
952
|
+
except pyjwt.ExpiredSignatureError:
|
|
953
|
+
raise ValueError("Token has expired")
|
|
954
|
+
except pyjwt.InvalidTokenError as e:
|
|
955
|
+
raise ValueError(f"Invalid token: {e}")
|
|
956
|
+
|
|
957
|
+
# ========================================
|
|
958
|
+
# PAGE ACCESS CONTROL (auth.pages table)
|
|
959
|
+
# ========================================
|
|
960
|
+
|
|
961
|
+
async def check_page_access(
|
|
962
|
+
self,
|
|
963
|
+
page_path: str,
|
|
964
|
+
user_jwt: Optional[str] = None
|
|
965
|
+
) -> Dict[str, Any]:
|
|
966
|
+
"""
|
|
967
|
+
Check if user has access to a specific page.
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
page_path: Page path (e.g., "/dashboard/admin")
|
|
971
|
+
user_jwt: Optional user's JWT token (for extracting scopes)
|
|
972
|
+
|
|
973
|
+
Returns:
|
|
974
|
+
{
|
|
975
|
+
"allowed": bool,
|
|
976
|
+
"reason": str (optional, if not allowed)
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
Logic (handled by Architect backend):
|
|
980
|
+
1. Query auth.pages for page_id
|
|
981
|
+
2. If page not found, return allowed=True (unregistered pages are open)
|
|
982
|
+
3. Extract scopes from user JWT
|
|
983
|
+
4. Compare JWT scopes with page's required_scopes
|
|
984
|
+
5. Return access decision
|
|
985
|
+
"""
|
|
986
|
+
return await self._execute(
|
|
987
|
+
"auth.check_page_access",
|
|
988
|
+
page_path=page_path,
|
|
989
|
+
user_jwt=user_jwt
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
# ========================================
|
|
993
|
+
# SCOPE NAVIGATION (auth.scope_navigation table)
|
|
994
|
+
# ========================================
|
|
995
|
+
|
|
996
|
+
async def get_scope_navigation(
|
|
997
|
+
self,
|
|
998
|
+
scope_id: str
|
|
999
|
+
) -> List[Dict[str, Any]]:
|
|
1000
|
+
"""
|
|
1001
|
+
Get navigation items for a specific scope.
|
|
1002
|
+
|
|
1003
|
+
Args:
|
|
1004
|
+
scope_id: UUID of the scope
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
List of navigation items (from auth.scope_navigation table)
|
|
1008
|
+
Sorted by order field
|
|
1009
|
+
"""
|
|
1010
|
+
return await self._execute(
|
|
1011
|
+
"auth.get_scope_navigation",
|
|
1012
|
+
scope_id=scope_id
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
async def get_user_navigation_scopes(
|
|
1016
|
+
self,
|
|
1017
|
+
user_id: str,
|
|
1018
|
+
jwt_scopes: List[str],
|
|
1019
|
+
tenant_category_name: str = 'admin'
|
|
1020
|
+
) -> List[Dict[str, Any]]:
|
|
1021
|
+
"""
|
|
1022
|
+
Get all navigation scopes available to a user.
|
|
1023
|
+
|
|
1024
|
+
Args:
|
|
1025
|
+
user_id: User UUID
|
|
1026
|
+
jwt_scopes: List of scope slugs from user's JWT
|
|
1027
|
+
tenant_category_name: Name of tenant category to filter by (default: 'admin')
|
|
1028
|
+
|
|
1029
|
+
Returns:
|
|
1030
|
+
List of Scope objects for navigation
|
|
1031
|
+
Filtered by:
|
|
1032
|
+
- tenant_category_id = category matching tenant_category_name
|
|
1033
|
+
- scope slug in jwt_scopes
|
|
1034
|
+
"""
|
|
1035
|
+
return await self._execute(
|
|
1036
|
+
"auth.get_user_navigation_scopes",
|
|
1037
|
+
user_id=user_id,
|
|
1038
|
+
jwt_scopes=jwt_scopes,
|
|
1039
|
+
tenant_category_name=tenant_category_name
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
# ========================================
|
|
1043
|
+
# USER PREFERENCES (auth.user_preferences table)
|
|
1044
|
+
# ========================================
|
|
1045
|
+
|
|
1046
|
+
async def get_user_preferences(
|
|
1047
|
+
self,
|
|
1048
|
+
user_id: str
|
|
1049
|
+
) -> Dict[str, Any]:
|
|
1050
|
+
"""
|
|
1051
|
+
Get user preferences from auth.user_preferences table.
|
|
1052
|
+
|
|
1053
|
+
Args:
|
|
1054
|
+
user_id: User UUID
|
|
1055
|
+
|
|
1056
|
+
Returns:
|
|
1057
|
+
Preferences JSONB object (defaults to empty dict if not found)
|
|
1058
|
+
"""
|
|
1059
|
+
result = await self._execute(
|
|
1060
|
+
"auth.get_user_preferences",
|
|
1061
|
+
user_id=user_id
|
|
1062
|
+
)
|
|
1063
|
+
# Return empty dict if not found, otherwise return preferences
|
|
1064
|
+
return result.get("preferences", {}) if result else {}
|
|
1065
|
+
|
|
1066
|
+
async def set_user_preference(
|
|
1067
|
+
self,
|
|
1068
|
+
user_id: str,
|
|
1069
|
+
key: str,
|
|
1070
|
+
value: Any
|
|
1071
|
+
) -> Dict[str, Any]:
|
|
1072
|
+
"""
|
|
1073
|
+
Set a user preference.
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
user_id: User UUID
|
|
1077
|
+
key: Preference key (e.g., "default_scope_id", "theme")
|
|
1078
|
+
value: Preference value
|
|
1079
|
+
|
|
1080
|
+
Returns:
|
|
1081
|
+
Updated preferences object
|
|
1082
|
+
"""
|
|
1083
|
+
return await self._execute(
|
|
1084
|
+
"auth.set_user_preference",
|
|
1085
|
+
user_id=user_id,
|
|
1086
|
+
key=key,
|
|
1087
|
+
value=value
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
class SQLSchemaNamespace:
|
|
1092
|
+
"""
|
|
1093
|
+
Schema DDL operations namespace.
|
|
1094
|
+
|
|
1095
|
+
Provides functions for managing table structure in app and secure schemas:
|
|
1096
|
+
- Create/drop tables
|
|
1097
|
+
- Add/drop columns
|
|
1098
|
+
- List tables and columns
|
|
1099
|
+
|
|
1100
|
+
DB User: schema_user (DDL on app/secure, CRUD on meta)
|
|
1101
|
+
"""
|
|
1102
|
+
|
|
1103
|
+
def __init__(self, _execute_command):
|
|
1104
|
+
"""
|
|
1105
|
+
Initialize schema namespace.
|
|
1106
|
+
|
|
1107
|
+
Args:
|
|
1108
|
+
_execute_command: Internal function to execute commands
|
|
1109
|
+
"""
|
|
1110
|
+
self._execute = _execute_command
|
|
1111
|
+
|
|
1112
|
+
# ========================================
|
|
1113
|
+
# APP SCHEMA (default)
|
|
1114
|
+
# ========================================
|
|
1115
|
+
|
|
1116
|
+
async def add_table(
|
|
1117
|
+
self,
|
|
1118
|
+
table_name: str,
|
|
1119
|
+
columns: List[Dict[str, Any]]
|
|
1120
|
+
) -> Dict[str, Any]:
|
|
1121
|
+
"""
|
|
1122
|
+
Create a new table in the app schema.
|
|
1123
|
+
|
|
1124
|
+
Args:
|
|
1125
|
+
table_name: Name of table to create
|
|
1126
|
+
columns: List of column definitions
|
|
1127
|
+
[{"name": "id", "type": "UUID", "constraints": ["PRIMARY KEY"]}]
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
Creation confirmation
|
|
1131
|
+
"""
|
|
1132
|
+
return await self._execute(
|
|
1133
|
+
"schema.add_table",
|
|
1134
|
+
table_name=table_name,
|
|
1135
|
+
columns=columns
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
async def delete_table(self, table_name: str) -> Dict[str, Any]:
|
|
1139
|
+
"""
|
|
1140
|
+
Drop a table from the app schema.
|
|
1141
|
+
|
|
1142
|
+
Args:
|
|
1143
|
+
table_name: Name of table to drop
|
|
1144
|
+
|
|
1145
|
+
Returns:
|
|
1146
|
+
Deletion confirmation
|
|
1147
|
+
"""
|
|
1148
|
+
return await self._execute(
|
|
1149
|
+
"schema.delete_table",
|
|
1150
|
+
table_name=table_name
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
async def list_tables(self) -> List[Dict[str, Any]]:
|
|
1154
|
+
"""
|
|
1155
|
+
List all tables in the app schema.
|
|
1156
|
+
|
|
1157
|
+
Returns:
|
|
1158
|
+
List of table information
|
|
1159
|
+
"""
|
|
1160
|
+
return await self._execute("schema.list_tables")
|
|
1161
|
+
|
|
1162
|
+
async def list_columns(self, table_name: str) -> List[Dict[str, Any]]:
|
|
1163
|
+
"""
|
|
1164
|
+
List columns in a table in the app schema.
|
|
1165
|
+
|
|
1166
|
+
Args:
|
|
1167
|
+
table_name: Name of table
|
|
1168
|
+
|
|
1169
|
+
Returns:
|
|
1170
|
+
List of column information
|
|
1171
|
+
"""
|
|
1172
|
+
return await self._execute(
|
|
1173
|
+
"schema.list_columns",
|
|
1174
|
+
table_name=table_name
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
async def add_column(
|
|
1178
|
+
self,
|
|
1179
|
+
table_name: str,
|
|
1180
|
+
column_name: str,
|
|
1181
|
+
column_type: str,
|
|
1182
|
+
constraints: Optional[List[str]] = None,
|
|
1183
|
+
default: Optional[str] = None
|
|
1184
|
+
) -> Dict[str, Any]:
|
|
1185
|
+
"""
|
|
1186
|
+
Add a column to a table in the app schema.
|
|
1187
|
+
|
|
1188
|
+
Args:
|
|
1189
|
+
table_name: Name of table
|
|
1190
|
+
column_name: Name of new column
|
|
1191
|
+
column_type: PostgreSQL type (e.g., "VARCHAR(100)", "INTEGER")
|
|
1192
|
+
constraints: Optional constraints (e.g., ["NOT NULL"])
|
|
1193
|
+
default: Optional default value
|
|
1194
|
+
|
|
1195
|
+
Returns:
|
|
1196
|
+
Alteration confirmation
|
|
1197
|
+
"""
|
|
1198
|
+
return await self._execute(
|
|
1199
|
+
"schema.add_column",
|
|
1200
|
+
table_name=table_name,
|
|
1201
|
+
column_name=column_name,
|
|
1202
|
+
column_type=column_type,
|
|
1203
|
+
constraints=constraints,
|
|
1204
|
+
default=default
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
async def delete_column(
|
|
1208
|
+
self,
|
|
1209
|
+
table_name: str,
|
|
1210
|
+
column_name: str
|
|
1211
|
+
) -> Dict[str, Any]:
|
|
1212
|
+
"""
|
|
1213
|
+
Drop a column from a table in the app schema.
|
|
1214
|
+
|
|
1215
|
+
Args:
|
|
1216
|
+
table_name: Name of table
|
|
1217
|
+
column_name: Name of column to drop
|
|
1218
|
+
|
|
1219
|
+
Returns:
|
|
1220
|
+
Alteration confirmation
|
|
1221
|
+
"""
|
|
1222
|
+
return await self._execute(
|
|
1223
|
+
"schema.delete_column",
|
|
1224
|
+
table_name=table_name,
|
|
1225
|
+
column_name=column_name
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
# ========================================
|
|
1229
|
+
# SECURE SCHEMA (explicit prefix)
|
|
1230
|
+
# ========================================
|
|
1231
|
+
|
|
1232
|
+
async def secure_add_table(
|
|
1233
|
+
self,
|
|
1234
|
+
table_name: str,
|
|
1235
|
+
columns: List[Dict[str, Any]]
|
|
1236
|
+
) -> Dict[str, Any]:
|
|
1237
|
+
"""
|
|
1238
|
+
Create a new table in the secure schema.
|
|
1239
|
+
|
|
1240
|
+
Args:
|
|
1241
|
+
table_name: Name of table to create
|
|
1242
|
+
columns: List of column definitions
|
|
1243
|
+
|
|
1244
|
+
Returns:
|
|
1245
|
+
Creation confirmation
|
|
1246
|
+
"""
|
|
1247
|
+
return await self._execute(
|
|
1248
|
+
"schema.secure_add_table",
|
|
1249
|
+
table_name=table_name,
|
|
1250
|
+
columns=columns
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
async def secure_delete_table(self, table_name: str) -> Dict[str, Any]:
|
|
1254
|
+
"""
|
|
1255
|
+
Drop a table from the secure schema.
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
table_name: Name of table to drop
|
|
1259
|
+
|
|
1260
|
+
Returns:
|
|
1261
|
+
Deletion confirmation
|
|
1262
|
+
"""
|
|
1263
|
+
return await self._execute(
|
|
1264
|
+
"schema.secure_delete_table",
|
|
1265
|
+
table_name=table_name
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
async def secure_list_tables(self) -> List[Dict[str, Any]]:
|
|
1269
|
+
"""
|
|
1270
|
+
List all tables in the secure schema.
|
|
1271
|
+
|
|
1272
|
+
Returns:
|
|
1273
|
+
List of table information
|
|
1274
|
+
"""
|
|
1275
|
+
return await self._execute("schema.secure_list_tables")
|
|
1276
|
+
|
|
1277
|
+
async def secure_list_columns(self, table_name: str) -> List[Dict[str, Any]]:
|
|
1278
|
+
"""
|
|
1279
|
+
List columns in a table in the secure schema.
|
|
1280
|
+
|
|
1281
|
+
Args:
|
|
1282
|
+
table_name: Name of table
|
|
1283
|
+
|
|
1284
|
+
Returns:
|
|
1285
|
+
List of column information
|
|
1286
|
+
"""
|
|
1287
|
+
return await self._execute(
|
|
1288
|
+
"schema.secure_list_columns",
|
|
1289
|
+
table_name=table_name
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
async def secure_add_column(
|
|
1293
|
+
self,
|
|
1294
|
+
table_name: str,
|
|
1295
|
+
column_name: str,
|
|
1296
|
+
column_type: str,
|
|
1297
|
+
constraints: Optional[List[str]] = None,
|
|
1298
|
+
default: Optional[str] = None
|
|
1299
|
+
) -> Dict[str, Any]:
|
|
1300
|
+
"""
|
|
1301
|
+
Add a column to a table in the secure schema.
|
|
1302
|
+
|
|
1303
|
+
Args:
|
|
1304
|
+
table_name: Name of table
|
|
1305
|
+
column_name: Name of new column
|
|
1306
|
+
column_type: PostgreSQL type
|
|
1307
|
+
constraints: Optional constraints
|
|
1308
|
+
default: Optional default value
|
|
1309
|
+
|
|
1310
|
+
Returns:
|
|
1311
|
+
Alteration confirmation
|
|
1312
|
+
"""
|
|
1313
|
+
return await self._execute(
|
|
1314
|
+
"schema.secure_add_column",
|
|
1315
|
+
table_name=table_name,
|
|
1316
|
+
column_name=column_name,
|
|
1317
|
+
column_type=column_type,
|
|
1318
|
+
constraints=constraints,
|
|
1319
|
+
default=default
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
async def secure_delete_column(
|
|
1323
|
+
self,
|
|
1324
|
+
table_name: str,
|
|
1325
|
+
column_name: str
|
|
1326
|
+
) -> Dict[str, Any]:
|
|
1327
|
+
"""
|
|
1328
|
+
Drop a column from a table in the secure schema.
|
|
1329
|
+
|
|
1330
|
+
Args:
|
|
1331
|
+
table_name: Name of table
|
|
1332
|
+
column_name: Name of column to drop
|
|
1333
|
+
|
|
1334
|
+
Returns:
|
|
1335
|
+
Alteration confirmation
|
|
1336
|
+
"""
|
|
1337
|
+
return await self._execute(
|
|
1338
|
+
"schema.secure_delete_column",
|
|
1339
|
+
table_name=table_name,
|
|
1340
|
+
column_name=column_name
|
|
1341
|
+
)
|