clickhouse-users-cli 0.1.1__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.1 → clickhouse_users_cli-0.1.2}/PKG-INFO +9 -1
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/README.md +7 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/__init__.py +1 -1
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/app.py +111 -15
- 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.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/PKG-INFO +9 -1
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/SOURCES.txt +1 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/requires.txt +1 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/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.2}/LICENSE +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/db.py +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/sql_builder.py +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/validators.py +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/dependency_links.txt +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/entry_points.txt +0 -0
- {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/top_level.txt +0 -0
- {clickhouse_users_cli-0.1.1 → 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.
|
|
@@ -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.
|
|
@@ -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,
|
|
@@ -288,7 +288,7 @@ def ask_create_options() -> bool:
|
|
|
288
288
|
|
|
289
289
|
def show_review(spec: UserSpec) -> None:
|
|
290
290
|
step("6/6 · Revisão", "Confira o resumo e o SQL exato que será executado.")
|
|
291
|
-
table =
|
|
291
|
+
table = make_table("Resumo do novo usuário")
|
|
292
292
|
table.add_column("Campo", style="dim")
|
|
293
293
|
table.add_column("Valor", style="bold")
|
|
294
294
|
table.add_row("Usuário", spec.username)
|
|
@@ -326,18 +326,107 @@ def confirm_and_execute(admin: ClickHouseAdmin, spec: UserSpec) -> None:
|
|
|
326
326
|
|
|
327
327
|
|
|
328
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
|
+
|
|
329
415
|
# ── Menu principal + listar/gerenciar ────────────────────────────────────────────
|
|
330
416
|
|
|
331
|
-
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"))
|
|
332
426
|
return _ask_or_abort(
|
|
333
427
|
Q.select(
|
|
334
428
|
"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
|
-
],
|
|
429
|
+
choices=choices,
|
|
341
430
|
default="create",
|
|
342
431
|
style=APP_STYLE, qmark="›",
|
|
343
432
|
instruction="(↑↓ navega · enter confirma)",
|
|
@@ -362,7 +451,7 @@ def show_user_details(admin: ClickHouseAdmin, username: str) -> tuple[str, list[
|
|
|
362
451
|
error(f"Falha ao ver grants de '{username}': {e}")
|
|
363
452
|
grants = []
|
|
364
453
|
|
|
365
|
-
table =
|
|
454
|
+
table = make_table(f"Usuário {username}")
|
|
366
455
|
table.add_column("Campo", style="dim")
|
|
367
456
|
table.add_column("Valor", style="bold")
|
|
368
457
|
status = "inativo (HOST NONE)" if _is_inactive(create_sql) else "ativo"
|
|
@@ -392,7 +481,7 @@ def flow_list_users(admin: ClickHouseAdmin) -> None:
|
|
|
392
481
|
info("Nenhum usuário encontrado.")
|
|
393
482
|
return
|
|
394
483
|
|
|
395
|
-
table =
|
|
484
|
+
table = make_table(f"{len(users)} usuário(s)")
|
|
396
485
|
table.add_column("#", style="dim")
|
|
397
486
|
table.add_column("Usuário", style="bold")
|
|
398
487
|
for i, u in enumerate(users, 1):
|
|
@@ -440,7 +529,7 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
|
|
|
440
529
|
inactive = _is_inactive(create_sql)
|
|
441
530
|
|
|
442
531
|
if target == admin.conn.username:
|
|
443
|
-
|
|
532
|
+
warn(f"'{target}' é o usuário conectado agora. Desativar/excluir pode te derrubar.")
|
|
444
533
|
|
|
445
534
|
options = []
|
|
446
535
|
if inactive:
|
|
@@ -518,17 +607,24 @@ def flow_create_user(admin: ClickHouseAdmin) -> None:
|
|
|
518
607
|
def main() -> None:
|
|
519
608
|
try:
|
|
520
609
|
banner()
|
|
521
|
-
|
|
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()
|
|
522
614
|
admin = connect_with_retry(conn)
|
|
615
|
+
if saved_conn is None:
|
|
616
|
+
ask_remember(conn)
|
|
523
617
|
|
|
524
618
|
while True:
|
|
525
|
-
action = ask_main_action()
|
|
619
|
+
action = ask_main_action(show_forget=has_saved_session())
|
|
526
620
|
if action == "create":
|
|
527
621
|
flow_create_user(admin)
|
|
528
622
|
elif action == "list":
|
|
529
623
|
flow_list_users(admin)
|
|
530
624
|
elif action == "manage":
|
|
531
625
|
flow_manage_users(admin)
|
|
626
|
+
elif action == "forget":
|
|
627
|
+
flow_forget_session()
|
|
532
628
|
else:
|
|
533
629
|
console.print("[dim]Até logo![/]")
|
|
534
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.1 → 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.
|
|
@@ -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.2}/clickhouse_users_cli/sql_builder.py
RENAMED
|
File without changes
|
{clickhouse_users_cli-0.1.1 → 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
|