dominus-sdk-python-dev 2.1.6__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.
@@ -0,0 +1,1056 @@
1
+ """
2
+ Auth Namespace - Guardian authentication and authorization operations.
3
+
4
+ Provides management of users, roles, scopes, tenants, clients, pages, and navigation.
5
+ Uses RESTful patterns matching the Guardian backend.
6
+ """
7
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from ..start import Dominus
11
+
12
+
13
+ class AuthNamespace:
14
+ """
15
+ Authentication and authorization namespace.
16
+
17
+ All auth operations go through /api/guardian/* endpoints.
18
+ Uses RESTful patterns: GET/POST/PUT/DELETE with path params.
19
+
20
+ Usage:
21
+ # User management
22
+ user = await dominus.auth.create_user(username="john", email="j@ex.com", password="secret")
23
+ users = await dominus.auth.list_users(status="active")
24
+ await dominus.auth.update_password(user_id, "new_password")
25
+
26
+ # Role management
27
+ role = await dominus.auth.create_role(name="admin", description="Admin role")
28
+ roles = await dominus.auth.list_roles()
29
+
30
+ # JWT validation
31
+ claims = await dominus.auth.validate_jwt(token)
32
+ """
33
+
34
+ def __init__(self, client: "Dominus"):
35
+ self._client = client
36
+ self._public_key_cache = None
37
+
38
+ # ========================================
39
+ # USERS
40
+ # ========================================
41
+
42
+ async def create_user(
43
+ self,
44
+ username: str,
45
+ email: str,
46
+ password: str,
47
+ status: str = "active"
48
+ ) -> Dict[str, Any]:
49
+ """
50
+ Create a new user.
51
+
52
+ Args:
53
+ username: Unique username
54
+ email: Email address
55
+ password: Raw password (will be hashed server-side)
56
+ status: User status (default: "active")
57
+
58
+ Returns:
59
+ Created user record (without password_hash)
60
+ """
61
+ return await self._client._request(
62
+ endpoint="/api/guardian/users",
63
+ body={
64
+ "username": username,
65
+ "email": email,
66
+ "password": password,
67
+ "status": status
68
+ }
69
+ )
70
+
71
+ async def get_user(self, user_id: str) -> Dict[str, Any]:
72
+ """Get user by ID."""
73
+ return await self._client._request(
74
+ endpoint=f"/api/guardian/users/{user_id}",
75
+ method="GET"
76
+ )
77
+
78
+ async def list_users(
79
+ self,
80
+ status: Optional[str] = None,
81
+ limit: int = 100,
82
+ offset: int = 0,
83
+ order_by: str = "created_at",
84
+ order_desc: bool = True
85
+ ) -> Dict[str, Any]:
86
+ """
87
+ List users with optional filters.
88
+
89
+ Returns:
90
+ Dict with "items" list and "total" count
91
+ """
92
+ params = f"?limit={limit}&offset={offset}&order_by={order_by}&order_desc={str(order_desc).lower()}"
93
+ if status:
94
+ params += f"&status={status}"
95
+
96
+ return await self._client._request(
97
+ endpoint=f"/api/guardian/users{params}",
98
+ method="GET"
99
+ )
100
+
101
+ async def update_user(
102
+ self,
103
+ user_id: str,
104
+ username: Optional[str] = None,
105
+ email: Optional[str] = None,
106
+ status: Optional[str] = None
107
+ ) -> Dict[str, Any]:
108
+ """Update user fields."""
109
+ body = {}
110
+ if username is not None:
111
+ body["username"] = username
112
+ if email is not None:
113
+ body["email"] = email
114
+ if status is not None:
115
+ body["status"] = status
116
+
117
+ return await self._client._request(
118
+ endpoint=f"/api/guardian/users/{user_id}",
119
+ method="PUT",
120
+ body=body
121
+ )
122
+
123
+ async def delete_user(self, user_id: str) -> Dict[str, Any]:
124
+ """Delete a user."""
125
+ return await self._client._request(
126
+ endpoint=f"/api/guardian/users/{user_id}",
127
+ method="DELETE"
128
+ )
129
+
130
+ async def update_password(self, user_id: str, password: str) -> Dict[str, Any]:
131
+ """Update user password."""
132
+ return await self._client._request(
133
+ endpoint=f"/api/guardian/users/{user_id}/password",
134
+ method="PUT",
135
+ body={"password": password}
136
+ )
137
+
138
+ async def verify_password(self, user_id: str, password: str) -> Dict[str, Any]:
139
+ """Verify user password."""
140
+ return await self._client._request(
141
+ endpoint=f"/api/guardian/users/{user_id}/verify-password",
142
+ body={"password": password}
143
+ )
144
+
145
+ # User junction tables
146
+ async def get_user_roles(self, user_id: str) -> List[Dict[str, Any]]:
147
+ """Get roles assigned to user."""
148
+ return await self._client._request(
149
+ endpoint=f"/api/guardian/users/{user_id}/roles",
150
+ method="GET"
151
+ )
152
+
153
+ async def add_user_roles(self, user_id: str, role_ids: List[str]) -> Dict[str, Any]:
154
+ """Add roles to user."""
155
+ return await self._client._request(
156
+ endpoint=f"/api/guardian/users/{user_id}/roles",
157
+ body={"role_ids": role_ids}
158
+ )
159
+
160
+ async def remove_user_roles(self, user_id: str, role_ids: List[str]) -> Dict[str, Any]:
161
+ """Remove roles from user."""
162
+ return await self._client._request(
163
+ endpoint=f"/api/guardian/users/{user_id}/roles",
164
+ method="DELETE",
165
+ body={"role_ids": role_ids}
166
+ )
167
+
168
+ async def get_user_scopes(self, user_id: str) -> List[Dict[str, Any]]:
169
+ """Get scopes assigned to user."""
170
+ return await self._client._request(
171
+ endpoint=f"/api/guardian/users/{user_id}/scopes",
172
+ method="GET"
173
+ )
174
+
175
+ async def add_user_scopes(self, user_id: str, scope_ids: List[str]) -> Dict[str, Any]:
176
+ """Add scopes to user."""
177
+ return await self._client._request(
178
+ endpoint=f"/api/guardian/users/{user_id}/scopes",
179
+ body={"scope_ids": scope_ids}
180
+ )
181
+
182
+ async def remove_user_scopes(self, user_id: str, scope_ids: List[str]) -> Dict[str, Any]:
183
+ """Remove scopes from user."""
184
+ return await self._client._request(
185
+ endpoint=f"/api/guardian/users/{user_id}/scopes",
186
+ method="DELETE",
187
+ body={"scope_ids": scope_ids}
188
+ )
189
+
190
+ async def get_user_tenants(self, user_id: str) -> List[Dict[str, Any]]:
191
+ """Get tenants assigned to user."""
192
+ return await self._client._request(
193
+ endpoint=f"/api/guardian/users/{user_id}/tenants",
194
+ method="GET"
195
+ )
196
+
197
+ async def add_user_tenants(self, user_id: str, tenant_ids: List[str]) -> Dict[str, Any]:
198
+ """Add tenants to user."""
199
+ return await self._client._request(
200
+ endpoint=f"/api/guardian/users/{user_id}/tenants",
201
+ body={"tenant_ids": tenant_ids}
202
+ )
203
+
204
+ async def remove_user_tenants(self, user_id: str, tenant_ids: List[str]) -> Dict[str, Any]:
205
+ """Remove tenants from user."""
206
+ return await self._client._request(
207
+ endpoint=f"/api/guardian/users/{user_id}/tenants",
208
+ method="DELETE",
209
+ body={"tenant_ids": tenant_ids}
210
+ )
211
+
212
+ async def get_user_subtypes(self, user_id: str) -> List[Dict[str, Any]]:
213
+ """Get subtypes assigned to user."""
214
+ return await self._client._request(
215
+ endpoint=f"/api/guardian/users/{user_id}/subtypes",
216
+ method="GET"
217
+ )
218
+
219
+ async def add_user_subtypes(self, user_id: str, subtype_ids: List[str]) -> Dict[str, Any]:
220
+ """Add subtypes to user."""
221
+ return await self._client._request(
222
+ endpoint=f"/api/guardian/users/{user_id}/subtypes",
223
+ body={"subtype_ids": subtype_ids}
224
+ )
225
+
226
+ async def remove_user_subtypes(self, user_id: str, subtype_ids: List[str]) -> Dict[str, Any]:
227
+ """Remove subtypes from user."""
228
+ return await self._client._request(
229
+ endpoint=f"/api/guardian/users/{user_id}/subtypes",
230
+ method="DELETE",
231
+ body={"subtype_ids": subtype_ids}
232
+ )
233
+
234
+ # ========================================
235
+ # ROLES
236
+ # ========================================
237
+
238
+ async def create_role(
239
+ self,
240
+ name: str,
241
+ description: Optional[str] = None
242
+ ) -> Dict[str, Any]:
243
+ """Create a new role."""
244
+ body = {"name": name}
245
+ if description:
246
+ body["description"] = description
247
+
248
+ return await self._client._request(
249
+ endpoint="/api/guardian/roles",
250
+ body=body
251
+ )
252
+
253
+ async def get_role(self, role_id: str) -> Dict[str, Any]:
254
+ """Get role by ID."""
255
+ return await self._client._request(
256
+ endpoint=f"/api/guardian/roles/{role_id}",
257
+ method="GET"
258
+ )
259
+
260
+ async def list_roles(
261
+ self,
262
+ limit: int = 100,
263
+ offset: int = 0
264
+ ) -> Dict[str, Any]:
265
+ """List all roles."""
266
+ return await self._client._request(
267
+ endpoint=f"/api/guardian/roles?limit={limit}&offset={offset}",
268
+ method="GET"
269
+ )
270
+
271
+ async def update_role(
272
+ self,
273
+ role_id: str,
274
+ name: Optional[str] = None,
275
+ description: Optional[str] = None
276
+ ) -> Dict[str, Any]:
277
+ """Update role."""
278
+ body = {}
279
+ if name is not None:
280
+ body["name"] = name
281
+ if description is not None:
282
+ body["description"] = description
283
+
284
+ return await self._client._request(
285
+ endpoint=f"/api/guardian/roles/{role_id}",
286
+ method="PUT",
287
+ body=body
288
+ )
289
+
290
+ async def delete_role(self, role_id: str) -> Dict[str, Any]:
291
+ """Delete a role."""
292
+ return await self._client._request(
293
+ endpoint=f"/api/guardian/roles/{role_id}",
294
+ method="DELETE"
295
+ )
296
+
297
+ async def get_role_scopes(self, role_id: str) -> List[Dict[str, Any]]:
298
+ """Get scopes assigned to role."""
299
+ return await self._client._request(
300
+ endpoint=f"/api/guardian/roles/{role_id}/scopes",
301
+ method="GET"
302
+ )
303
+
304
+ async def add_role_scopes(self, role_id: str, scope_ids: List[str]) -> Dict[str, Any]:
305
+ """Add scopes to role."""
306
+ return await self._client._request(
307
+ endpoint=f"/api/guardian/roles/{role_id}/scopes",
308
+ body={"scope_ids": scope_ids}
309
+ )
310
+
311
+ async def remove_role_scopes(self, role_id: str, scope_ids: List[str]) -> Dict[str, Any]:
312
+ """Remove scopes from role."""
313
+ return await self._client._request(
314
+ endpoint=f"/api/guardian/roles/{role_id}/scopes",
315
+ method="DELETE",
316
+ body={"scope_ids": scope_ids}
317
+ )
318
+
319
+ # ========================================
320
+ # SCOPES
321
+ # ========================================
322
+
323
+ async def create_scope(
324
+ self,
325
+ slug: str,
326
+ display_name: str,
327
+ description: Optional[str] = None
328
+ ) -> Dict[str, Any]:
329
+ """Create a new scope."""
330
+ body = {"slug": slug, "display_name": display_name}
331
+ if description:
332
+ body["description"] = description
333
+
334
+ return await self._client._request(
335
+ endpoint="/api/guardian/scopes",
336
+ body=body
337
+ )
338
+
339
+ async def get_scope(self, scope_id: str) -> Dict[str, Any]:
340
+ """Get scope by ID."""
341
+ return await self._client._request(
342
+ endpoint=f"/api/guardian/scopes/{scope_id}",
343
+ method="GET"
344
+ )
345
+
346
+ async def list_scopes(
347
+ self,
348
+ limit: int = 100,
349
+ offset: int = 0
350
+ ) -> Dict[str, Any]:
351
+ """List all scopes."""
352
+ return await self._client._request(
353
+ endpoint=f"/api/guardian/scopes?limit={limit}&offset={offset}",
354
+ method="GET"
355
+ )
356
+
357
+ async def update_scope(
358
+ self,
359
+ scope_id: str,
360
+ slug: Optional[str] = None,
361
+ display_name: Optional[str] = None,
362
+ description: Optional[str] = None
363
+ ) -> Dict[str, Any]:
364
+ """Update scope."""
365
+ body = {}
366
+ if slug is not None:
367
+ body["slug"] = slug
368
+ if display_name is not None:
369
+ body["display_name"] = display_name
370
+ if description is not None:
371
+ body["description"] = description
372
+
373
+ return await self._client._request(
374
+ endpoint=f"/api/guardian/scopes/{scope_id}",
375
+ method="PUT",
376
+ body=body
377
+ )
378
+
379
+ async def delete_scope(self, scope_id: str) -> Dict[str, Any]:
380
+ """Delete a scope."""
381
+ return await self._client._request(
382
+ endpoint=f"/api/guardian/scopes/{scope_id}",
383
+ method="DELETE"
384
+ )
385
+
386
+ # ========================================
387
+ # CLIENTS (PSK/Machine Auth)
388
+ # ========================================
389
+
390
+ async def create_client(
391
+ self,
392
+ label: str,
393
+ description: Optional[str] = None
394
+ ) -> Dict[str, Any]:
395
+ """
396
+ Create a new service client with PSK.
397
+
398
+ Returns dict with client info and one-time-visible "psk".
399
+ """
400
+ body = {"label": label}
401
+ if description:
402
+ body["description"] = description
403
+
404
+ return await self._client._request(
405
+ endpoint="/api/guardian/clients",
406
+ body=body
407
+ )
408
+
409
+ async def get_client(self, client_id: str) -> Dict[str, Any]:
410
+ """Get client by ID."""
411
+ return await self._client._request(
412
+ endpoint=f"/api/guardian/clients/{client_id}",
413
+ method="GET"
414
+ )
415
+
416
+ async def list_clients(
417
+ self,
418
+ limit: int = 100,
419
+ offset: int = 0
420
+ ) -> Dict[str, Any]:
421
+ """List all clients."""
422
+ return await self._client._request(
423
+ endpoint=f"/api/guardian/clients?limit={limit}&offset={offset}",
424
+ method="GET"
425
+ )
426
+
427
+ async def update_client(
428
+ self,
429
+ client_id: str,
430
+ label: Optional[str] = None,
431
+ description: Optional[str] = None,
432
+ status: Optional[str] = None
433
+ ) -> Dict[str, Any]:
434
+ """Update client."""
435
+ body = {}
436
+ if label is not None:
437
+ body["label"] = label
438
+ if description is not None:
439
+ body["description"] = description
440
+ if status is not None:
441
+ body["status"] = status
442
+
443
+ return await self._client._request(
444
+ endpoint=f"/api/guardian/clients/{client_id}",
445
+ method="PUT",
446
+ body=body
447
+ )
448
+
449
+ async def delete_client(self, client_id: str) -> Dict[str, Any]:
450
+ """Delete a client."""
451
+ return await self._client._request(
452
+ endpoint=f"/api/guardian/clients/{client_id}",
453
+ method="DELETE"
454
+ )
455
+
456
+ async def regenerate_psk(self, client_id: str) -> Dict[str, Any]:
457
+ """Regenerate client PSK. Returns new one-time-visible PSK."""
458
+ return await self._client._request(
459
+ endpoint=f"/api/guardian/clients/{client_id}/regenerate-psk",
460
+ body={}
461
+ )
462
+
463
+ async def verify_psk(self, client_id: str, psk: str) -> Dict[str, Any]:
464
+ """Verify client PSK."""
465
+ return await self._client._request(
466
+ endpoint=f"/api/guardian/clients/{client_id}/verify-psk",
467
+ body={"psk": psk}
468
+ )
469
+
470
+ # Client junction tables
471
+ async def get_client_tenants(self, client_id: str) -> List[Dict[str, Any]]:
472
+ """Get tenants assigned to client."""
473
+ return await self._client._request(
474
+ endpoint=f"/api/guardian/clients/{client_id}/tenants",
475
+ method="GET"
476
+ )
477
+
478
+ async def add_client_tenants(self, client_id: str, tenant_ids: List[str]) -> Dict[str, Any]:
479
+ """Add tenants to client."""
480
+ return await self._client._request(
481
+ endpoint=f"/api/guardian/clients/{client_id}/tenants",
482
+ body={"tenant_ids": tenant_ids}
483
+ )
484
+
485
+ async def remove_client_tenants(self, client_id: str, tenant_ids: List[str]) -> Dict[str, Any]:
486
+ """Remove tenants from client."""
487
+ return await self._client._request(
488
+ endpoint=f"/api/guardian/clients/{client_id}/tenants",
489
+ method="DELETE",
490
+ body={"tenant_ids": tenant_ids}
491
+ )
492
+
493
+ # ========================================
494
+ # TENANTS
495
+ # ========================================
496
+
497
+ async def create_tenant(
498
+ self,
499
+ name: str,
500
+ slug: str,
501
+ category_id: Optional[str] = None,
502
+ display_name: Optional[str] = None,
503
+ description: Optional[str] = None
504
+ ) -> Dict[str, Any]:
505
+ """Create a new tenant."""
506
+ body = {"name": name, "slug": slug}
507
+ if category_id:
508
+ body["category_id"] = category_id
509
+ if display_name:
510
+ body["display_name"] = display_name
511
+ if description:
512
+ body["description"] = description
513
+
514
+ return await self._client._request(
515
+ endpoint="/api/guardian/tenants",
516
+ body=body
517
+ )
518
+
519
+ async def get_tenant(self, tenant_id: str) -> Dict[str, Any]:
520
+ """Get tenant by ID."""
521
+ return await self._client._request(
522
+ endpoint=f"/api/guardian/tenants/{tenant_id}",
523
+ method="GET"
524
+ )
525
+
526
+ async def list_tenants(
527
+ self,
528
+ status: Optional[str] = None,
529
+ category_id: Optional[str] = None,
530
+ limit: int = 100,
531
+ offset: int = 0
532
+ ) -> Dict[str, Any]:
533
+ """List tenants with optional filters."""
534
+ params = f"?limit={limit}&offset={offset}"
535
+ if status:
536
+ params += f"&status={status}"
537
+ if category_id:
538
+ params += f"&category_id={category_id}"
539
+
540
+ return await self._client._request(
541
+ endpoint=f"/api/guardian/tenants{params}",
542
+ method="GET"
543
+ )
544
+
545
+ async def update_tenant(
546
+ self,
547
+ tenant_id: str,
548
+ name: Optional[str] = None,
549
+ display_name: Optional[str] = None,
550
+ status: Optional[str] = None
551
+ ) -> Dict[str, Any]:
552
+ """Update tenant."""
553
+ body = {}
554
+ if name is not None:
555
+ body["name"] = name
556
+ if display_name is not None:
557
+ body["display_name"] = display_name
558
+ if status is not None:
559
+ body["status"] = status
560
+
561
+ return await self._client._request(
562
+ endpoint=f"/api/guardian/tenants/{tenant_id}",
563
+ method="PUT",
564
+ body=body
565
+ )
566
+
567
+ async def delete_tenant(self, tenant_id: str) -> Dict[str, Any]:
568
+ """Delete a tenant."""
569
+ return await self._client._request(
570
+ endpoint=f"/api/guardian/tenants/{tenant_id}",
571
+ method="DELETE"
572
+ )
573
+
574
+ # ========================================
575
+ # TENANT CATEGORIES
576
+ # ========================================
577
+
578
+ async def create_tenant_category(
579
+ self,
580
+ name: str,
581
+ slug: str,
582
+ description: Optional[str] = None,
583
+ color: str = "#3B82F6"
584
+ ) -> Dict[str, Any]:
585
+ """Create a new tenant category."""
586
+ body = {"name": name, "slug": slug, "color": color}
587
+ if description:
588
+ body["description"] = description
589
+
590
+ return await self._client._request(
591
+ endpoint="/api/guardian/tenant-categories",
592
+ body=body
593
+ )
594
+
595
+ async def get_tenant_category(self, category_id: str) -> Dict[str, Any]:
596
+ """Get tenant category by ID."""
597
+ return await self._client._request(
598
+ endpoint=f"/api/guardian/tenant-categories/{category_id}",
599
+ method="GET"
600
+ )
601
+
602
+ async def list_tenant_categories(
603
+ self,
604
+ limit: int = 100,
605
+ offset: int = 0
606
+ ) -> Dict[str, Any]:
607
+ """List all tenant categories."""
608
+ return await self._client._request(
609
+ endpoint=f"/api/guardian/tenant-categories?limit={limit}&offset={offset}",
610
+ method="GET"
611
+ )
612
+
613
+ async def update_tenant_category(
614
+ self,
615
+ category_id: str,
616
+ name: Optional[str] = None,
617
+ description: Optional[str] = None,
618
+ color: Optional[str] = None
619
+ ) -> Dict[str, Any]:
620
+ """Update tenant category."""
621
+ body = {}
622
+ if name is not None:
623
+ body["name"] = name
624
+ if description is not None:
625
+ body["description"] = description
626
+ if color is not None:
627
+ body["color"] = color
628
+
629
+ return await self._client._request(
630
+ endpoint=f"/api/guardian/tenant-categories/{category_id}",
631
+ method="PUT",
632
+ body=body
633
+ )
634
+
635
+ async def delete_tenant_category(self, category_id: str) -> Dict[str, Any]:
636
+ """Delete a tenant category."""
637
+ return await self._client._request(
638
+ endpoint=f"/api/guardian/tenant-categories/{category_id}",
639
+ method="DELETE"
640
+ )
641
+
642
+ # ========================================
643
+ # SUBTYPES
644
+ # ========================================
645
+
646
+ async def create_subtype(
647
+ self,
648
+ name: str,
649
+ slug: str,
650
+ description: Optional[str] = None
651
+ ) -> Dict[str, Any]:
652
+ """Create a new user subtype."""
653
+ body = {"name": name, "slug": slug}
654
+ if description:
655
+ body["description"] = description
656
+
657
+ return await self._client._request(
658
+ endpoint="/api/guardian/subtypes",
659
+ body=body
660
+ )
661
+
662
+ async def get_subtype(self, subtype_id: str) -> Dict[str, Any]:
663
+ """Get subtype by ID."""
664
+ return await self._client._request(
665
+ endpoint=f"/api/guardian/subtypes/{subtype_id}",
666
+ method="GET"
667
+ )
668
+
669
+ async def list_subtypes(
670
+ self,
671
+ limit: int = 100,
672
+ offset: int = 0
673
+ ) -> Dict[str, Any]:
674
+ """List all subtypes."""
675
+ return await self._client._request(
676
+ endpoint=f"/api/guardian/subtypes?limit={limit}&offset={offset}",
677
+ method="GET"
678
+ )
679
+
680
+ async def update_subtype(
681
+ self,
682
+ subtype_id: str,
683
+ name: Optional[str] = None,
684
+ description: Optional[str] = None
685
+ ) -> Dict[str, Any]:
686
+ """Update subtype."""
687
+ body = {}
688
+ if name is not None:
689
+ body["name"] = name
690
+ if description is not None:
691
+ body["description"] = description
692
+
693
+ return await self._client._request(
694
+ endpoint=f"/api/guardian/subtypes/{subtype_id}",
695
+ method="PUT",
696
+ body=body
697
+ )
698
+
699
+ async def delete_subtype(self, subtype_id: str) -> Dict[str, Any]:
700
+ """Delete a subtype."""
701
+ return await self._client._request(
702
+ endpoint=f"/api/guardian/subtypes/{subtype_id}",
703
+ method="DELETE"
704
+ )
705
+
706
+ # ========================================
707
+ # PAGES
708
+ # ========================================
709
+
710
+ async def create_page(
711
+ self,
712
+ path: str,
713
+ name: str,
714
+ description: Optional[str] = None,
715
+ is_active: bool = True
716
+ ) -> Dict[str, Any]:
717
+ """Create a new page."""
718
+ body = {"path": path, "name": name, "is_active": is_active}
719
+ if description:
720
+ body["description"] = description
721
+
722
+ return await self._client._request(
723
+ endpoint="/api/guardian/pages",
724
+ body=body
725
+ )
726
+
727
+ async def get_page(self, page_id: str) -> Dict[str, Any]:
728
+ """Get page by ID."""
729
+ return await self._client._request(
730
+ endpoint=f"/api/guardian/pages/{page_id}",
731
+ method="GET"
732
+ )
733
+
734
+ async def list_pages(
735
+ self,
736
+ is_active: Optional[bool] = None,
737
+ limit: int = 100,
738
+ offset: int = 0
739
+ ) -> Dict[str, Any]:
740
+ """List pages with optional filters."""
741
+ params = f"?limit={limit}&offset={offset}"
742
+ if is_active is not None:
743
+ params += f"&is_active={str(is_active).lower()}"
744
+
745
+ return await self._client._request(
746
+ endpoint=f"/api/guardian/pages{params}",
747
+ method="GET"
748
+ )
749
+
750
+ async def update_page(
751
+ self,
752
+ page_id: str,
753
+ path: Optional[str] = None,
754
+ name: Optional[str] = None,
755
+ description: Optional[str] = None,
756
+ is_active: Optional[bool] = None
757
+ ) -> Dict[str, Any]:
758
+ """Update page."""
759
+ body = {}
760
+ if path is not None:
761
+ body["path"] = path
762
+ if name is not None:
763
+ body["name"] = name
764
+ if description is not None:
765
+ body["description"] = description
766
+ if is_active is not None:
767
+ body["is_active"] = is_active
768
+
769
+ return await self._client._request(
770
+ endpoint=f"/api/guardian/pages/{page_id}",
771
+ method="PUT",
772
+ body=body
773
+ )
774
+
775
+ async def delete_page(self, page_id: str) -> Dict[str, Any]:
776
+ """Delete a page."""
777
+ return await self._client._request(
778
+ endpoint=f"/api/guardian/pages/{page_id}",
779
+ method="DELETE"
780
+ )
781
+
782
+ # Page-tenant junction
783
+ async def get_page_tenants(self, page_id: str) -> List[Dict[str, Any]]:
784
+ """Get tenants assigned to page."""
785
+ return await self._client._request(
786
+ endpoint=f"/api/guardian/pages/{page_id}/tenants",
787
+ method="GET"
788
+ )
789
+
790
+ async def add_page_tenants(self, page_id: str, tenant_ids: List[str]) -> Dict[str, Any]:
791
+ """Add tenants to page."""
792
+ return await self._client._request(
793
+ endpoint=f"/api/guardian/pages/{page_id}/tenants",
794
+ body={"tenant_ids": tenant_ids}
795
+ )
796
+
797
+ async def remove_page_tenants(self, page_id: str, tenant_ids: List[str]) -> Dict[str, Any]:
798
+ """Remove tenants from page."""
799
+ return await self._client._request(
800
+ endpoint=f"/api/guardian/pages/{page_id}/tenants",
801
+ method="DELETE",
802
+ body={"tenant_ids": tenant_ids}
803
+ )
804
+
805
+ # Page-scope junction
806
+ async def get_page_scopes(self, page_id: str) -> List[Dict[str, Any]]:
807
+ """Get scopes assigned to page."""
808
+ return await self._client._request(
809
+ endpoint=f"/api/guardian/pages/{page_id}/scopes",
810
+ method="GET"
811
+ )
812
+
813
+ async def add_page_scopes(self, page_id: str, scope_ids: List[str]) -> Dict[str, Any]:
814
+ """Add scopes to page."""
815
+ return await self._client._request(
816
+ endpoint=f"/api/guardian/pages/{page_id}/scopes",
817
+ body={"scope_ids": scope_ids}
818
+ )
819
+
820
+ async def remove_page_scopes(self, page_id: str, scope_ids: List[str]) -> Dict[str, Any]:
821
+ """Remove scopes from page."""
822
+ return await self._client._request(
823
+ endpoint=f"/api/guardian/pages/{page_id}/scopes",
824
+ method="DELETE",
825
+ body={"scope_ids": scope_ids}
826
+ )
827
+
828
+ async def set_page_scopes(self, page_id: str, scope_ids: List[str]) -> List[Dict[str, Any]]:
829
+ """Set all scopes for page (replaces existing)."""
830
+ return await self._client._request(
831
+ endpoint=f"/api/guardian/pages/{page_id}/scopes",
832
+ method="PUT",
833
+ body={"scope_ids": scope_ids}
834
+ )
835
+
836
+ # ========================================
837
+ # NAVIGATION
838
+ # ========================================
839
+
840
+ async def create_nav_item(
841
+ self,
842
+ title: str,
843
+ icon: Optional[str] = None,
844
+ page_id: Optional[str] = None,
845
+ parent_id: Optional[str] = None,
846
+ sort_order: int = 0
847
+ ) -> Dict[str, Any]:
848
+ """Create a new navigation item."""
849
+ body = {"title": title, "sort_order": sort_order}
850
+ if icon:
851
+ body["icon"] = icon
852
+ if page_id:
853
+ body["page_id"] = page_id
854
+ if parent_id:
855
+ body["parent_id"] = parent_id
856
+
857
+ return await self._client._request(
858
+ endpoint="/api/guardian/nav-items",
859
+ body=body
860
+ )
861
+
862
+ async def get_nav_item(self, nav_id: str) -> Dict[str, Any]:
863
+ """Get navigation item by ID."""
864
+ return await self._client._request(
865
+ endpoint=f"/api/guardian/nav-items/{nav_id}",
866
+ method="GET"
867
+ )
868
+
869
+ async def list_nav_items(
870
+ self,
871
+ limit: int = 100,
872
+ offset: int = 0
873
+ ) -> Dict[str, Any]:
874
+ """List all navigation items."""
875
+ return await self._client._request(
876
+ endpoint=f"/api/guardian/nav-items?limit={limit}&offset={offset}",
877
+ method="GET"
878
+ )
879
+
880
+ async def update_nav_item(
881
+ self,
882
+ nav_id: str,
883
+ title: Optional[str] = None,
884
+ icon: Optional[str] = None,
885
+ sort_order: Optional[int] = None
886
+ ) -> Dict[str, Any]:
887
+ """Update navigation item."""
888
+ body = {}
889
+ if title is not None:
890
+ body["title"] = title
891
+ if icon is not None:
892
+ body["icon"] = icon
893
+ if sort_order is not None:
894
+ body["sort_order"] = sort_order
895
+
896
+ return await self._client._request(
897
+ endpoint=f"/api/guardian/nav-items/{nav_id}",
898
+ method="PUT",
899
+ body=body
900
+ )
901
+
902
+ async def delete_nav_item(self, nav_id: str) -> Dict[str, Any]:
903
+ """Delete a navigation item."""
904
+ return await self._client._request(
905
+ endpoint=f"/api/guardian/nav-items/{nav_id}",
906
+ method="DELETE"
907
+ )
908
+
909
+ # Nav-tenant junction
910
+ async def get_nav_tenants(self, nav_id: str) -> List[Dict[str, Any]]:
911
+ """Get tenants assigned to nav item."""
912
+ return await self._client._request(
913
+ endpoint=f"/api/guardian/nav-items/{nav_id}/tenants",
914
+ method="GET"
915
+ )
916
+
917
+ async def add_nav_tenants(self, nav_id: str, tenant_ids: List[str]) -> Dict[str, Any]:
918
+ """Add tenants to nav item."""
919
+ return await self._client._request(
920
+ endpoint=f"/api/guardian/nav-items/{nav_id}/tenants",
921
+ body={"tenant_ids": tenant_ids}
922
+ )
923
+
924
+ async def remove_nav_tenants(self, nav_id: str, tenant_ids: List[str]) -> Dict[str, Any]:
925
+ """Remove tenants from nav item."""
926
+ return await self._client._request(
927
+ endpoint=f"/api/guardian/nav-items/{nav_id}/tenants",
928
+ method="DELETE",
929
+ body={"tenant_ids": tenant_ids}
930
+ )
931
+
932
+ # ========================================
933
+ # SECURE TABLES
934
+ # ========================================
935
+
936
+ async def create_secure_table(
937
+ self,
938
+ table_name: str,
939
+ schema_name: str = "public"
940
+ ) -> Dict[str, Any]:
941
+ """Mark a table as requiring access justification."""
942
+ return await self._client._request(
943
+ endpoint="/api/guardian/secure-tables",
944
+ body={"table_name": table_name, "schema_name": schema_name}
945
+ )
946
+
947
+ async def get_secure_table(self, secure_table_id: str) -> Dict[str, Any]:
948
+ """Get secure table by ID."""
949
+ return await self._client._request(
950
+ endpoint=f"/api/guardian/secure-tables/{secure_table_id}",
951
+ method="GET"
952
+ )
953
+
954
+ async def list_secure_tables(
955
+ self,
956
+ limit: int = 100,
957
+ offset: int = 0
958
+ ) -> Dict[str, Any]:
959
+ """List all secure tables."""
960
+ return await self._client._request(
961
+ endpoint=f"/api/guardian/secure-tables?limit={limit}&offset={offset}",
962
+ method="GET"
963
+ )
964
+
965
+ async def delete_secure_table(self, secure_table_id: str) -> Dict[str, Any]:
966
+ """Remove secure table designation."""
967
+ return await self._client._request(
968
+ endpoint=f"/api/guardian/secure-tables/{secure_table_id}",
969
+ method="DELETE"
970
+ )
971
+
972
+ # Secure table scope junction
973
+ async def get_secure_table_scopes(self, secure_table_id: str) -> List[Dict[str, Any]]:
974
+ """Get scopes required to access secure table."""
975
+ return await self._client._request(
976
+ endpoint=f"/api/guardian/secure-tables/{secure_table_id}/scopes",
977
+ method="GET"
978
+ )
979
+
980
+ async def add_secure_table_scopes(
981
+ self,
982
+ secure_table_id: str,
983
+ scope_ids: List[str]
984
+ ) -> Dict[str, Any]:
985
+ """Add scope requirements to secure table."""
986
+ return await self._client._request(
987
+ endpoint=f"/api/guardian/secure-tables/{secure_table_id}/scopes",
988
+ body={"scope_ids": scope_ids}
989
+ )
990
+
991
+ async def remove_secure_table_scopes(
992
+ self,
993
+ secure_table_id: str,
994
+ scope_ids: List[str]
995
+ ) -> Dict[str, Any]:
996
+ """Remove scope requirements from secure table."""
997
+ return await self._client._request(
998
+ endpoint=f"/api/guardian/secure-tables/{secure_table_id}/scopes",
999
+ method="DELETE",
1000
+ body={"scope_ids": scope_ids}
1001
+ )
1002
+
1003
+ # ========================================
1004
+ # JWT OPERATIONS (via Warden)
1005
+ # ========================================
1006
+
1007
+ async def get_jwks(self) -> Dict[str, Any]:
1008
+ """Get JWKS for JWT validation (cached)."""
1009
+ if self._public_key_cache:
1010
+ return self._public_key_cache
1011
+
1012
+ result = await self._client._request(
1013
+ endpoint="/api/warden/jwks",
1014
+ method="GET"
1015
+ )
1016
+ self._public_key_cache = result
1017
+ return result
1018
+
1019
+ async def validate_jwt(self, token: str) -> Dict[str, Any]:
1020
+ """
1021
+ Validate a JWT and return its claims.
1022
+
1023
+ Raises ValueError if token is invalid or expired.
1024
+ """
1025
+ import jwt as pyjwt
1026
+ from jwt import PyJWK
1027
+
1028
+ jwks = await self.get_jwks()
1029
+
1030
+ try:
1031
+ unverified_header = pyjwt.get_unverified_header(token)
1032
+ kid = unverified_header.get("kid")
1033
+
1034
+ key_data = None
1035
+ for key in jwks.get("keys", []):
1036
+ if key.get("kid") == kid or kid is None:
1037
+ key_data = key
1038
+ break
1039
+
1040
+ if not key_data:
1041
+ raise ValueError(f"No matching key found for kid: {kid}")
1042
+
1043
+ jwk = PyJWK.from_dict(key_data)
1044
+ public_key = jwk.key
1045
+
1046
+ claims = pyjwt.decode(
1047
+ token,
1048
+ public_key,
1049
+ algorithms=["RS256"],
1050
+ options={"verify_exp": True}
1051
+ )
1052
+ return claims
1053
+ except pyjwt.ExpiredSignatureError:
1054
+ raise ValueError("Token has expired")
1055
+ except pyjwt.InvalidTokenError as e:
1056
+ raise ValueError(f"Invalid token: {e}")