ravmem-cli 0.1.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.
ravmem/cli/auth.py ADDED
@@ -0,0 +1,675 @@
1
+ """ravmem auth — authentication and authorization management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ app = typer.Typer(
10
+ name="auth",
11
+ help="Manage authentication tokens, users, and roles",
12
+ no_args_is_help=True,
13
+ )
14
+
15
+
16
+ def _console():
17
+ from rich.console import Console
18
+ return Console()
19
+
20
+
21
+ def _get_client(token: str | None = None):
22
+ """Build a client, optionally overriding the api_key with *token*."""
23
+ from ravmem.client.config import load_config
24
+ from ravmem.client.api import RavMemClient
25
+
26
+ cfg = load_config()
27
+ if not cfg.get("server_url"):
28
+ typer.echo(
29
+ "Remote server not configured. Run 'ravmem remote config --url <URL>' first.",
30
+ err=True,
31
+ )
32
+ raise typer.Exit(1)
33
+
34
+ api_key = token or cfg.get("api_key") or None
35
+ return RavMemClient(base_url=cfg["server_url"], api_key=api_key)
36
+
37
+
38
+ def _handle_http_error(exc) -> None:
39
+ import httpx
40
+ if not isinstance(exc, httpx.HTTPStatusError):
41
+ typer.echo(f"Error: {exc}", err=True)
42
+ return
43
+ code = exc.response.status_code
44
+ try:
45
+ detail = exc.response.json().get("detail", exc.response.text)
46
+ except Exception:
47
+ detail = exc.response.text
48
+ if code == 401:
49
+ typer.echo("Error: authentication required — check your token.", err=True)
50
+ elif code == 403:
51
+ typer.echo("Error: permission denied.", err=True)
52
+ elif code == 404:
53
+ typer.echo(f"Error: not found — {detail}", err=True)
54
+ elif code == 409:
55
+ typer.echo(f"Error: conflict — {detail}", err=True)
56
+ elif code == 503:
57
+ typer.echo(f"Error: server unavailable — {detail}", err=True)
58
+ else:
59
+ typer.echo(f"Error {code}: {detail}", err=True)
60
+
61
+
62
+ @app.command("login")
63
+ def login(
64
+ username: str = typer.Option(..., "--username", "-u", help="Username"),
65
+ password: str = typer.Option(..., "--password", "-p", help="Password", hide_input=True),
66
+ ):
67
+ """Authenticate with the server and save the API key locally."""
68
+ import httpx
69
+ from ravmem.client.config import load_config, save_config
70
+
71
+ client = _get_client()
72
+ try:
73
+ result = client._request(
74
+ "POST", "/api/auth/login",
75
+ json={"username": username, "password": password}
76
+ )
77
+ except httpx.HTTPStatusError as exc:
78
+ _handle_http_error(exc)
79
+ raise typer.Exit(1)
80
+ finally:
81
+ client.close()
82
+
83
+ token = result["access_token"]
84
+ cfg = load_config()
85
+ cfg["api_key"] = token
86
+ save_config(cfg)
87
+
88
+ console = _console()
89
+ console.print(f"\n[bold green]Logged in as '{username}'.[/bold green]")
90
+ console.print(f"API key saved to local configuration.\n")
91
+
92
+
93
+ @app.command("me")
94
+ def me():
95
+ """Show details of the current authenticated user."""
96
+ import httpx
97
+
98
+ client = _get_client()
99
+ try:
100
+ user = client._request("GET", "/api/auth/me")
101
+ except httpx.HTTPStatusError as exc:
102
+ _handle_http_error(exc)
103
+ raise typer.Exit(1)
104
+ finally:
105
+ client.close()
106
+
107
+ console = _console()
108
+ console.print(f"\n[bold]Current User:[/bold]")
109
+ console.print(f" ID: {user['id']}")
110
+ console.print(f" Email: {user['email']}")
111
+ console.print(f" Name: {user['name']}")
112
+ console.print(f" Role: {user.get('role', '—')}\n")
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Admin token
117
+ # ---------------------------------------------------------------------------
118
+
119
+ @app.command("reset-admin-token")
120
+ def reset_admin_token(
121
+ token: Optional[str] = typer.Option(
122
+ None,
123
+ "--token", "-t",
124
+ help="Current admin token (reads RAVMEM_ADMIN_TOKEN env var if omitted)",
125
+ envvar="RAVMEM_ADMIN_TOKEN",
126
+ ),
127
+ ):
128
+ """Rotate the admin token. Requires the current admin token.
129
+
130
+ The old token is invalidated immediately. The new token is printed once.
131
+ """
132
+ import httpx
133
+
134
+ if not token:
135
+ typer.echo(
136
+ "Current admin token required. Pass --token or set RAVMEM_ADMIN_TOKEN.",
137
+ err=True,
138
+ )
139
+ raise typer.Exit(1)
140
+
141
+ client = _get_client(token=token)
142
+ try:
143
+ result = client._request("POST", "/api/auth/admin/reset-token")
144
+ except httpx.HTTPStatusError as exc:
145
+ _handle_http_error(exc)
146
+ raise typer.Exit(1)
147
+ finally:
148
+ client.close()
149
+
150
+ console = _console()
151
+ console.print("\n[bold green]Admin token rotated successfully.[/bold green]")
152
+ console.print(f"\n[bold]New token:[/bold] {result['token']}\n")
153
+ console.print("[yellow]Save this token — it will not be shown again.[/yellow]")
154
+ console.print("Update RAVMEM_ADMIN_TOKEN and restart, or pass --token on each CLI call.\n")
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Users
159
+ # ---------------------------------------------------------------------------
160
+
161
+ @app.command("create-user")
162
+ def create_user(
163
+ email: str = typer.Option(..., "--email", "-e", help="User email address"),
164
+ name: str = typer.Option(..., "--name", "-n", help="User display name"),
165
+ ):
166
+ """Create a new user (admin only)."""
167
+ import httpx
168
+
169
+ client = _get_client()
170
+ try:
171
+ user = client._request("POST", "/api/auth/users", json={"email": email, "name": name})
172
+ except httpx.HTTPStatusError as exc:
173
+ _handle_http_error(exc)
174
+ raise typer.Exit(1)
175
+ finally:
176
+ client.close()
177
+
178
+ console = _console()
179
+ console.print(f"\n[bold green]User created.[/bold green]")
180
+ console.print(f" ID: {user['id']}")
181
+ console.print(f" Email: {user['email']}")
182
+ console.print(f" Name: {user['name']}\n")
183
+
184
+
185
+ @app.command("list-users")
186
+ def list_users(
187
+ output_json: bool = typer.Option(False, "--json", help="Output raw JSON"),
188
+ ):
189
+ """List all users (admin only)."""
190
+ import httpx
191
+ import json
192
+ from rich.table import Table
193
+
194
+ client = _get_client()
195
+ try:
196
+ users = client._request("GET", "/api/auth/users")
197
+ except httpx.HTTPStatusError as exc:
198
+ _handle_http_error(exc)
199
+ raise typer.Exit(1)
200
+ finally:
201
+ client.close()
202
+
203
+ if output_json:
204
+ typer.echo(json.dumps(users, indent=2, default=str))
205
+ return
206
+
207
+ console = _console()
208
+ if not users:
209
+ console.print("No users found.")
210
+ return
211
+
212
+ table = Table(show_header=True, header_style="bold")
213
+ table.add_column("ID")
214
+ table.add_column("Email")
215
+ table.add_column("Name")
216
+ table.add_column("Status")
217
+ table.add_column("Created")
218
+ for u in users:
219
+ status_style = "green" if u.get("status") == "active" else "red"
220
+ table.add_row(
221
+ u.get("id", ""),
222
+ u.get("email", ""),
223
+ u.get("name", ""),
224
+ f"[{status_style}]{u.get('status', '')}[/{status_style}]",
225
+ u.get("created_at", "")[:10],
226
+ )
227
+ console.print(table)
228
+
229
+
230
+ @app.command("delete-user")
231
+ def delete_user(
232
+ user_id: str = typer.Argument(..., help="User ID to delete"),
233
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
234
+ ):
235
+ """Delete a user and all their tokens/roles (admin only)."""
236
+ import httpx
237
+
238
+ if not yes:
239
+ typer.confirm(f"Are you sure you want to delete user '{user_id}'? This cannot be undone.", abort=True)
240
+
241
+ client = _get_client()
242
+ try:
243
+ client._request("DELETE", f"/api/auth/users/{user_id}")
244
+ except httpx.HTTPStatusError as exc:
245
+ _handle_http_error(exc)
246
+ raise typer.Exit(1)
247
+ finally:
248
+ client.close()
249
+
250
+ _console().print(f"[green]User '{user_id}' deleted.[/green]")
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # Tokens
255
+ # ---------------------------------------------------------------------------
256
+
257
+ @app.command("create-token")
258
+ def create_token(
259
+ user: str = typer.Option(..., "--user", "-u", help="User ID to create token for"),
260
+ name: str = typer.Option(..., "--name", "-n", help="Token name / description"),
261
+ expires_in: Optional[str] = typer.Option(
262
+ None, "--expires-in", help="Expiry: e.g. 30d, 24h, 60m. Omit for never-expiring."
263
+ ),
264
+ ):
265
+ """Create an API token. Admin can create for any user; users for themselves only."""
266
+ import httpx
267
+
268
+ client = _get_client()
269
+ body: dict = {"user_id": user, "name": name}
270
+ if expires_in:
271
+ body["expires_in"] = expires_in
272
+
273
+ try:
274
+ result = client._request("POST", "/api/auth/tokens", json=body)
275
+ except httpx.HTTPStatusError as exc:
276
+ _handle_http_error(exc)
277
+ raise typer.Exit(1)
278
+ finally:
279
+ client.close()
280
+
281
+ console = _console()
282
+ console.print(f"\n[bold green]Token created.[/bold green]")
283
+ console.print(f" ID: {result['id']}")
284
+ console.print(f" Name: {result['name']}")
285
+ console.print(f" Expires: {result.get('expires_at') or 'never'}")
286
+ console.print(f"\n[bold]Token:[/bold] {result['token']}\n")
287
+ console.print("[yellow]Save this token — it will not be shown again.[/yellow]\n")
288
+
289
+
290
+ @app.command("revoke-token")
291
+ def revoke_token(
292
+ token_id: Optional[str] = typer.Option(None, "--token-id", help="Token ID to revoke"),
293
+ user: Optional[str] = typer.Option(None, "--user", help="User ID (use with --all)"),
294
+ all_tokens: bool = typer.Option(False, "--all", help="Revoke all tokens for --user"),
295
+ ):
296
+ """Revoke a token by ID, or all tokens for a user (admin only)."""
297
+ import httpx
298
+
299
+ if all_tokens and not user:
300
+ typer.echo("Error: --all requires --user", err=True)
301
+ raise typer.Exit(1)
302
+ if not all_tokens and not token_id:
303
+ typer.echo("Error: provide --token-id, or --user with --all", err=True)
304
+ raise typer.Exit(1)
305
+
306
+ client = _get_client()
307
+ try:
308
+ if all_tokens:
309
+ result = client._request(
310
+ "DELETE", "/api/auth/tokens/_all",
311
+ params={"user": user},
312
+ )
313
+ _console().print(f"[green]Revoked {result.get('revoked', 0)} token(s) for user {user}.[/green]")
314
+ else:
315
+ client._request("DELETE", f"/api/auth/tokens/{token_id}")
316
+ _console().print(f"[green]Token {token_id} revoked.[/green]")
317
+ except httpx.HTTPStatusError as exc:
318
+ _handle_http_error(exc)
319
+ raise typer.Exit(1)
320
+ finally:
321
+ client.close()
322
+
323
+
324
+ @app.command("list-tokens")
325
+ def list_tokens(
326
+ user: Optional[str] = typer.Option(None, "--user", "-u", help="Filter by user ID (admin only for other users)"),
327
+ output_json: bool = typer.Option(False, "--json", help="Output raw JSON"),
328
+ ):
329
+ """List API tokens. Admin sees all; users see only their own."""
330
+ import httpx
331
+ import json
332
+ from rich.table import Table
333
+
334
+ client = _get_client()
335
+ params = {}
336
+ if user:
337
+ params["user_id"] = user
338
+
339
+ try:
340
+ tokens = client._request("GET", "/api/auth/tokens", params=params if params else None)
341
+ except httpx.HTTPStatusError as exc:
342
+ _handle_http_error(exc)
343
+ raise typer.Exit(1)
344
+ finally:
345
+ client.close()
346
+
347
+ if output_json:
348
+ typer.echo(json.dumps(tokens, indent=2, default=str))
349
+ return
350
+
351
+ console = _console()
352
+ if not tokens:
353
+ console.print("No tokens found.")
354
+ return
355
+
356
+ table = Table(show_header=True, header_style="bold")
357
+ table.add_column("ID")
358
+ table.add_column("User")
359
+ table.add_column("Name")
360
+ table.add_column("Status")
361
+ table.add_column("Expires")
362
+ table.add_column("Last Used")
363
+ table.add_column("Created")
364
+ for t in tokens:
365
+ status_style = "green" if t.get("status") == "active" else "red"
366
+ table.add_row(
367
+ t.get("id", ""),
368
+ t.get("user_id", ""),
369
+ t.get("name", ""),
370
+ f"[{status_style}]{t.get('status', '')}[/{status_style}]",
371
+ t.get("expires_at") or "never",
372
+ t.get("last_used_at") or "—",
373
+ t.get("created_at", "")[:10],
374
+ )
375
+ console.print(table)
376
+
377
+
378
+ # ---------------------------------------------------------------------------
379
+ # Roles (Phase 3)
380
+ # ---------------------------------------------------------------------------
381
+
382
+ @app.command("create-role")
383
+ def create_role(
384
+ name: str = typer.Option(..., "--name", "-n", help="Role name"),
385
+ ):
386
+ """Create a new role with no permissions (admin only)."""
387
+ import httpx
388
+ client = _get_client()
389
+ try:
390
+ role = client._request("POST", "/api/auth/roles", json={"name": name})
391
+ except httpx.HTTPStatusError as exc:
392
+ _handle_http_error(exc)
393
+ raise typer.Exit(1)
394
+ finally:
395
+ client.close()
396
+ _console().print(f"[green]Role '{role['name']}' created (id: {role['id']}).[/green]")
397
+
398
+
399
+ @app.command("delete-role")
400
+ def delete_role(
401
+ name: str = typer.Option(..., "--name", "-n", help="Role name"),
402
+ ):
403
+ """Delete a role. Blocked if users are assigned to it (admin only)."""
404
+ import httpx
405
+ client = _get_client()
406
+ try:
407
+ client._request("DELETE", f"/api/auth/roles/{name}")
408
+ except httpx.HTTPStatusError as exc:
409
+ _handle_http_error(exc)
410
+ raise typer.Exit(1)
411
+ finally:
412
+ client.close()
413
+ _console().print(f"[green]Role '{name}' deleted.[/green]")
414
+
415
+
416
+ @app.command("add-permission")
417
+ def add_permission(
418
+ role: str = typer.Option(..., "--role", "-r", help="Role name"),
419
+ permission: str = typer.Option(..., "--permission", "-p", help="Permission string (e.g. branch:read)"),
420
+ ):
421
+ """Add a permission to a role (admin only)."""
422
+ import httpx
423
+ client = _get_client()
424
+ try:
425
+ result = client._request("POST", f"/api/auth/roles/{role}/permissions", json={"permission": permission})
426
+ except httpx.HTTPStatusError as exc:
427
+ _handle_http_error(exc)
428
+ raise typer.Exit(1)
429
+ finally:
430
+ client.close()
431
+ _console().print(f"[green]Permission '{permission}' added to role '{role}'.[/green]")
432
+ _console().print(f" Current permissions: {result.get('permissions', [])}")
433
+
434
+
435
+ @app.command("remove-permission")
436
+ def remove_permission(
437
+ role: str = typer.Option(..., "--role", "-r", help="Role name"),
438
+ permission: str = typer.Option(..., "--permission", "-p", help="Permission string to remove"),
439
+ ):
440
+ """Remove a permission from a role (admin only)."""
441
+ import httpx
442
+ client = _get_client()
443
+ try:
444
+ result = client._request("DELETE", f"/api/auth/roles/{role}/permissions/{permission}")
445
+ except httpx.HTTPStatusError as exc:
446
+ _handle_http_error(exc)
447
+ raise typer.Exit(1)
448
+ finally:
449
+ client.close()
450
+ _console().print(f"[green]Permission '{permission}' removed from role '{role}'.[/green]")
451
+
452
+
453
+ @app.command("list-roles")
454
+ def list_roles(
455
+ output_json: bool = typer.Option(False, "--json", help="Output raw JSON"),
456
+ ):
457
+ """List all roles and their permissions (admin only)."""
458
+ import httpx
459
+ import json
460
+ from rich.table import Table
461
+
462
+ client = _get_client()
463
+ try:
464
+ roles = client._request("GET", "/api/auth/roles")
465
+ except httpx.HTTPStatusError as exc:
466
+ _handle_http_error(exc)
467
+ raise typer.Exit(1)
468
+ finally:
469
+ client.close()
470
+
471
+ if output_json:
472
+ typer.echo(json.dumps(roles, indent=2, default=str))
473
+ return
474
+
475
+ console = _console()
476
+ if not roles:
477
+ console.print("No roles found.")
478
+ return
479
+
480
+ table = Table(show_header=True, header_style="bold")
481
+ table.add_column("ID")
482
+ table.add_column("Name")
483
+ table.add_column("Permissions")
484
+ for r in roles:
485
+ perms = ", ".join(r.get("permissions", [])) or "—"
486
+ table.add_row(r.get("id", ""), r.get("name", ""), perms)
487
+ console.print(table)
488
+
489
+
490
+ @app.command("show-role")
491
+ def show_role(
492
+ name: str = typer.Option(..., "--name", "-n", help="Role name"),
493
+ output_json: bool = typer.Option(False, "--json", help="Output raw JSON"),
494
+ ):
495
+ """Show permissions for a single role (admin only)."""
496
+ import httpx
497
+ import json
498
+ client = _get_client()
499
+ try:
500
+ role = client._request("GET", f"/api/auth/roles/{name}")
501
+ except httpx.HTTPStatusError as exc:
502
+ _handle_http_error(exc)
503
+ raise typer.Exit(1)
504
+ finally:
505
+ client.close()
506
+
507
+ if output_json:
508
+ typer.echo(json.dumps(role, indent=2, default=str))
509
+ return
510
+
511
+ console = _console()
512
+ console.print(f"\n[bold]Role:[/bold] {role['name']} ({role['id']})")
513
+ perms = role.get("permissions", [])
514
+ if perms:
515
+ for p in perms:
516
+ console.print(f" • {p}")
517
+ else:
518
+ console.print(" (no permissions)")
519
+ console.print()
520
+
521
+
522
+ @app.command("assign-role")
523
+ def assign_role(
524
+ user: str = typer.Option(..., "--user", "-u", help="User ID"),
525
+ role: str = typer.Option(..., "--role", "-r", help="Role name"),
526
+ ):
527
+ """Assign a role to a user (admin only)."""
528
+ import httpx
529
+ client = _get_client()
530
+ try:
531
+ client._request("POST", "/api/auth/roles/assign", json={"user_id": user, "role": role})
532
+ except httpx.HTTPStatusError as exc:
533
+ _handle_http_error(exc)
534
+ raise typer.Exit(1)
535
+ finally:
536
+ client.close()
537
+ _console().print(f"[green]Role '{role}' assigned to user {user}.[/green]")
538
+
539
+
540
+ @app.command("unassign-role")
541
+ def unassign_role(
542
+ user: str = typer.Option(..., "--user", "-u", help="User ID"),
543
+ role: str = typer.Option(..., "--role", "-r", help="Role name"),
544
+ ):
545
+ """Unassign a role from a user (admin only)."""
546
+ import httpx
547
+ client = _get_client()
548
+ try:
549
+ client._request("DELETE", "/api/auth/roles/assign", json={"user_id": user, "role": role})
550
+ except httpx.HTTPStatusError as exc:
551
+ _handle_http_error(exc)
552
+ raise typer.Exit(1)
553
+ finally:
554
+ client.close()
555
+ _console().print(f"[green]Role '{role}' unassigned from user {user}.[/green]")
556
+
557
+
558
+ # ---------------------------------------------------------------------------
559
+ # Rate Limit Tiers (Phase 3)
560
+ # ---------------------------------------------------------------------------
561
+
562
+ @app.command("set-tier")
563
+ def set_tier(
564
+ role: str = typer.Option(..., "--role", "-r", help="Role name"),
565
+ tier: str = typer.Option(..., "--tier", "-t", help="Tier: high | standard | restricted"),
566
+ ):
567
+ """Set the rate-limit tier for a role (admin only).
568
+
569
+ Valid tiers: high, standard, restricted.
570
+ The 'admin' tier is reserved and cannot be assigned to roles.
571
+
572
+ Example:
573
+ ravmem auth set-tier --role ci-agents --tier high
574
+ """
575
+ import httpx
576
+ client = _get_client()
577
+ try:
578
+ result = client._request("PUT", f"/api/auth/roles/{role}/tier", json={"tier": tier})
579
+ except httpx.HTTPStatusError as exc:
580
+ _handle_http_error(exc)
581
+ raise typer.Exit(1)
582
+ finally:
583
+ client.close()
584
+ _console().print(f"[green]Rate-limit tier for role '{role}' set to '{tier}'.[/green]")
585
+ if result.get("rate_limit_tier"):
586
+ _console().print(f" Confirmed tier: {result['rate_limit_tier']}")
587
+
588
+
589
+ # ---------------------------------------------------------------------------
590
+ # Branch Policies (Phase 4)
591
+ # ---------------------------------------------------------------------------
592
+
593
+ @app.command("add-branch-policy")
594
+ def add_branch_policy(
595
+ role: str = typer.Option(..., "--role", "-r", help="Role name"),
596
+ permission: str = typer.Option(..., "--permission", "-p", help="Permission (e.g. branch:create)"),
597
+ pattern: str = typer.Option(..., "--pattern", help="Python regex (e.g. 'feat/.*')"),
598
+ ):
599
+ """Add a branch name regex policy to a role (admin only).
600
+
601
+ Pattern is validated immediately. Invalid patterns are rejected.
602
+
603
+ Example:
604
+ ravmem auth add-branch-policy --role developer --permission branch:create --pattern "feat/.*"
605
+ """
606
+ import httpx
607
+ client = _get_client()
608
+ try:
609
+ result = client._request(
610
+ "POST", "/api/auth/branch-policies",
611
+ json={"role": role, "permission": permission, "pattern": pattern},
612
+ )
613
+ except httpx.HTTPStatusError as exc:
614
+ _handle_http_error(exc)
615
+ raise typer.Exit(1)
616
+ finally:
617
+ client.close()
618
+ _console().print(f"[green]Branch policy added:[/green] role={role}, permission={permission}, pattern={pattern!r}")
619
+
620
+
621
+ @app.command("remove-branch-policy")
622
+ def remove_branch_policy(
623
+ role: str = typer.Option(..., "--role", "-r", help="Role name"),
624
+ permission: str = typer.Option(..., "--permission", "-p", help="Permission"),
625
+ pattern: str = typer.Option(..., "--pattern", help="Exact pattern to remove"),
626
+ ):
627
+ """Remove a branch policy from a role (admin only)."""
628
+ import httpx
629
+ client = _get_client()
630
+ try:
631
+ client._request(
632
+ "DELETE", "/api/auth/branch-policies",
633
+ json={"role": role, "permission": permission, "pattern": pattern},
634
+ )
635
+ except httpx.HTTPStatusError as exc:
636
+ _handle_http_error(exc)
637
+ raise typer.Exit(1)
638
+ finally:
639
+ client.close()
640
+ _console().print(f"[green]Branch policy removed:[/green] role={role}, permission={permission}, pattern={pattern!r}")
641
+
642
+
643
+ @app.command("list-branch-policies")
644
+ def list_branch_policies(
645
+ role: str = typer.Option(..., "--role", "-r", help="Role name"),
646
+ ):
647
+ """List all branch policies for a role (admin only)."""
648
+ import httpx
649
+ from rich.table import Table
650
+
651
+ client = _get_client()
652
+ try:
653
+ policies = client._request("GET", "/api/auth/branch-policies", params={"role": role})
654
+ except httpx.HTTPStatusError as exc:
655
+ _handle_http_error(exc)
656
+ raise typer.Exit(1)
657
+ finally:
658
+ client.close()
659
+
660
+ console = _console()
661
+ if not policies:
662
+ console.print(f"No branch policies found for role '{role}'.")
663
+ return
664
+
665
+ table = Table(show_header=True, header_style="bold")
666
+ table.add_column("ID")
667
+ table.add_column("Permission")
668
+ table.add_column("Pattern")
669
+ for pol in policies:
670
+ table.add_row(
671
+ pol.get("id", ""),
672
+ pol.get("permission", ""),
673
+ pol.get("branch_pattern", ""),
674
+ )
675
+ console.print(table)