From 3b98b1c1bf7c98cc02de8a64e61213ae767a7242 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 28 Jan 2025 13:19:13 +0100 Subject: [PATCH 1/9] clickhouse_grants: new module --- .../targets/clickhouse_grants/meta/main.yml | 2 + .../clickhouse_grants/tasks/initial.yml | 48 +++++++++++++++++++ .../targets/clickhouse_grants/tasks/main.yml | 7 +++ 3 files changed, 57 insertions(+) create mode 100644 tests/integration/targets/clickhouse_grants/meta/main.yml create mode 100644 tests/integration/targets/clickhouse_grants/tasks/initial.yml create mode 100644 tests/integration/targets/clickhouse_grants/tasks/main.yml diff --git a/tests/integration/targets/clickhouse_grants/meta/main.yml b/tests/integration/targets/clickhouse_grants/meta/main.yml new file mode 100644 index 0000000..4a78727 --- /dev/null +++ b/tests/integration/targets/clickhouse_grants/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_clickhouse diff --git a/tests/integration/targets/clickhouse_grants/tasks/initial.yml b/tests/integration/targets/clickhouse_grants/tasks/initial.yml new file mode 100644 index 0000000..0d0c0b9 --- /dev/null +++ b/tests/integration/targets/clickhouse_grants/tasks/initial.yml @@ -0,0 +1,48 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Create a test dabase + community.clickhouse.clickhouse_client: + execute: CREATE DATABASE foo + +- name: Create a test table + register: result + community.clickhouse.clickhouse_client: + login_host: localhost + client_kwargs: + connect_timeout: 20 + execute: > + CREATE TABLE IF NOT EXISTS foo.test_table_1 + (id UInt64, x String) ENGINE = Memory + execute_kwargs: + types_check: false + +- name: Insert into test table using named args + register: result + community.clickhouse.clickhouse_client: + execute: "INSERT INTO test_table_1 (x) VALUES (%(a)s), (%(b)s), (%(c)s)" + execute_kwargs: + params: + a: one + b: two + c: three + +- name: Create a test user + community.clickhouse.clickhouse_user: + state: present + name: alice + password: querty + +- name: Check the user exists + register: result + community.clickhouse.clickhouse_info: + login_host: localhost + client_kwargs: + connect_timeout: 20 + +- name: Check user's attributes + ansible.builtin.assert: + that: + - result["users"]["alice"] != {} diff --git a/tests/integration/targets/clickhouse_grants/tasks/main.yml b/tests/integration/targets/clickhouse_grants/tasks/main.yml new file mode 100644 index 0000000..2e0449c --- /dev/null +++ b/tests/integration/targets/clickhouse_grants/tasks/main.yml @@ -0,0 +1,7 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# Initial CI tests of clickhouse_grants module +- import_tasks: initial.yml From a5bc8d21d5c3f08cb4ccf27c232336776497ccc1 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 28 Jan 2025 13:47:12 +0100 Subject: [PATCH 2/9] Fix --- .../targets/clickhouse_grants/tasks/initial.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/clickhouse_grants/tasks/initial.yml b/tests/integration/targets/clickhouse_grants/tasks/initial.yml index 0d0c0b9..db4be73 100644 --- a/tests/integration/targets/clickhouse_grants/tasks/initial.yml +++ b/tests/integration/targets/clickhouse_grants/tasks/initial.yml @@ -5,7 +5,7 @@ - name: Create a test dabase community.clickhouse.clickhouse_client: - execute: CREATE DATABASE foo + execute: CREATE DATABASE IF NOT EXISTS foo - name: Create a test table register: result @@ -14,15 +14,15 @@ client_kwargs: connect_timeout: 20 execute: > - CREATE TABLE IF NOT EXISTS foo.test_table_1 - (id UInt64, x String) ENGINE = Memory + CREATE TABLE IF NOT EXISTS foo.test_table1 + (a String, b String, c String) ENGINE = Memory execute_kwargs: types_check: false - name: Insert into test table using named args register: result community.clickhouse.clickhouse_client: - execute: "INSERT INTO test_table_1 (x) VALUES (%(a)s), (%(b)s), (%(c)s)" + execute: "INSERT INTO test_table1 (x) VALUES (%(a)s), (%(b)s), (%(c)s)" execute_kwargs: params: a: one From f4e759ccde5da69763c07e9d53dd0a184ef8979e Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 28 Jan 2025 13:50:20 +0100 Subject: [PATCH 3/9] Fix --- tests/integration/targets/clickhouse_grants/tasks/initial.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/clickhouse_grants/tasks/initial.yml b/tests/integration/targets/clickhouse_grants/tasks/initial.yml index db4be73..631431a 100644 --- a/tests/integration/targets/clickhouse_grants/tasks/initial.yml +++ b/tests/integration/targets/clickhouse_grants/tasks/initial.yml @@ -22,7 +22,7 @@ - name: Insert into test table using named args register: result community.clickhouse.clickhouse_client: - execute: "INSERT INTO test_table1 (x) VALUES (%(a)s), (%(b)s), (%(c)s)" + execute: "INSERT INTO foo.test_table1 (x) VALUES (%(a)s), (%(b)s), (%(c)s)" execute_kwargs: params: a: one From aa11293f46ebb4f41efbe59fa4903ea95d05e0b7 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 28 Jan 2025 14:04:23 +0100 Subject: [PATCH 4/9] Fix --- tests/integration/targets/clickhouse_grants/tasks/initial.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/clickhouse_grants/tasks/initial.yml b/tests/integration/targets/clickhouse_grants/tasks/initial.yml index 631431a..fc0f718 100644 --- a/tests/integration/targets/clickhouse_grants/tasks/initial.yml +++ b/tests/integration/targets/clickhouse_grants/tasks/initial.yml @@ -15,7 +15,7 @@ connect_timeout: 20 execute: > CREATE TABLE IF NOT EXISTS foo.test_table1 - (a String, b String, c String) ENGINE = Memory + (x String) ENGINE = Memory execute_kwargs: types_check: false From 9af4ce642c1c07da4d8a2aa399856b270fcfbec9 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 29 Jan 2025 14:24:15 +0100 Subject: [PATCH 5/9] Add more tests --- .../clickhouse_grants/tasks/initial.yml | 57 +++++++++++++++++-- .../targets/setup_clickhouse/vars/main.yml | 3 +- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/tests/integration/targets/clickhouse_grants/tasks/initial.yml b/tests/integration/targets/clickhouse_grants/tasks/initial.yml index fc0f718..efafde6 100644 --- a/tests/integration/targets/clickhouse_grants/tasks/initial.yml +++ b/tests/integration/targets/clickhouse_grants/tasks/initial.yml @@ -11,8 +11,6 @@ register: result community.clickhouse.clickhouse_client: login_host: localhost - client_kwargs: - connect_timeout: 20 execute: > CREATE TABLE IF NOT EXISTS foo.test_table1 (x String) ENGINE = Memory @@ -29,6 +27,25 @@ b: two c: three +- name: Create a test table 2 + register: result + community.clickhouse.clickhouse_client: + login_host: localhost + execute: > + CREATE TABLE IF NOT EXISTS foo.test_table2 + (y String, z String) ENGINE = Memory + execute_kwargs: + types_check: false + +- name: Insert into test table using named args + register: result + community.clickhouse.clickhouse_client: + execute: "INSERT INTO foo.test_table2 (y, z) VALUES (%(a)s, %(b)s)" + execute_kwargs: + params: + a: one + b: two + - name: Create a test user community.clickhouse.clickhouse_user: state: present @@ -39,10 +56,38 @@ register: result community.clickhouse.clickhouse_info: login_host: localhost - client_kwargs: - connect_timeout: 20 -- name: Check user's attributes +- name: Check user's grants, they are empty + ansible.builtin.assert: + that: + - result["users"]["alice"]["grants"] == [] + +- name: Grant SELECT on a column WITH GRANT OPTION + register: result + community.clickhouse.clickhouse_client: + execute: GRANT SELECT(x) ON foo.test_table1 TO alice WITH GRANT OPTION + +- name: Grant SELECT on a table + register: result + community.clickhouse.clickhouse_client: + execute: GRANT SELECT ON foo.test_table2 TO alice + +- name: Grant SELECT on a column in the same table + register: result + community.clickhouse.clickhouse_client: + execute: GRANT SELECT(y) ON foo.test_table2 TO alice + +- name: Grant UPDATE on another column in the same table + register: result + community.clickhouse.clickhouse_client: + execute: GRANT UPDATE(z) ON foo.test_table2 TO alice + +- name: Get user info + register: result + community.clickhouse.clickhouse_info: + login_host: localhost + +- name: Check user's grants again, not empty ansible.builtin.assert: that: - - result["users"]["alice"] != {} + - result["users"]["alice"]["grants"] != [] diff --git a/tests/integration/targets/setup_clickhouse/vars/main.yml b/tests/integration/targets/setup_clickhouse/vars/main.yml index 5e2c1a9..7903a81 100644 --- a/tests/integration/targets/setup_clickhouse/vars/main.yml +++ b/tests/integration/targets/setup_clickhouse/vars/main.yml @@ -1 +1,2 @@ -clickhouse_version: 24.8.6.70 +#clickhouse_version: 24.8.6.70 +clickhouse_version: 23.8.9.54 From ad0753814ba4971c4bbed78c9e1ad54937c1066c Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Thu, 30 Jan 2025 14:26:45 +0100 Subject: [PATCH 6/9] Add a module's basic version --- plugins/modules/clickhouse_grants.py | 185 ++++++++++++++++++ .../clickhouse_grants/tasks/initial.yml | 14 ++ 2 files changed, 199 insertions(+) create mode 100644 plugins/modules/clickhouse_grants.py diff --git a/plugins/modules/clickhouse_grants.py b/plugins/modules/clickhouse_grants.py new file mode 100644 index 0000000..5e27d78 --- /dev/null +++ b/plugins/modules/clickhouse_grants.py @@ -0,0 +1,185 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2025, Andrew Klychkov (@Andersson007) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: clickhouse_grants + +short_description: TBD + +description: + - Grants, updates, or removes privileges using the + L(clickhouse-driver,https://clickhouse-driver.readthedocs.io/en/latest) Client interface. + +attributes: + check_mode: + description: Supports check_mode. + support: full + +author: + - Andrew Klychkov (@Andersson007) + +extends_documentation_fragment: + - community.clickhouse.client_inst_opts + +version_added: '0.8.0' + +options: + state: + description: + - User state. + - If C(present), will grant or update privileges. + - If C(absent), will revoke privileges if granted. + type: str + choices: ['present', 'absent'] + default: 'present' + grantee: + description: TBD + type: str + required: true +''' + +EXAMPLES = r''' +- name: TBD Grant privileges + community.clickhouse.clickhouse_user: + login_host: localhost + login_user: alice + login_db: foo + login_password: my_password + grantee: bob + # TBD +''' + +RETURN = r''' +# TBD +executed_statements: + description: + - Data-modifying executed statements. + returned: on success + type: list + sample: ["TBD"] +''' + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.clickhouse.plugins.module_utils.clickhouse import ( + check_clickhouse_driver, + client_common_argument_spec, + connect_to_db_via_client, + execute_query, + get_main_conn_kwargs, +) + +PRIV_ERR_CODE = 497 +executed_statements = [] + + +class ClickHouseGrants(): + def __init__(self, module, client, grantee): + # TODO Maybe move the function determining if the + # user/group exists or not from here to another class? + self.changed = False + self.module = module + self.client = client + self.grantee = grantee + # Set default values, then update + self.grantee_exists = False + # Fetch actual values from DB and + # update the attributes with them + self.__populate_info() + + def __populate_info(self): + # WIP + # TODO Should we check the existence for a group too? + # TODO Should we move it from here to a separate class instead? + # Collecting user information + query = ("SELECT 1 FROM system.users " + "WHERE name = '%s'" % self.grantee) + + result = execute_query(self.module, self.client, query) + + if result == PRIV_ERR_CODE: + login_user = self.module.params['login_user'] + msg = "Not enough privileges for user: %s" % login_user + self.module.fail_json(msg=msg) + + if result != []: + self.grantee_exists = True + + def get(self): + # WIP + return {} + + def update(self): + # WIP + return True + + def revoke(self): + # WIP + return True + + +def main(): + argument_spec = client_common_argument_spec() + argument_spec.update( + state=dict(type='str', choices=['present', 'absent'], default='present'), + grantee=dict(type='str', required=True), + ) + + # Instantiate an object of module class + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + # Assign passed options to variables + client_kwargs = module.params['client_kwargs'] + # The reason why these arguments are separate from client_kwargs + # is that we need to protect some sensitive data like passwords passed + # to the module from logging (see the arguments above with no_log=True); + # Such data must be passed as module arguments (not nested deep in values). + main_conn_kwargs = get_main_conn_kwargs(module) + state = module.params['state'] + grantee = module.params['grantee'] + + # Will fail if no driver informing the user + check_clickhouse_driver(module) + + # Connect to DB + client = connect_to_db_via_client(module, main_conn_kwargs, client_kwargs) + + # Do the job + # TODO Check if the grantee not exits, fail here + changed = False + grants = ClickHouseGrants(module, client, grantee) + # Get current grants + # TODO Should be returned via diff + start_grants = grants.get() + + if state == 'present': + changed = grants.update() + elif state == 'absent': + changed = grants.revoke() + + end_grants = grants.get() + # Close connection + client.disconnect_connection() + + # Users will get this in JSON output after execution + module.exit_json( + changed=changed, + executed_statements=executed_statements, + # TODO Change the below to use diff + start_grants=start_grants, + end_grants=end_grants, + ) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/clickhouse_grants/tasks/initial.yml b/tests/integration/targets/clickhouse_grants/tasks/initial.yml index efafde6..8fabc0e 100644 --- a/tests/integration/targets/clickhouse_grants/tasks/initial.yml +++ b/tests/integration/targets/clickhouse_grants/tasks/initial.yml @@ -91,3 +91,17 @@ ansible.builtin.assert: that: - result["users"]["alice"]["grants"] != [] + +- name: Grant privs + register: result + community.clickhouse.clickhouse_grants: + state: present + grantee: bob + +- name: Check + ansible.builtin.assert: + that: + - result is changed + # TODO Must be updated to use diff + - result["start_grants"] == {} + - result["end_grants"] == {} From 265951b6ee1bbe1b4b8767ffcb018b65a7a3f5b9 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Fri, 31 Jan 2025 13:42:44 +0100 Subject: [PATCH 7/9] Add more grats/revokes --- .../clickhouse_grants/tasks/initial.yml | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/tests/integration/targets/clickhouse_grants/tasks/initial.yml b/tests/integration/targets/clickhouse_grants/tasks/initial.yml index 8fabc0e..06ad73a 100644 --- a/tests/integration/targets/clickhouse_grants/tasks/initial.yml +++ b/tests/integration/targets/clickhouse_grants/tasks/initial.yml @@ -62,25 +62,22 @@ that: - result["users"]["alice"]["grants"] == [] -- name: Grant SELECT on a column WITH GRANT OPTION +- name: Grant privs register: result community.clickhouse.clickhouse_client: - execute: GRANT SELECT(x) ON foo.test_table1 TO alice WITH GRANT OPTION - -- name: Grant SELECT on a table - register: result - community.clickhouse.clickhouse_client: - execute: GRANT SELECT ON foo.test_table2 TO alice - -- name: Grant SELECT on a column in the same table - register: result - community.clickhouse.clickhouse_client: - execute: GRANT SELECT(y) ON foo.test_table2 TO alice - -- name: Grant UPDATE on another column in the same table - register: result - community.clickhouse.clickhouse_client: - execute: GRANT UPDATE(z) ON foo.test_table2 TO alice + execute: '{{ item }}' + with_items: + - GRANT SELECT(x) ON foo.test_table1 TO alice WITH GRANT OPTION + - GRANT SELECT ON foo.test_table2 TO alice + - REVOKE SELECT(y) ON foo.test_table2 FROM alice + - GRANT UPDATE(z) ON foo.test_table2 TO alice + - GRANT DELETE ON foo.test_table2 TO alice + - GRANT CREATE TABLE ON foo.* TO alice + - GRANT CREATE DATABASE ON *.* TO alice + - GRANT KILL QUERY ON *.* TO alice + - GRANT CREATE USER ON *.* TO alice + - GRANT ALTER USER ON *.* TO alice + - GRANT DROP USER ON *.* TO alice - name: Get user info register: result From 1767afd6bf6b06d7e0a2342e5f4a86409a90899f Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 4 Feb 2025 15:41:41 +0100 Subject: [PATCH 8/9] Fail when grantee does not exist --- plugins/modules/clickhouse_grants.py | 2 ++ .../targets/clickhouse_grants/tasks/initial.yml | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/plugins/modules/clickhouse_grants.py b/plugins/modules/clickhouse_grants.py index 5e27d78..eceb05f 100644 --- a/plugins/modules/clickhouse_grants.py +++ b/plugins/modules/clickhouse_grants.py @@ -111,6 +111,8 @@ def __populate_info(self): if result != []: self.grantee_exists = True + else: + self.module.fail_json(msg="Grantee %s does not exist" % self.grantee) def get(self): # WIP diff --git a/tests/integration/targets/clickhouse_grants/tasks/initial.yml b/tests/integration/targets/clickhouse_grants/tasks/initial.yml index 06ad73a..2fcbf68 100644 --- a/tests/integration/targets/clickhouse_grants/tasks/initial.yml +++ b/tests/integration/targets/clickhouse_grants/tasks/initial.yml @@ -89,11 +89,24 @@ that: - result["users"]["alice"]["grants"] != [] +- name: Grant privs to non-existent grantee, must fail + register: result + ignore_errors: true + community.clickhouse.clickhouse_grants: + state: present + grantee: notexists + +- name: Check that it failed + ansible.builtin.assert: + that: + - result is failed + - result.msg is search("does not exist") + - name: Grant privs register: result community.clickhouse.clickhouse_grants: state: present - grantee: bob + grantee: alice - name: Check ansible.builtin.assert: From 1d395b45c5969acac3f9cb6199c4d1048576f72b Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 5 Feb 2025 14:47:26 +0100 Subject: [PATCH 9/9] change bob to alice in the example --- plugins/modules/clickhouse_grants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/clickhouse_grants.py b/plugins/modules/clickhouse_grants.py index eceb05f..30bea99 100644 --- a/plugins/modules/clickhouse_grants.py +++ b/plugins/modules/clickhouse_grants.py @@ -52,7 +52,7 @@ login_user: alice login_db: foo login_password: my_password - grantee: bob + grantee: alice # TBD '''