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/__init__.py +0 -0
- ravmem/cli/analyze.py +484 -0
- ravmem/cli/auth.py +675 -0
- ravmem/cli/browse.py +144 -0
- ravmem/cli/config_cmd.py +69 -0
- ravmem/cli/diff.py +122 -0
- ravmem/cli/git_local.py +131 -0
- ravmem/cli/impact.py +106 -0
- ravmem/cli/info.py +82 -0
- ravmem/cli/kg.py +787 -0
- ravmem/cli/main.py +96 -0
- ravmem/cli/remote.py +549 -0
- ravmem/cli/search.py +91 -0
- ravmem/cli/serve.py +41 -0
- ravmem/client/__init__.py +3 -0
- ravmem/client/api.py +279 -0
- ravmem/client/config.py +52 -0
- ravmem/config.py +103 -0
- ravmem/config_loader.py +247 -0
- ravmem_cli-0.1.0.dist-info/METADATA +62 -0
- ravmem_cli-0.1.0.dist-info/RECORD +25 -0
- ravmem_cli-0.1.0.dist-info/WHEEL +5 -0
- ravmem_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ravmem_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ravmem_cli-0.1.0.dist-info/top_level.txt +1 -0
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)
|