clickhouse-users-cli 0.1.0__tar.gz → 0.1.2__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.0 → clickhouse_users_cli-0.1.2}/PKG-INFO +10 -2
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/README.md +8 -1
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/__init__.py +1 -1
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/app.py +116 -18
- clickhouse_users_cli-0.1.2/clickhouse_users_cli/session.py +80 -0
- clickhouse_users_cli-0.1.2/clickhouse_users_cli/style.py +114 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/PKG-INFO +10 -2
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/SOURCES.txt +1 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/requires.txt +1 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/pyproject.toml +1 -0
- clickhouse_users_cli-0.1.0/clickhouse_users_cli/style.py +0 -55
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/LICENSE +0 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/db.py +0 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/sql_builder.py +0 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/validators.py +0 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/dependency_links.txt +0 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/entry_points.txt +0 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/top_level.txt +0 -0
- {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/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.2
|
|
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.
|
|
@@ -88,7 +96,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
|
|
|
88
96
|
|
|
89
97
|
Decisões de UX:
|
|
90
98
|
|
|
91
|
-
- **
|
|
99
|
+
- **Nada pré-marcado + opção `✓ Todos os bancos`** (least privilege: acesso só via opt-in explícito; sistemas vão por último com tag `(sistema)`).
|
|
92
100
|
- **Ctrl+C = saída limpa**, nada executa sem confirmação final.
|
|
93
101
|
- **SQL separado da UI** (`clickhouse_users_cli/sql_builder.py`) — dá para testar sem banco.
|
|
94
102
|
- **Escape correto** de identificadores (backticks) e strings (aspas simples).
|
|
@@ -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.
|
|
@@ -65,7 +72,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
|
|
|
65
72
|
|
|
66
73
|
Decisões de UX:
|
|
67
74
|
|
|
68
|
-
- **
|
|
75
|
+
- **Nada pré-marcado + opção `✓ Todos os bancos`** (least privilege: acesso só via opt-in explícito; sistemas vão por último com tag `(sistema)`).
|
|
69
76
|
- **Ctrl+C = saída limpa**, nada executa sem confirmação final.
|
|
70
77
|
- **SQL separado da UI** (`clickhouse_users_cli/sql_builder.py`) — dá para testar sem banco.
|
|
71
78
|
- **Escape correto** de identificadores (backticks) e strings (aspas simples).
|
|
@@ -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 (
|
|
@@ -19,7 +18,8 @@ from clickhouse_users_cli.sql_builder import (
|
|
|
19
18
|
build_deactivate_user_sql,
|
|
20
19
|
build_drop_user_sql,
|
|
21
20
|
)
|
|
22
|
-
from clickhouse_users_cli.style import APP_STYLE, banner, console, error, info, step, success
|
|
21
|
+
from clickhouse_users_cli.style import APP_STYLE, banner, console, error, info, make_table, step, success, warn
|
|
22
|
+
from clickhouse_users_cli.session import SESSION_FILE
|
|
23
23
|
from clickhouse_users_cli.validators import (
|
|
24
24
|
validate_at_least_one,
|
|
25
25
|
validate_host,
|
|
@@ -152,12 +152,12 @@ def ask_databases(admin: ClickHouseAdmin) -> list[str]:
|
|
|
152
152
|
error("Nenhum banco encontrado.")
|
|
153
153
|
raise SystemExit(1)
|
|
154
154
|
|
|
155
|
-
choices = [
|
|
155
|
+
choices = [Q.Choice(title="✓ Todos os bancos", value="__ALL__")] + [
|
|
156
156
|
Q.Choice(
|
|
157
157
|
title=f"{db} (sistema)" if db in SYSTEM_DATABASES else db,
|
|
158
158
|
value=db,
|
|
159
|
-
# UX:
|
|
160
|
-
checked=
|
|
159
|
+
# UX: nada pré-marcado (least privilege) — o usuário opta explicitamente.
|
|
160
|
+
checked=False,
|
|
161
161
|
)
|
|
162
162
|
for db in databases
|
|
163
163
|
]
|
|
@@ -170,6 +170,8 @@ def ask_databases(admin: ClickHouseAdmin) -> list[str]:
|
|
|
170
170
|
instruction="(espaço marca · enter confirma)",
|
|
171
171
|
)
|
|
172
172
|
)
|
|
173
|
+
if "__ALL__" in selected:
|
|
174
|
+
return list(databases)
|
|
173
175
|
return selected
|
|
174
176
|
|
|
175
177
|
|
|
@@ -286,7 +288,7 @@ def ask_create_options() -> bool:
|
|
|
286
288
|
|
|
287
289
|
def show_review(spec: UserSpec) -> None:
|
|
288
290
|
step("6/6 · Revisão", "Confira o resumo e o SQL exato que será executado.")
|
|
289
|
-
table =
|
|
291
|
+
table = make_table("Resumo do novo usuário")
|
|
290
292
|
table.add_column("Campo", style="dim")
|
|
291
293
|
table.add_column("Valor", style="bold")
|
|
292
294
|
table.add_row("Usuário", spec.username)
|
|
@@ -324,18 +326,107 @@ def confirm_and_execute(admin: ClickHouseAdmin, spec: UserSpec) -> None:
|
|
|
324
326
|
|
|
325
327
|
|
|
326
328
|
|
|
329
|
+
# ── Sessão salva (YAML local, opt-in) ────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
def maybe_load_saved_session() -> ConnectionInfo | None:
|
|
332
|
+
"""Se há YAML salvo, oferece usar / nova conexão / apagar. None = digitar."""
|
|
333
|
+
from clickhouse_users_cli.session import delete_session, has_saved_session, load_session
|
|
334
|
+
|
|
335
|
+
if not has_saved_session():
|
|
336
|
+
return None
|
|
337
|
+
step("Sessão salva", f"Arquivo: {SESSION_FILE} (senha em texto puro — nunca compartilhe)")
|
|
338
|
+
try:
|
|
339
|
+
saved = load_session()
|
|
340
|
+
except RuntimeError as e:
|
|
341
|
+
error(str(e))
|
|
342
|
+
fix = _ask_or_abort(
|
|
343
|
+
Q.select(
|
|
344
|
+
"A sessão salva está inválida. O que fazer?",
|
|
345
|
+
choices=[
|
|
346
|
+
Q.Choice("Apagar e digitar nova conexão", value="delete"),
|
|
347
|
+
Q.Choice("Digitar nova conexão (manter arquivo)", value="new"),
|
|
348
|
+
],
|
|
349
|
+
style=APP_STYLE, qmark="›",
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
if fix == "delete":
|
|
353
|
+
delete_session()
|
|
354
|
+
success("Sessão apagada.")
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
info(f"Conexão guardada: {saved.protocol}://{saved.host}:{saved.port} · usuário '{saved.username}'.")
|
|
358
|
+
choice = _ask_or_abort(
|
|
359
|
+
Q.select(
|
|
360
|
+
"Usar a conexão salva?",
|
|
361
|
+
choices=[
|
|
362
|
+
Q.Choice("Usar conexão salva", value="use"),
|
|
363
|
+
Q.Choice("Nova conexão (digitar de novo)", value="new"),
|
|
364
|
+
Q.Choice("Apagar conexão salva", value="delete"),
|
|
365
|
+
],
|
|
366
|
+
default="use",
|
|
367
|
+
style=APP_STYLE, qmark="›",
|
|
368
|
+
instruction="(↑↓ navega · enter confirma)",
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
if choice == "delete":
|
|
372
|
+
delete_session()
|
|
373
|
+
success("Sessão apagada.")
|
|
374
|
+
return None
|
|
375
|
+
if choice == "new":
|
|
376
|
+
return None
|
|
377
|
+
return saved
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def ask_remember(conn: ConnectionInfo) -> None:
|
|
381
|
+
"""Opt-in: salva a conexão em YAML local (senha em texto puro)."""
|
|
382
|
+
from clickhouse_users_cli.session import save_session
|
|
383
|
+
|
|
384
|
+
warn("A senha será gravada em texto puro no arquivo — só use em máquina confiável.")
|
|
385
|
+
keep = _ask_or_abort(
|
|
386
|
+
Q.confirm("Manter conectado neste computador (salvar sessão)?", default=False, style=APP_STYLE, qmark="›")
|
|
387
|
+
)
|
|
388
|
+
if not keep:
|
|
389
|
+
return
|
|
390
|
+
try:
|
|
391
|
+
path = save_session(conn)
|
|
392
|
+
except RuntimeError as e:
|
|
393
|
+
error(str(e))
|
|
394
|
+
return
|
|
395
|
+
success(f"Sessão salva em {path}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def flow_forget_session() -> None:
|
|
399
|
+
from clickhouse_users_cli.session import delete_session, has_saved_session
|
|
400
|
+
|
|
401
|
+
if not has_saved_session():
|
|
402
|
+
info("Nenhuma sessão salva.")
|
|
403
|
+
return
|
|
404
|
+
step("Esquecer sessão", f"Arquivo: {SESSION_FILE}")
|
|
405
|
+
go = _ask_or_abort(
|
|
406
|
+
Q.confirm("Apagar a sessão salva (vai pedir credenciais na próxima vez)?", default=False, style=APP_STYLE, qmark="›")
|
|
407
|
+
)
|
|
408
|
+
if not go:
|
|
409
|
+
console.print("[dim]Nada foi apagado.[/]")
|
|
410
|
+
return
|
|
411
|
+
delete_session()
|
|
412
|
+
success("Sessão apagada.")
|
|
413
|
+
|
|
414
|
+
|
|
327
415
|
# ── Menu principal + listar/gerenciar ────────────────────────────────────────────
|
|
328
416
|
|
|
329
|
-
def ask_main_action() -> str:
|
|
417
|
+
def ask_main_action(show_forget: bool = False) -> str:
|
|
418
|
+
choices = [
|
|
419
|
+
Q.Choice("Criar usuário — com privilégio mínimo", value="create"),
|
|
420
|
+
Q.Choice("Listar usuários — SHOW USERS + grants", value="list"),
|
|
421
|
+
Q.Choice("Gerenciar usuário — desativar / reativar / excluir", value="manage"),
|
|
422
|
+
]
|
|
423
|
+
if show_forget:
|
|
424
|
+
choices.append(Q.Choice("Esquecer sessão — apagar YAML salvo", value="forget"))
|
|
425
|
+
choices.append(Q.Choice("Sair", value="exit"))
|
|
330
426
|
return _ask_or_abort(
|
|
331
427
|
Q.select(
|
|
332
428
|
"O que deseja fazer?",
|
|
333
|
-
choices=
|
|
334
|
-
Q.Choice("Criar usuário — com privilégio mínimo", value="create"),
|
|
335
|
-
Q.Choice("Listar usuários — SHOW USERS + grants", value="list"),
|
|
336
|
-
Q.Choice("Gerenciar usuário — desativar / reativar / excluir", value="manage"),
|
|
337
|
-
Q.Choice("Sair", value="exit"),
|
|
338
|
-
],
|
|
429
|
+
choices=choices,
|
|
339
430
|
default="create",
|
|
340
431
|
style=APP_STYLE, qmark="›",
|
|
341
432
|
instruction="(↑↓ navega · enter confirma)",
|
|
@@ -360,7 +451,7 @@ def show_user_details(admin: ClickHouseAdmin, username: str) -> tuple[str, list[
|
|
|
360
451
|
error(f"Falha ao ver grants de '{username}': {e}")
|
|
361
452
|
grants = []
|
|
362
453
|
|
|
363
|
-
table =
|
|
454
|
+
table = make_table(f"Usuário {username}")
|
|
364
455
|
table.add_column("Campo", style="dim")
|
|
365
456
|
table.add_column("Valor", style="bold")
|
|
366
457
|
status = "inativo (HOST NONE)" if _is_inactive(create_sql) else "ativo"
|
|
@@ -390,7 +481,7 @@ def flow_list_users(admin: ClickHouseAdmin) -> None:
|
|
|
390
481
|
info("Nenhum usuário encontrado.")
|
|
391
482
|
return
|
|
392
483
|
|
|
393
|
-
table =
|
|
484
|
+
table = make_table(f"{len(users)} usuário(s)")
|
|
394
485
|
table.add_column("#", style="dim")
|
|
395
486
|
table.add_column("Usuário", style="bold")
|
|
396
487
|
for i, u in enumerate(users, 1):
|
|
@@ -438,7 +529,7 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
|
|
|
438
529
|
inactive = _is_inactive(create_sql)
|
|
439
530
|
|
|
440
531
|
if target == admin.conn.username:
|
|
441
|
-
|
|
532
|
+
warn(f"'{target}' é o usuário conectado agora. Desativar/excluir pode te derrubar.")
|
|
442
533
|
|
|
443
534
|
options = []
|
|
444
535
|
if inactive:
|
|
@@ -516,17 +607,24 @@ def flow_create_user(admin: ClickHouseAdmin) -> None:
|
|
|
516
607
|
def main() -> None:
|
|
517
608
|
try:
|
|
518
609
|
banner()
|
|
519
|
-
|
|
610
|
+
from clickhouse_users_cli.session import has_saved_session
|
|
611
|
+
|
|
612
|
+
saved_conn = maybe_load_saved_session()
|
|
613
|
+
conn = saved_conn or ask_connection()
|
|
520
614
|
admin = connect_with_retry(conn)
|
|
615
|
+
if saved_conn is None:
|
|
616
|
+
ask_remember(conn)
|
|
521
617
|
|
|
522
618
|
while True:
|
|
523
|
-
action = ask_main_action()
|
|
619
|
+
action = ask_main_action(show_forget=has_saved_session())
|
|
524
620
|
if action == "create":
|
|
525
621
|
flow_create_user(admin)
|
|
526
622
|
elif action == "list":
|
|
527
623
|
flow_list_users(admin)
|
|
528
624
|
elif action == "manage":
|
|
529
625
|
flow_manage_users(admin)
|
|
626
|
+
elif action == "forget":
|
|
627
|
+
flow_forget_session()
|
|
530
628
|
else:
|
|
531
629
|
console.print("[dim]Até logo![/]")
|
|
532
630
|
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
|
|
@@ -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.0 → clickhouse_users_cli-0.1.2}/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.2
|
|
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.
|
|
@@ -88,7 +96,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
|
|
|
88
96
|
|
|
89
97
|
Decisões de UX:
|
|
90
98
|
|
|
91
|
-
- **
|
|
99
|
+
- **Nada pré-marcado + opção `✓ Todos os bancos`** (least privilege: acesso só via opt-in explícito; sistemas vão por último com tag `(sistema)`).
|
|
92
100
|
- **Ctrl+C = saída limpa**, nada executa sem confirmação final.
|
|
93
101
|
- **SQL separado da UI** (`clickhouse_users_cli/sql_builder.py`) — dá para testar sem banco.
|
|
94
102
|
- **Escape correto** de identificadores (backticks) e strings (aspas simples).
|
|
@@ -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.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/sql_builder.py
RENAMED
|
File without changes
|
{clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/validators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|