clickhouse-users-cli 0.1.1__tar.gz → 0.1.3__tar.gz
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.
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/PKG-INFO +10 -1
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/README.md +8 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli/__init__.py +1 -1
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli/app.py +159 -15
- clickhouse_users_cli-0.1.3/clickhouse_users_cli/session.py +80 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli/sql_builder.py +13 -0
- clickhouse_users_cli-0.1.3/clickhouse_users_cli/style.py +114 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli.egg-info/PKG-INFO +10 -1
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli.egg-info/SOURCES.txt +1 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli.egg-info/requires.txt +1 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/pyproject.toml +1 -0
- clickhouse_users_cli-0.1.1/clickhouse_users_cli/style.py +0 -55
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/LICENSE +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli/db.py +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli/validators.py +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli.egg-info/dependency_links.txt +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli.egg-info/entry_points.txt +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli.egg-info/top_level.txt +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clickhouse-users-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: CLI interativa para criar usuarios ClickHouse com privilegio minimo (least privilege)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Keywords: clickhouse,cli,users,access-control,least-privilege
|
|
@@ -19,6 +19,7 @@ License-File: LICENSE
|
|
|
19
19
|
Requires-Dist: questionary>=2.0.0
|
|
20
20
|
Requires-Dist: clickhouse-connect>=0.8.0
|
|
21
21
|
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
23
|
Dynamic: license-file
|
|
23
24
|
|
|
24
25
|
# ClickHouse Users CLI
|
|
@@ -47,6 +48,13 @@ Para listar/gerenciar também precisa de `SHOW USERS` + `ALTER USER` / `DROP USE
|
|
|
47
48
|
Após conectar, escolha: **Criar usuário** · **Listar usuários** · **Gerenciar usuário** · **Sair**.
|
|
48
49
|
O menu fica em loop — dá para criar, auditar e desativar vários sem reconectar.
|
|
49
50
|
|
|
51
|
+
## Sessão salva (manter conectado)
|
|
52
|
+
|
|
53
|
+
- Na primeira conexão o CLI pergunta se quer **manter conectado**: salva `~/.ch-users/connection.yaml` com host, porta, protocolo, usuário e senha.
|
|
54
|
+
- Na próxima execução, oferece **usar a salva / nova conexão / apagar a salva**.
|
|
55
|
+
- No menu há **Esquecer sessão** (apaga o YAML, com confirmação).
|
|
56
|
+
- Segurança: a senha fica em **texto puro** no arquivo (permissão `0600` no Linux/mac; no Windows vale o aviso em tela). Só use em máquina confiável — nunca compartilhe o arquivo.
|
|
57
|
+
|
|
50
58
|
## Fluxo — criar (6 etapas)
|
|
51
59
|
|
|
52
60
|
1. **Conexão** — host, protocolo, porta, usuário/senha admin. Testa com `SELECT 1` e permite tentar de novo / editar / sair.
|
|
@@ -68,6 +76,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
|
|
|
68
76
|
|
|
69
77
|
- **Listar** — `SHOW USERS` em tabela + opção de ver `SHOW CREATE USER` e `SHOW GRANTS FOR` de um usuário.
|
|
70
78
|
- **Gerenciar** — escolhe 1 usuário, vê status (`HOST NONE` = inativo) + grants, depois:
|
|
79
|
+
- **Editar permissões** → pergunta perfil (tipo de acesso) + bancos/tabelas e aplica `REVOKE ALL ON *.*` + novos `GRANTs` (com preview e confirmação);
|
|
71
80
|
- **Desativar** → `ALTER USER \`nome\` HOST NONE` (reversível, bloqueia login);
|
|
72
81
|
- **Reativar** → `ALTER USER \`nome\` HOST ANY`;
|
|
73
82
|
- **Excluir** → `DROP USER IF EXISTS \`nome\`` (definitivo).
|
|
@@ -24,6 +24,13 @@ Para listar/gerenciar também precisa de `SHOW USERS` + `ALTER USER` / `DROP USE
|
|
|
24
24
|
Após conectar, escolha: **Criar usuário** · **Listar usuários** · **Gerenciar usuário** · **Sair**.
|
|
25
25
|
O menu fica em loop — dá para criar, auditar e desativar vários sem reconectar.
|
|
26
26
|
|
|
27
|
+
## Sessão salva (manter conectado)
|
|
28
|
+
|
|
29
|
+
- Na primeira conexão o CLI pergunta se quer **manter conectado**: salva `~/.ch-users/connection.yaml` com host, porta, protocolo, usuário e senha.
|
|
30
|
+
- Na próxima execução, oferece **usar a salva / nova conexão / apagar a salva**.
|
|
31
|
+
- No menu há **Esquecer sessão** (apaga o YAML, com confirmação).
|
|
32
|
+
- Segurança: a senha fica em **texto puro** no arquivo (permissão `0600` no Linux/mac; no Windows vale o aviso em tela). Só use em máquina confiável — nunca compartilhe o arquivo.
|
|
33
|
+
|
|
27
34
|
## Fluxo — criar (6 etapas)
|
|
28
35
|
|
|
29
36
|
1. **Conexão** — host, protocolo, porta, usuário/senha admin. Testa com `SELECT 1` e permite tentar de novo / editar / sair.
|
|
@@ -45,6 +52,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
|
|
|
45
52
|
|
|
46
53
|
- **Listar** — `SHOW USERS` em tabela + opção de ver `SHOW CREATE USER` e `SHOW GRANTS FOR` de um usuário.
|
|
47
54
|
- **Gerenciar** — escolhe 1 usuário, vê status (`HOST NONE` = inativo) + grants, depois:
|
|
55
|
+
- **Editar permissões** → pergunta perfil (tipo de acesso) + bancos/tabelas e aplica `REVOKE ALL ON *.*` + novos `GRANTs` (com preview e confirmação);
|
|
48
56
|
- **Desativar** → `ALTER USER \`nome\` HOST NONE` (reversível, bloqueia login);
|
|
49
57
|
- **Reativar** → `ALTER USER \`nome\` HOST ANY`;
|
|
50
58
|
- **Excluir** → `DROP USER IF EXISTS \`nome\`` (definitivo).
|
|
@@ -6,7 +6,6 @@ import sys
|
|
|
6
6
|
|
|
7
7
|
import questionary
|
|
8
8
|
from rich.syntax import Syntax
|
|
9
|
-
from rich.table import Table
|
|
10
9
|
|
|
11
10
|
from clickhouse_users_cli.db import SYSTEM_DATABASES, ClickHouseAdmin, ConnectionInfo
|
|
12
11
|
from clickhouse_users_cli.sql_builder import (
|
|
@@ -18,8 +17,10 @@ from clickhouse_users_cli.sql_builder import (
|
|
|
18
17
|
build_all_statements,
|
|
19
18
|
build_deactivate_user_sql,
|
|
20
19
|
build_drop_user_sql,
|
|
20
|
+
build_edit_grants_statements,
|
|
21
21
|
)
|
|
22
|
-
from clickhouse_users_cli.style import APP_STYLE, banner, console, error, info, step, success
|
|
22
|
+
from clickhouse_users_cli.style import APP_STYLE, banner, console, error, info, make_table, step, success, warn
|
|
23
|
+
from clickhouse_users_cli.session import SESSION_FILE
|
|
23
24
|
from clickhouse_users_cli.validators import (
|
|
24
25
|
validate_at_least_one,
|
|
25
26
|
validate_host,
|
|
@@ -288,7 +289,7 @@ def ask_create_options() -> bool:
|
|
|
288
289
|
|
|
289
290
|
def show_review(spec: UserSpec) -> None:
|
|
290
291
|
step("6/6 · Revisão", "Confira o resumo e o SQL exato que será executado.")
|
|
291
|
-
table =
|
|
292
|
+
table = make_table("Resumo do novo usuário")
|
|
292
293
|
table.add_column("Campo", style="dim")
|
|
293
294
|
table.add_column("Valor", style="bold")
|
|
294
295
|
table.add_row("Usuário", spec.username)
|
|
@@ -326,18 +327,107 @@ def confirm_and_execute(admin: ClickHouseAdmin, spec: UserSpec) -> None:
|
|
|
326
327
|
|
|
327
328
|
|
|
328
329
|
|
|
330
|
+
# ── Sessão salva (YAML local, opt-in) ────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
def maybe_load_saved_session() -> ConnectionInfo | None:
|
|
333
|
+
"""Se há YAML salvo, oferece usar / nova conexão / apagar. None = digitar."""
|
|
334
|
+
from clickhouse_users_cli.session import delete_session, has_saved_session, load_session
|
|
335
|
+
|
|
336
|
+
if not has_saved_session():
|
|
337
|
+
return None
|
|
338
|
+
step("Sessão salva", f"Arquivo: {SESSION_FILE} (senha em texto puro — nunca compartilhe)")
|
|
339
|
+
try:
|
|
340
|
+
saved = load_session()
|
|
341
|
+
except RuntimeError as e:
|
|
342
|
+
error(str(e))
|
|
343
|
+
fix = _ask_or_abort(
|
|
344
|
+
Q.select(
|
|
345
|
+
"A sessão salva está inválida. O que fazer?",
|
|
346
|
+
choices=[
|
|
347
|
+
Q.Choice("Apagar e digitar nova conexão", value="delete"),
|
|
348
|
+
Q.Choice("Digitar nova conexão (manter arquivo)", value="new"),
|
|
349
|
+
],
|
|
350
|
+
style=APP_STYLE, qmark="›",
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
if fix == "delete":
|
|
354
|
+
delete_session()
|
|
355
|
+
success("Sessão apagada.")
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
info(f"Conexão guardada: {saved.protocol}://{saved.host}:{saved.port} · usuário '{saved.username}'.")
|
|
359
|
+
choice = _ask_or_abort(
|
|
360
|
+
Q.select(
|
|
361
|
+
"Usar a conexão salva?",
|
|
362
|
+
choices=[
|
|
363
|
+
Q.Choice("Usar conexão salva", value="use"),
|
|
364
|
+
Q.Choice("Nova conexão (digitar de novo)", value="new"),
|
|
365
|
+
Q.Choice("Apagar conexão salva", value="delete"),
|
|
366
|
+
],
|
|
367
|
+
default="use",
|
|
368
|
+
style=APP_STYLE, qmark="›",
|
|
369
|
+
instruction="(↑↓ navega · enter confirma)",
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
if choice == "delete":
|
|
373
|
+
delete_session()
|
|
374
|
+
success("Sessão apagada.")
|
|
375
|
+
return None
|
|
376
|
+
if choice == "new":
|
|
377
|
+
return None
|
|
378
|
+
return saved
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def ask_remember(conn: ConnectionInfo) -> None:
|
|
382
|
+
"""Opt-in: salva a conexão em YAML local (senha em texto puro)."""
|
|
383
|
+
from clickhouse_users_cli.session import save_session
|
|
384
|
+
|
|
385
|
+
warn("A senha será gravada em texto puro no arquivo — só use em máquina confiável.")
|
|
386
|
+
keep = _ask_or_abort(
|
|
387
|
+
Q.confirm("Manter conectado neste computador (salvar sessão)?", default=False, style=APP_STYLE, qmark="›")
|
|
388
|
+
)
|
|
389
|
+
if not keep:
|
|
390
|
+
return
|
|
391
|
+
try:
|
|
392
|
+
path = save_session(conn)
|
|
393
|
+
except RuntimeError as e:
|
|
394
|
+
error(str(e))
|
|
395
|
+
return
|
|
396
|
+
success(f"Sessão salva em {path}")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def flow_forget_session() -> None:
|
|
400
|
+
from clickhouse_users_cli.session import delete_session, has_saved_session
|
|
401
|
+
|
|
402
|
+
if not has_saved_session():
|
|
403
|
+
info("Nenhuma sessão salva.")
|
|
404
|
+
return
|
|
405
|
+
step("Esquecer sessão", f"Arquivo: {SESSION_FILE}")
|
|
406
|
+
go = _ask_or_abort(
|
|
407
|
+
Q.confirm("Apagar a sessão salva (vai pedir credenciais na próxima vez)?", default=False, style=APP_STYLE, qmark="›")
|
|
408
|
+
)
|
|
409
|
+
if not go:
|
|
410
|
+
console.print("[dim]Nada foi apagado.[/]")
|
|
411
|
+
return
|
|
412
|
+
delete_session()
|
|
413
|
+
success("Sessão apagada.")
|
|
414
|
+
|
|
415
|
+
|
|
329
416
|
# ── Menu principal + listar/gerenciar ────────────────────────────────────────────
|
|
330
417
|
|
|
331
|
-
def ask_main_action() -> str:
|
|
418
|
+
def ask_main_action(show_forget: bool = False) -> str:
|
|
419
|
+
choices = [
|
|
420
|
+
Q.Choice("Criar usuário — com privilégio mínimo", value="create"),
|
|
421
|
+
Q.Choice("Listar usuários — SHOW USERS + grants", value="list"),
|
|
422
|
+
Q.Choice("Gerenciar usuário — acessos, desativar / reativar / excluir", value="manage"),
|
|
423
|
+
]
|
|
424
|
+
if show_forget:
|
|
425
|
+
choices.append(Q.Choice("Esquecer sessão — apagar YAML salvo", value="forget"))
|
|
426
|
+
choices.append(Q.Choice("Sair", value="exit"))
|
|
332
427
|
return _ask_or_abort(
|
|
333
428
|
Q.select(
|
|
334
429
|
"O que deseja fazer?",
|
|
335
|
-
choices=
|
|
336
|
-
Q.Choice("Criar usuário — com privilégio mínimo", value="create"),
|
|
337
|
-
Q.Choice("Listar usuários — SHOW USERS + grants", value="list"),
|
|
338
|
-
Q.Choice("Gerenciar usuário — desativar / reativar / excluir", value="manage"),
|
|
339
|
-
Q.Choice("Sair", value="exit"),
|
|
340
|
-
],
|
|
430
|
+
choices=choices,
|
|
341
431
|
default="create",
|
|
342
432
|
style=APP_STYLE, qmark="›",
|
|
343
433
|
instruction="(↑↓ navega · enter confirma)",
|
|
@@ -362,7 +452,7 @@ def show_user_details(admin: ClickHouseAdmin, username: str) -> tuple[str, list[
|
|
|
362
452
|
error(f"Falha ao ver grants de '{username}': {e}")
|
|
363
453
|
grants = []
|
|
364
454
|
|
|
365
|
-
table =
|
|
455
|
+
table = make_table(f"Usuário {username}")
|
|
366
456
|
table.add_column("Campo", style="dim")
|
|
367
457
|
table.add_column("Valor", style="bold")
|
|
368
458
|
status = "inativo (HOST NONE)" if _is_inactive(create_sql) else "ativo"
|
|
@@ -392,7 +482,7 @@ def flow_list_users(admin: ClickHouseAdmin) -> None:
|
|
|
392
482
|
info("Nenhum usuário encontrado.")
|
|
393
483
|
return
|
|
394
484
|
|
|
395
|
-
table =
|
|
485
|
+
table = make_table(f"{len(users)} usuário(s)")
|
|
396
486
|
table.add_column("#", style="dim")
|
|
397
487
|
table.add_column("Usuário", style="bold")
|
|
398
488
|
for i, u in enumerate(users, 1):
|
|
@@ -415,6 +505,49 @@ def flow_list_users(admin: ClickHouseAdmin) -> None:
|
|
|
415
505
|
show_user_details(admin, target)
|
|
416
506
|
|
|
417
507
|
|
|
508
|
+
def flow_edit_grants(admin: ClickHouseAdmin, username: str) -> None:
|
|
509
|
+
"""Redefine os acessos: REVOKE ALL + GRANTs novos (perfil + escopo + hosts intactos)."""
|
|
510
|
+
step("Usuários · Editar permissões", "Os acessos atuais serão zerados e reaplicados. Nada muda sem confirmação.")
|
|
511
|
+
if username == admin.conn.username:
|
|
512
|
+
warn(f"'{username}' é o usuário conectado agora. Zerar os próprios acessos pode te derrubar.")
|
|
513
|
+
|
|
514
|
+
profile = ask_profile()
|
|
515
|
+
privileges, grant_option = ask_privileges(profile)
|
|
516
|
+
if profile == "admin":
|
|
517
|
+
grant_option = True
|
|
518
|
+
databases = ask_databases(admin)
|
|
519
|
+
scopes = ask_tables(admin, databases)
|
|
520
|
+
|
|
521
|
+
statements = build_edit_grants_statements(username, privileges, scopes, grant_option)
|
|
522
|
+
table = make_table(f"Novos acessos de '{username}'")
|
|
523
|
+
table.add_column("Privilégios", style="bold")
|
|
524
|
+
table.add_column("Escopo", style="bold")
|
|
525
|
+
table.add_column("Grant option", style="dim")
|
|
526
|
+
for s in scopes:
|
|
527
|
+
table.add_row(", ".join(privileges), s.on_clause(), "sim" if grant_option else "não")
|
|
528
|
+
console.print(table)
|
|
529
|
+
|
|
530
|
+
console.print("[dim]SQL a executar (ordem importa: primeiro zera, depois concede):[/]")
|
|
531
|
+
from rich.panel import Panel as RichPanel
|
|
532
|
+
|
|
533
|
+
for sql in statements:
|
|
534
|
+
console.print(RichPanel(Syntax(sql + ";", "sql", theme="monokai"), border_style="dim"))
|
|
535
|
+
|
|
536
|
+
go = _ask_or_abort(Q.confirm(f"Aplicar novos acessos a '{username}' agora?", default=False, style=APP_STYLE, qmark="›"))
|
|
537
|
+
if not go:
|
|
538
|
+
console.print("[dim]Nada foi executado.[/]")
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
with console.status("[cyan]Aplicando acessos…[/]", spinner="dots"):
|
|
542
|
+
try:
|
|
543
|
+
admin.execute_statements(statements)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
error(f"Falha ao editar acessos: {e}")
|
|
546
|
+
info("Dica: a conta precisa de GRANT/REVOKE nos escopos envolvidos. Os acessos antigos podem ter sido zerados — confira com Listar.")
|
|
547
|
+
return
|
|
548
|
+
success(f"Acessos de '{username}' atualizados ({len(statements)} comando(s)).")
|
|
549
|
+
|
|
550
|
+
|
|
418
551
|
def flow_manage_users(admin: ClickHouseAdmin) -> None:
|
|
419
552
|
step("Usuários · Gerenciar", "Desativar = HOST NONE (reversível). Excluir = DROP USER (definitivo).")
|
|
420
553
|
with console.status("[cyan]Listando usuários…[/]", spinner="dots"):
|
|
@@ -440,7 +573,7 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
|
|
|
440
573
|
inactive = _is_inactive(create_sql)
|
|
441
574
|
|
|
442
575
|
if target == admin.conn.username:
|
|
443
|
-
|
|
576
|
+
warn(f"'{target}' é o usuário conectado agora. Desativar/excluir pode te derrubar.")
|
|
444
577
|
|
|
445
578
|
options = []
|
|
446
579
|
if inactive:
|
|
@@ -448,6 +581,7 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
|
|
|
448
581
|
else:
|
|
449
582
|
options.append(Q.Choice("Desativar — ALTER USER … HOST NONE (reversível)", value="deactivate"))
|
|
450
583
|
options += [
|
|
584
|
+
Q.Choice("Editar permissões — bancos/tabelas + tipo de acesso", value="edit"),
|
|
451
585
|
Q.Choice("Excluir — DROP USER (definitivo!)", value="drop"),
|
|
452
586
|
Q.Choice("Voltar ao menu", value="back"),
|
|
453
587
|
]
|
|
@@ -456,6 +590,9 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
|
|
|
456
590
|
)
|
|
457
591
|
if action == "back":
|
|
458
592
|
return
|
|
593
|
+
if action == "edit":
|
|
594
|
+
flow_edit_grants(admin, target)
|
|
595
|
+
return
|
|
459
596
|
|
|
460
597
|
preview = {
|
|
461
598
|
"deactivate": build_deactivate_user_sql(target),
|
|
@@ -518,17 +655,24 @@ def flow_create_user(admin: ClickHouseAdmin) -> None:
|
|
|
518
655
|
def main() -> None:
|
|
519
656
|
try:
|
|
520
657
|
banner()
|
|
521
|
-
|
|
658
|
+
from clickhouse_users_cli.session import has_saved_session
|
|
659
|
+
|
|
660
|
+
saved_conn = maybe_load_saved_session()
|
|
661
|
+
conn = saved_conn or ask_connection()
|
|
522
662
|
admin = connect_with_retry(conn)
|
|
663
|
+
if saved_conn is None:
|
|
664
|
+
ask_remember(conn)
|
|
523
665
|
|
|
524
666
|
while True:
|
|
525
|
-
action = ask_main_action()
|
|
667
|
+
action = ask_main_action(show_forget=has_saved_session())
|
|
526
668
|
if action == "create":
|
|
527
669
|
flow_create_user(admin)
|
|
528
670
|
elif action == "list":
|
|
529
671
|
flow_list_users(admin)
|
|
530
672
|
elif action == "manage":
|
|
531
673
|
flow_manage_users(admin)
|
|
674
|
+
elif action == "forget":
|
|
675
|
+
flow_forget_session()
|
|
532
676
|
else:
|
|
533
677
|
console.print("[dim]Até logo![/]")
|
|
534
678
|
return
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Sessão local opcional — salva a conexão admin em YAML para reutilizar.
|
|
2
|
+
|
|
3
|
+
Segurança: a senha fica em texto puro no arquivo. O arquivo é criado com
|
|
4
|
+
permissão restrita (0600 no POSIX) e o salvamento é sempre opt-in explícito.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from clickhouse_users_cli.db import ConnectionInfo
|
|
13
|
+
|
|
14
|
+
SESSION_DIR = Path.home() / ".ch-users"
|
|
15
|
+
SESSION_FILE = SESSION_DIR / "connection.yaml"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def has_saved_session() -> bool:
|
|
19
|
+
return SESSION_FILE.is_file()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def describe_session(data: dict) -> str:
|
|
23
|
+
proto = "https" if data.get("secure") else "http"
|
|
24
|
+
return f"{proto}://{data.get('host')}:{data.get('port')} · usuário '{data.get('username')}'"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def save_session(conn: ConnectionInfo) -> Path:
|
|
28
|
+
"""Grava a conexão em YAML com permissão restrita. Retorna o caminho."""
|
|
29
|
+
try:
|
|
30
|
+
import yaml
|
|
31
|
+
except ImportError as e: # pragma: no cover
|
|
32
|
+
raise RuntimeError("Dependência 'pyyaml' não instalada. Rode: pip install pyyaml") from e
|
|
33
|
+
|
|
34
|
+
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
payload = {
|
|
36
|
+
"host": conn.host,
|
|
37
|
+
"port": conn.port,
|
|
38
|
+
"secure": conn.secure,
|
|
39
|
+
"username": conn.username,
|
|
40
|
+
"password": conn.password,
|
|
41
|
+
}
|
|
42
|
+
with open(SESSION_FILE, "w", encoding="utf-8") as f:
|
|
43
|
+
yaml.safe_dump(payload, f, allow_unicode=True, sort_keys=False)
|
|
44
|
+
try:
|
|
45
|
+
os.chmod(SESSION_FILE, 0o600)
|
|
46
|
+
except OSError:
|
|
47
|
+
pass # Windows: melhor esforço; o aviso em tela cobre o risco
|
|
48
|
+
return SESSION_FILE
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_session() -> ConnectionInfo:
|
|
52
|
+
"""Lê o YAML e devolve ConnectionInfo. Erro amigável se ausente/corrompido."""
|
|
53
|
+
try:
|
|
54
|
+
import yaml
|
|
55
|
+
except ImportError as e: # pragma: no cover
|
|
56
|
+
raise RuntimeError("Dependência 'pyyaml' não instalada. Rode: pip install pyyaml") from e
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
with open(SESSION_FILE, encoding="utf-8") as f:
|
|
60
|
+
data = yaml.safe_load(f) or {}
|
|
61
|
+
return ConnectionInfo(
|
|
62
|
+
host=str(data["host"]),
|
|
63
|
+
port=int(data["port"]),
|
|
64
|
+
secure=bool(data["secure"]),
|
|
65
|
+
username=str(data["username"]),
|
|
66
|
+
password=str(data.get("password") or ""),
|
|
67
|
+
)
|
|
68
|
+
except KeyError as e:
|
|
69
|
+
raise RuntimeError(f"Arquivo de sessão incompleto (falta {e}). Apague e salve de novo.") from e
|
|
70
|
+
except (OSError, ValueError, TypeError) as e:
|
|
71
|
+
raise RuntimeError(f"Não foi possível ler a sessão salva: {e}") from e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def delete_session() -> bool:
|
|
75
|
+
"""Apaga o YAML. Retorna True se algo foi removido."""
|
|
76
|
+
try:
|
|
77
|
+
SESSION_FILE.unlink()
|
|
78
|
+
return True
|
|
79
|
+
except FileNotFoundError:
|
|
80
|
+
return False
|
{clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli/sql_builder.py
RENAMED
|
@@ -114,3 +114,16 @@ def build_drop_user_sql(username: str, if_exists: bool = True) -> str:
|
|
|
114
114
|
"""Exclui o usuário em definitivo."""
|
|
115
115
|
ine = "IF EXISTS " if if_exists else ""
|
|
116
116
|
return f"DROP USER {ine}{escape_ident(username)}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_revoke_all_sql(username: str) -> str:
|
|
120
|
+
"""Remove todos os privilégios em todos os escopos (zera antes de reaplicar)."""
|
|
121
|
+
return f"REVOKE ALL ON *.* FROM {escape_ident(username)}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def build_edit_grants_statements(
|
|
125
|
+
username: str, privileges: list[str], scopes: list[GrantScope], grant_option: bool = False
|
|
126
|
+
) -> list[str]:
|
|
127
|
+
"""Edição de acessos: REVOKE ALL + GRANTs novos (com preview + confirmação no CLI)."""
|
|
128
|
+
spec = UserSpec(username=username, password="", privileges=privileges, scopes=scopes, grant_option=grant_option)
|
|
129
|
+
return [build_revoke_all_sql(username), *build_grant_sql(spec)]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Estilo centralizado (UI) + helpers de apresentação com Rich."""
|
|
2
|
+
|
|
3
|
+
from questionary import Style
|
|
4
|
+
from rich.align import Align
|
|
5
|
+
from rich.box import DOUBLE, ROUNDED
|
|
6
|
+
from rich.console import Console, Group
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.rule import Rule
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from clickhouse_users_cli import __version__
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
CYAN = "#00d7ff"
|
|
17
|
+
MAGENTA = "#c792ea"
|
|
18
|
+
GREEN = "#00ff87"
|
|
19
|
+
RED = "#ff5555"
|
|
20
|
+
AMBER = "#ffcb6b"
|
|
21
|
+
MUTED = "#6c7086"
|
|
22
|
+
BORDER = "#3a3f4b"
|
|
23
|
+
|
|
24
|
+
# Estilo questionary: alto contraste, foco visível, perigo em vermelho.
|
|
25
|
+
# Mantém a navegação por teclado óbvia (setas + espaço + enter).
|
|
26
|
+
APP_STYLE = Style(
|
|
27
|
+
[
|
|
28
|
+
("qmark", "fg:#00d7ff bold"), # "?" das perguntas
|
|
29
|
+
("question", "fg:#ffffff bold"), # texto da pergunta
|
|
30
|
+
("answer", "fg:#00ff87 bold"), # resposta escolhida
|
|
31
|
+
("pointer", "fg:#00d7ff bold"), # "»" do item focado
|
|
32
|
+
("highlighted", "fg:#00d7ff bold"), # item focado
|
|
33
|
+
("selected", "fg:#00ff87"), # checkbox marcado "●"
|
|
34
|
+
("separator", "fg:#6c7086"),
|
|
35
|
+
("instruction", "fg:#a6adc8 italic"),
|
|
36
|
+
("text", "fg:#ffffff"),
|
|
37
|
+
("disabled", "fg:#6c7086 italic"),
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _logo_title() -> Text:
|
|
43
|
+
"""Título em degradê ciano → magenta."""
|
|
44
|
+
t = Text(no_wrap=True)
|
|
45
|
+
t.append("⬢ ", style=f"bold {CYAN}")
|
|
46
|
+
t.append("ClickHouse", style=f"bold {CYAN}")
|
|
47
|
+
t.append(" ", style="bold")
|
|
48
|
+
t.append("Users CLI", style=f"bold {MAGENTA}")
|
|
49
|
+
return t
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def banner() -> None:
|
|
53
|
+
mark = Text("╭──────────╮\n│ ▓▓▓▓▓▓▓▓ │\n│ │\n╰──────────╯", style=f"bold {CYAN}", justify="center")
|
|
54
|
+
body = Group(
|
|
55
|
+
Align.center(mark),
|
|
56
|
+
Align.center(_logo_title()),
|
|
57
|
+
Align.center(Text("Crie usuários com escopo mínimo necessário", style="dim")),
|
|
58
|
+
Align.center(Text("criar ◆ listar ◆ desativar ◆ auditar", style=f"bold {CYAN}")),
|
|
59
|
+
)
|
|
60
|
+
console.print(
|
|
61
|
+
Panel(
|
|
62
|
+
body,
|
|
63
|
+
box=DOUBLE,
|
|
64
|
+
border_style=CYAN,
|
|
65
|
+
title=f"[black on {CYAN}] v{__version__} ",
|
|
66
|
+
subtitle="[dim]least privilege por padrão[/]",
|
|
67
|
+
padding=(1, 4),
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def step(title: str, subtitle: str = "") -> None:
|
|
73
|
+
"""Cabeçalho de etapa — orienta o usuário sobre 'onde estou' no fluxo."""
|
|
74
|
+
console.print()
|
|
75
|
+
console.print(Rule(f"[black on {CYAN}] {title} [/]", style=CYAN))
|
|
76
|
+
if subtitle:
|
|
77
|
+
console.print(f" [dim]{subtitle}[/]")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def make_table(title: str) -> Table:
|
|
81
|
+
"""Tabela padrão: bordas arredondadas, cabeçalho ciano."""
|
|
82
|
+
return Table(
|
|
83
|
+
title=f"[bold white]{title}[/]",
|
|
84
|
+
show_header=True,
|
|
85
|
+
header_style=f"bold {CYAN}",
|
|
86
|
+
border_style=BORDER,
|
|
87
|
+
box=ROUNDED,
|
|
88
|
+
show_lines=False,
|
|
89
|
+
padding=(0, 1),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def success(msg: str) -> None:
|
|
94
|
+
console.print(f"[black on {GREEN}] ✔ [/] [bold {GREEN}]{msg}[/]")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def error(msg: str) -> None:
|
|
98
|
+
console.print(
|
|
99
|
+
Panel(
|
|
100
|
+
f"[bold {RED}]{msg}[/]",
|
|
101
|
+
title=f"[bold {RED}]✘ Erro[/]",
|
|
102
|
+
border_style=RED,
|
|
103
|
+
box=ROUNDED,
|
|
104
|
+
padding=(0, 1),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def warn(msg: str) -> None:
|
|
110
|
+
console.print(f"[black on {AMBER}] ! [/] [{AMBER}]{msg}[/]")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def info(msg: str) -> None:
|
|
114
|
+
console.print(f"[dim]▸ {msg}[/]")
|
{clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clickhouse-users-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: CLI interativa para criar usuarios ClickHouse com privilegio minimo (least privilege)
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Keywords: clickhouse,cli,users,access-control,least-privilege
|
|
@@ -19,6 +19,7 @@ License-File: LICENSE
|
|
|
19
19
|
Requires-Dist: questionary>=2.0.0
|
|
20
20
|
Requires-Dist: clickhouse-connect>=0.8.0
|
|
21
21
|
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
23
|
Dynamic: license-file
|
|
23
24
|
|
|
24
25
|
# ClickHouse Users CLI
|
|
@@ -47,6 +48,13 @@ Para listar/gerenciar também precisa de `SHOW USERS` + `ALTER USER` / `DROP USE
|
|
|
47
48
|
Após conectar, escolha: **Criar usuário** · **Listar usuários** · **Gerenciar usuário** · **Sair**.
|
|
48
49
|
O menu fica em loop — dá para criar, auditar e desativar vários sem reconectar.
|
|
49
50
|
|
|
51
|
+
## Sessão salva (manter conectado)
|
|
52
|
+
|
|
53
|
+
- Na primeira conexão o CLI pergunta se quer **manter conectado**: salva `~/.ch-users/connection.yaml` com host, porta, protocolo, usuário e senha.
|
|
54
|
+
- Na próxima execução, oferece **usar a salva / nova conexão / apagar a salva**.
|
|
55
|
+
- No menu há **Esquecer sessão** (apaga o YAML, com confirmação).
|
|
56
|
+
- Segurança: a senha fica em **texto puro** no arquivo (permissão `0600` no Linux/mac; no Windows vale o aviso em tela). Só use em máquina confiável — nunca compartilhe o arquivo.
|
|
57
|
+
|
|
50
58
|
## Fluxo — criar (6 etapas)
|
|
51
59
|
|
|
52
60
|
1. **Conexão** — host, protocolo, porta, usuário/senha admin. Testa com `SELECT 1` e permite tentar de novo / editar / sair.
|
|
@@ -68,6 +76,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
|
|
|
68
76
|
|
|
69
77
|
- **Listar** — `SHOW USERS` em tabela + opção de ver `SHOW CREATE USER` e `SHOW GRANTS FOR` de um usuário.
|
|
70
78
|
- **Gerenciar** — escolhe 1 usuário, vê status (`HOST NONE` = inativo) + grants, depois:
|
|
79
|
+
- **Editar permissões** → pergunta perfil (tipo de acesso) + bancos/tabelas e aplica `REVOKE ALL ON *.*` + novos `GRANTs` (com preview e confirmação);
|
|
71
80
|
- **Desativar** → `ALTER USER \`nome\` HOST NONE` (reversível, bloqueia login);
|
|
72
81
|
- **Reativar** → `ALTER USER \`nome\` HOST ANY`;
|
|
73
82
|
- **Excluir** → `DROP USER IF EXISTS \`nome\`` (definitivo).
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""Estilo centralizado (UI) + helpers de apresentação com Rich."""
|
|
2
|
-
|
|
3
|
-
from questionary import Style
|
|
4
|
-
from rich.console import Console
|
|
5
|
-
from rich.panel import Panel
|
|
6
|
-
from rich.rule import Rule
|
|
7
|
-
|
|
8
|
-
console = Console()
|
|
9
|
-
|
|
10
|
-
# Estilo questionary: alto contraste, foco visível, perigo em vermelho.
|
|
11
|
-
# Mantém a navegação por teclado óbvia (setas + espaço + enter).
|
|
12
|
-
APP_STYLE = Style(
|
|
13
|
-
[
|
|
14
|
-
("qmark", "fg:#00d7ff bold"), # "?" das perguntas
|
|
15
|
-
("question", "fg:#ffffff bold"), # texto da pergunta
|
|
16
|
-
("answer", "fg:#00ff87 bold"), # resposta escolhida
|
|
17
|
-
("pointer", "fg:#00d7ff bold"), # "»" do item focado
|
|
18
|
-
("highlighted", "fg:#00d7ff bold"), # item focado
|
|
19
|
-
("selected", "fg:#00ff87"), # checkbox marcado "●"
|
|
20
|
-
("separator", "fg:#6c7086"),
|
|
21
|
-
("instruction", "fg:#a6adc8 italic"),
|
|
22
|
-
("text", "fg:#ffffff"),
|
|
23
|
-
("disabled", "fg:#6c7086 italic"),
|
|
24
|
-
]
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def banner() -> None:
|
|
29
|
-
console.print(
|
|
30
|
-
Panel.fit(
|
|
31
|
-
"[bold cyan]ClickHouse Users CLI[/]\n"
|
|
32
|
-
"[dim]Crie usuários com escopo mínimo necessário (least privilege)[/dim]",
|
|
33
|
-
border_style="cyan",
|
|
34
|
-
)
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def step(title: str, subtitle: str = "") -> None:
|
|
39
|
-
"""Cabeçalho de etapa — orienta o usuário sobre 'onde estou' no fluxo."""
|
|
40
|
-
console.print()
|
|
41
|
-
console.print(Rule(f"[bold cyan]{title}[/]", style="cyan"))
|
|
42
|
-
if subtitle:
|
|
43
|
-
console.print(f"[dim]{subtitle}[/]")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def success(msg: str) -> None:
|
|
47
|
-
console.print(f"[bold green]✔ {msg}[/]")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def error(msg: str) -> None:
|
|
51
|
-
console.print(f"[bold red]✘ {msg}[/]")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def info(msg: str) -> None:
|
|
55
|
-
console.print(f"[dim]{msg}[/]")
|
|
File without changes
|
|
File without changes
|
{clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.3}/clickhouse_users_cli/validators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|