Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] clickhouse_grants: new module #94

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions plugins/modules/clickhouse_grants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2025, Andrew Klychkov (@Andersson007) <[email protected]>
# 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: alice
# 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
else:
self.module.fail_json(msg="Grantee %s does not exist" % self.grantee)

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()
2 changes: 2 additions & 0 deletions tests/integration/targets/clickhouse_grants/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- setup_clickhouse
117 changes: 117 additions & 0 deletions tests/integration/targets/clickhouse_grants/tasks/initial.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
####################################################################
# 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 IF NOT EXISTS foo

- name: Create a test table
register: result
community.clickhouse.clickhouse_client:
login_host: localhost
execute: >
CREATE TABLE IF NOT EXISTS foo.test_table1
(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 foo.test_table1 (x) VALUES (%(a)s), (%(b)s), (%(c)s)"
execute_kwargs:
params:
a: one
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
name: alice
password: querty

- name: Check the user exists
register: result
community.clickhouse.clickhouse_info:
login_host: localhost

- name: Check user's grants, they are empty
ansible.builtin.assert:
that:
- result["users"]["alice"]["grants"] == []

- name: Grant privs
register: result
community.clickhouse.clickhouse_client:
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
community.clickhouse.clickhouse_info:
login_host: localhost

- name: Check user's grants again, not empty
ansible.builtin.assert:
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: alice

- name: Check
ansible.builtin.assert:
that:
- result is changed
# TODO Must be updated to use diff
- result["start_grants"] == {}
- result["end_grants"] == {}
7 changes: 7 additions & 0 deletions tests/integration/targets/clickhouse_grants/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion tests/integration/targets/setup_clickhouse/vars/main.yml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
clickhouse_version: 24.8.6.70
#clickhouse_version: 24.8.6.70
clickhouse_version: 23.8.9.54