clickhouse-users-cli 0.1.2__tar.gz → 0.1.4__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.
Files changed (18) hide show
  1. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/PKG-INFO +2 -1
  2. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/README.md +1 -0
  3. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli/__init__.py +1 -1
  4. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli/app.py +73 -11
  5. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli/sql_builder.py +13 -0
  6. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli.egg-info/PKG-INFO +2 -1
  7. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/LICENSE +0 -0
  8. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli/db.py +0 -0
  9. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli/session.py +0 -0
  10. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli/style.py +0 -0
  11. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli/validators.py +0 -0
  12. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli.egg-info/SOURCES.txt +0 -0
  13. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli.egg-info/dependency_links.txt +0 -0
  14. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli.egg-info/entry_points.txt +0 -0
  15. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli.egg-info/requires.txt +0 -0
  16. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/clickhouse_users_cli.egg-info/top_level.txt +0 -0
  17. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/pyproject.toml +0 -0
  18. {clickhouse_users_cli-0.1.2 → clickhouse_users_cli-0.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clickhouse-users-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
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
@@ -76,6 +76,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
76
76
 
77
77
  - **Listar** — `SHOW USERS` em tabela + opção de ver `SHOW CREATE USER` e `SHOW GRANTS FOR` de um usuário.
78
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);
79
80
  - **Desativar** → `ALTER USER \`nome\` HOST NONE` (reversível, bloqueia login);
80
81
  - **Reativar** → `ALTER USER \`nome\` HOST ANY`;
81
82
  - **Excluir** → `DROP USER IF EXISTS \`nome\`` (definitivo).
@@ -52,6 +52,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
52
52
 
53
53
  - **Listar** — `SHOW USERS` em tabela + opção de ver `SHOW CREATE USER` e `SHOW GRANTS FOR` de um usuário.
54
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);
55
56
  - **Desativar** → `ALTER USER \`nome\` HOST NONE` (reversível, bloqueia login);
56
57
  - **Reativar** → `ALTER USER \`nome\` HOST ANY`;
57
58
  - **Excluir** → `DROP USER IF EXISTS \`nome\`` (definitivo).
@@ -1,5 +1,5 @@
1
1
  """ClickHouse Users CLI — crie usuários com escopo mínimo (least privilege)."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.4"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -17,6 +17,7 @@ from clickhouse_users_cli.sql_builder import (
17
17
  build_all_statements,
18
18
  build_deactivate_user_sql,
19
19
  build_drop_user_sql,
20
+ build_edit_grants_statements,
20
21
  )
21
22
  from clickhouse_users_cli.style import APP_STYLE, banner, console, error, info, make_table, step, success, warn
22
23
  from clickhouse_users_cli.session import SESSION_FILE
@@ -105,11 +106,16 @@ def connect_with_retry(conn: ConnectionInfo) -> ClickHouseAdmin:
105
106
 
106
107
  # ── Etapa 2: perfil do novo usuário ───────────────────────────────────────────
107
108
 
108
- def ask_profile() -> str:
109
- step("2/6 · Tipo de usuário", "O perfil define os privilégios padrão (least privilege primeiro).")
109
+ def ask_profile(mode: str = "create") -> str:
110
+ if mode == "edit":
111
+ step("Editar · Tipo de acesso", "O perfil redefine os privilégios (os atuais serão zerados).")
112
+ question = "Qual o novo tipo de acesso?"
113
+ else:
114
+ step("2/6 · Tipo de usuário", "O perfil define os privilégios padrão (least privilege primeiro).")
115
+ question = "Que tipo de usuário criar?"
110
116
  return _ask_or_abort(
111
117
  Q.select(
112
- "Que tipo de usuário criar?",
118
+ question,
113
119
  choices=[Q.Choice(PROFILE_LABELS[k], value=k) for k in ("readonly", "readwrite", "admin", "custom")],
114
120
  default="readonly",
115
121
  style=APP_STYLE, qmark="›",
@@ -139,8 +145,11 @@ def ask_new_credentials() -> tuple[str, str]:
139
145
 
140
146
  # ── Etapa 4: escopo (bancos → tabelas) ────────────────────────────────────────
141
147
 
142
- def ask_databases(admin: ClickHouseAdmin) -> list[str]:
143
- step("4/6 · Escopo — bancos", "Espaço marca · A confirma · Ctrl+A inverte a seleção.")
148
+ def ask_databases(admin: ClickHouseAdmin, mode: str = "create") -> list[str]:
149
+ if mode == "edit":
150
+ step("Editar · Escopo — bancos", "Espaço marca · A confirma · Ctrl+A inverte a seleção.")
151
+ else:
152
+ step("4/6 · Escopo — bancos", "Espaço marca · A confirma · Ctrl+A inverte a seleção.")
144
153
  with console.status("[cyan]Listando bancos…[/]", spinner="dots"):
145
154
  try:
146
155
  databases = admin.list_databases()
@@ -175,8 +184,11 @@ def ask_databases(admin: ClickHouseAdmin) -> list[str]:
175
184
  return selected
176
185
 
177
186
 
178
- def ask_tables(admin: ClickHouseAdmin, databases: list[str]) -> list[GrantScope]:
179
- step("4/6 · Escopo — tabelas", "Escolha '*' para o banco inteiro ou marque tabelas específicas.")
187
+ def ask_tables(admin: ClickHouseAdmin, databases: list[str], mode: str = "create") -> list[GrantScope]:
188
+ if mode == "edit":
189
+ step("Editar · Escopo — tabelas", "Escolha '*' para o banco inteiro ou marque tabelas específicas.")
190
+ else:
191
+ step("4/6 · Escopo — tabelas", "Escolha '*' para o banco inteiro ou marque tabelas específicas.")
180
192
  scopes: list[GrantScope] = []
181
193
  for db in databases:
182
194
  try:
@@ -225,13 +237,16 @@ PRIVILEGE_CHOICES = [
225
237
  ]
226
238
 
227
239
 
228
- def ask_privileges(profile: str) -> tuple[list[str], bool]:
240
+ def ask_privileges(profile: str, mode: str = "create") -> tuple[list[str], bool]:
229
241
  """Retorna (privilégios, grant_option)."""
230
242
  if profile != "custom":
231
243
  grant_option = profile == "admin"
232
244
  return list(PROFILE_PRIVILEGES[profile]), grant_option
233
245
 
234
- step("5/6 · Privilégios personalizados", "Marque só o necessário — DROP/TRUNCATE exigem confirmação extra.")
246
+ if mode == "edit":
247
+ step("Editar · Privilégios personalizados", "Marque só o necessário — DROP/TRUNCATE exigem confirmação extra.")
248
+ else:
249
+ step("5/6 · Privilégios personalizados", "Marque só o necessário — DROP/TRUNCATE exigem confirmação extra.")
235
250
  privs: list[str] = _ask_or_abort(
236
251
  Q.checkbox(
237
252
  "Quais privilégios conceder?",
@@ -246,7 +261,7 @@ def ask_privileges(profile: str) -> tuple[list[str], bool]:
246
261
  Q.confirm("Você marcou privilégio destrutivo. Tem certeza?", default=False, style=APP_STYLE, qmark="›")
247
262
  )
248
263
  if not grant_option_confirm:
249
- return ask_privileges("custom")
264
+ return ask_privileges("custom", mode=mode)
250
265
  else:
251
266
  grant_option = _ask_or_abort(
252
267
  Q.confirm("Permitir repassar acessos (WITH GRANT OPTION)?", default=False, style=APP_STYLE, qmark="›")
@@ -418,7 +433,7 @@ def ask_main_action(show_forget: bool = False) -> str:
418
433
  choices = [
419
434
  Q.Choice("Criar usuário — com privilégio mínimo", value="create"),
420
435
  Q.Choice("Listar usuários — SHOW USERS + grants", value="list"),
421
- Q.Choice("Gerenciar usuário — desativar / reativar / excluir", value="manage"),
436
+ Q.Choice("Gerenciar usuário — acessos, desativar / reativar / excluir", value="manage"),
422
437
  ]
423
438
  if show_forget:
424
439
  choices.append(Q.Choice("Esquecer sessão — apagar YAML salvo", value="forget"))
@@ -504,6 +519,49 @@ def flow_list_users(admin: ClickHouseAdmin) -> None:
504
519
  show_user_details(admin, target)
505
520
 
506
521
 
522
+ def flow_edit_grants(admin: ClickHouseAdmin, username: str) -> None:
523
+ """Redefine os acessos: REVOKE ALL + GRANTs novos (perfil + escopo + hosts intactos)."""
524
+ step("Usuários · Editar permissões", "Os acessos atuais serão zerados e reaplicados. Nada muda sem confirmação.")
525
+ if username == admin.conn.username:
526
+ warn(f"'{username}' é o usuário conectado agora. Zerar os próprios acessos pode te derrubar.")
527
+
528
+ profile = ask_profile(mode="edit")
529
+ privileges, grant_option = ask_privileges(profile, mode="edit")
530
+ if profile == "admin":
531
+ grant_option = True
532
+ databases = ask_databases(admin, mode="edit")
533
+ scopes = ask_tables(admin, databases, mode="edit")
534
+
535
+ statements = build_edit_grants_statements(username, privileges, scopes, grant_option)
536
+ table = make_table(f"Novos acessos de '{username}'")
537
+ table.add_column("Privilégios", style="bold")
538
+ table.add_column("Escopo", style="bold")
539
+ table.add_column("Grant option", style="dim")
540
+ for s in scopes:
541
+ table.add_row(", ".join(privileges), s.on_clause(), "sim" if grant_option else "não")
542
+ console.print(table)
543
+
544
+ console.print("[dim]SQL a executar (ordem importa: primeiro zera, depois concede):[/]")
545
+ from rich.panel import Panel as RichPanel
546
+
547
+ for sql in statements:
548
+ console.print(RichPanel(Syntax(sql + ";", "sql", theme="monokai"), border_style="dim"))
549
+
550
+ go = _ask_or_abort(Q.confirm(f"Aplicar novos acessos a '{username}' agora?", default=False, style=APP_STYLE, qmark="›"))
551
+ if not go:
552
+ console.print("[dim]Nada foi executado.[/]")
553
+ return
554
+
555
+ with console.status("[cyan]Aplicando acessos…[/]", spinner="dots"):
556
+ try:
557
+ admin.execute_statements(statements)
558
+ except Exception as e:
559
+ error(f"Falha ao editar acessos: {e}")
560
+ info("Dica: a conta precisa de GRANT/REVOKE nos escopos envolvidos. Os acessos antigos podem ter sido zerados — confira com Listar.")
561
+ return
562
+ success(f"Acessos de '{username}' atualizados ({len(statements)} comando(s)).")
563
+
564
+
507
565
  def flow_manage_users(admin: ClickHouseAdmin) -> None:
508
566
  step("Usuários · Gerenciar", "Desativar = HOST NONE (reversível). Excluir = DROP USER (definitivo).")
509
567
  with console.status("[cyan]Listando usuários…[/]", spinner="dots"):
@@ -537,6 +595,7 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
537
595
  else:
538
596
  options.append(Q.Choice("Desativar — ALTER USER … HOST NONE (reversível)", value="deactivate"))
539
597
  options += [
598
+ Q.Choice("Editar permissões — bancos/tabelas + tipo de acesso", value="edit"),
540
599
  Q.Choice("Excluir — DROP USER (definitivo!)", value="drop"),
541
600
  Q.Choice("Voltar ao menu", value="back"),
542
601
  ]
@@ -545,6 +604,9 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
545
604
  )
546
605
  if action == "back":
547
606
  return
607
+ if action == "edit":
608
+ flow_edit_grants(admin, target)
609
+ return
548
610
 
549
611
  preview = {
550
612
  "deactivate": build_deactivate_user_sql(target),
@@ -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)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clickhouse-users-cli
3
- Version: 0.1.2
3
+ Version: 0.1.4
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
@@ -76,6 +76,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
76
76
 
77
77
  - **Listar** — `SHOW USERS` em tabela + opção de ver `SHOW CREATE USER` e `SHOW GRANTS FOR` de um usuário.
78
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);
79
80
  - **Desativar** → `ALTER USER \`nome\` HOST NONE` (reversível, bloqueia login);
80
81
  - **Reativar** → `ALTER USER \`nome\` HOST ANY`;
81
82
  - **Excluir** → `DROP USER IF EXISTS \`nome\`` (definitivo).