WIP attempt to retrieve all users privileges

This commit is contained in:
Laurent Indermuehle 2023-09-11 19:28:26 +02:00
parent 033b4c74f9
commit d7beeec410
No known key found for this signature in database
GPG key ID: 93FA944C9F34DD09
6 changed files with 307 additions and 238 deletions

View file

@ -50,55 +50,63 @@ test-integration:
--health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ --health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \
docker.io/library/$(db_engine_name):$(db_engine_version) \ docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld mysqld
podman run \ # podman run \
--detach \ # --detach \
--replace \ # --replace \
--name replica1 \ # --name replica1 \
--env MARIADB_ROOT_PASSWORD=msandbox \ # --env MARIADB_ROOT_PASSWORD=msandbox \
--env MYSQL_ROOT_PASSWORD=msandbox \ # --env MYSQL_ROOT_PASSWORD=msandbox \
--network podman \ # --network podman \
--publish 3308:3306 \ # --publish 3308:3306 \
--health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ # --health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \
docker.io/library/$(db_engine_name):$(db_engine_version) \ # docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld # mysqld
podman run \ # podman run \
--detach \ # --detach \
--replace \ # --replace \
--name replica2 \ # --name replica2 \
--env MARIADB_ROOT_PASSWORD=msandbox \ # --env MARIADB_ROOT_PASSWORD=msandbox \
--env MYSQL_ROOT_PASSWORD=msandbox \ # --env MYSQL_ROOT_PASSWORD=msandbox \
--network podman \ # --network podman \
--publish 3309:3306 \ # --publish 3309:3306 \
--health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ # --health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \
docker.io/library/$(db_engine_name):$(db_engine_version) \ # docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld # mysqld
# Setup replication and restart containers # # Setup replication and restart containers
podman exec primary bash -c 'echo -e [mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin > /etc/mysql/conf.d/replication.cnf' # podman exec primary bash -c 'echo -e [mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin > /etc/mysql/conf.d/replication.cnf'
podman exec replica1 bash -c 'echo -e [mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin > /etc/mysql/conf.d/replication.cnf' # podman exec replica1 bash -c 'echo -e [mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin > /etc/mysql/conf.d/replication.cnf'
podman exec replica2 bash -c 'echo -e [mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin > /etc/mysql/conf.d/replication.cnf' # podman exec replica2 bash -c 'echo -e [mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin > /etc/mysql/conf.d/replication.cnf'
# Don't restart a container unless it is healthy # # Don't restart a container unless it is healthy
while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done # while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
podman restart -t 30 primary # podman restart -t 30 primary
while ! podman healthcheck run replica1 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done # while ! podman healthcheck run replica1 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
podman restart -t 30 replica1 # podman restart -t 30 replica1
while ! podman healthcheck run replica2 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done # while ! podman healthcheck run replica2 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
podman restart -t 30 replica2 # podman restart -t 30 replica2
while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done # while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
mkdir -p .venv/$(ansible) # mkdir -p .venv/$(ansible)
python$(local_python_version) -m venv .venv/$(ansible) # python$(local_python_version) -m venv .venv/$(ansible)
# Start venv (use `; \` to keep the same shell) # Start venv (use `; \` to keep the same shell)
# source .venv/$(ansible)/bin/activate; \
# python$(local_python_version) -m ensurepip; \
# python$(local_python_version) -m pip install --disable-pip-version-check \
# https://github.com/ansible/ansible/archive/$(ansible).tar.gz; \
# set -x; \
# ansible-test integration $(target) -v --color --coverage --diff \
# --docker ghcr.io/ansible-collections/community.mysql/test-container\
# -$(db_client)-py$(python_version_flat)-$(connector_name)$(connector_version_flat):latest \
# --docker-network podman $(_continue_on_errors) $(_keep_containers_alive) --python $(python); \
# set +x
# End of venv
source .venv/$(ansible)/bin/activate; \ source .venv/$(ansible)/bin/activate; \
python$(local_python_version) -m ensurepip; \
python$(local_python_version) -m pip install --disable-pip-version-check \
https://github.com/ansible/ansible/archive/$(ansible).tar.gz; \
set -x; \ set -x; \
ansible-test integration $(target) -v --color --coverage --diff \ ansible-test integration $(target) -v --color --coverage --diff \
--docker ghcr.io/ansible-collections/community.mysql/test-container\ --docker ghcr.io/ansible-collections/community.mysql/test-container\
-$(db_client)-py$(python_version_flat)-$(connector_name)$(connector_version_flat):latest \ -$(db_client)-py$(python_version_flat)-$(connector_name)$(connector_version_flat):latest \
--docker-network podman $(_continue_on_errors) $(_keep_containers_alive) --python $(python); \ --docker-network podman $(_continue_on_errors) $(_keep_containers_alive) --python $(python); \
set +x set +x
# End of venv
rm tests/integration/db_engine_name rm tests/integration/db_engine_name
rm tests/integration/db_engine_version rm tests/integration/db_engine_version

View file

@ -104,9 +104,13 @@ def get_tls_requires(cursor, user, host):
return requires or None return requires or None
def get_grants(cursor, user, host): def get_grants(module, cursor, user, host):
cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) cursor.execute("SHOW GRANTS FOR %s@%s", (user, host))
grants_line = list(filter(lambda x: "ON *.*" in x[0], cursor.fetchall()))[0] try:
grants_line = list(filter(lambda x: "ON *.*" in x[0], cursor.fetchall()))[0]
except Exception as e:
module.fail_json(msg="Error %s" % e)
pattern = r"(?<=\bGRANT\b)(.*?)(?=(?:\bON\b))" pattern = r"(?<=\bGRANT\b)(.*?)(?=(?:\bON\b))"
grants = re.search(pattern, grants_line[0]).group().strip() grants = re.search(pattern, grants_line[0]).group().strip()
return grants.split(", ") return grants.split(", ")
@ -132,7 +136,7 @@ def get_existing_authentication(cursor, user):
return None return None
def user_add(cursor, user, host, host_all, password, encrypted, def user_add(module, cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, new_priv, plugin, plugin_hash_string, plugin_auth_string, new_priv,
tls_requires, check_mode, reuse_existing_password): tls_requires, check_mode, reuse_existing_password):
# we cannot create users without a proper hostname # we cannot create users without a proper hostname
@ -187,7 +191,7 @@ def user_add(cursor, user, host, host_all, password, encrypted,
for db_table, priv in iteritems(new_priv): for db_table, priv in iteritems(new_priv):
privileges_grant(cursor, user, host, db_table, priv, tls_requires) privileges_grant(cursor, user, host, db_table, priv, tls_requires)
if tls_requires is not None: if tls_requires is not None:
privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires) privileges_grant(cursor, user, host, "*.*", get_grants(module, cursor, user, host), tls_requires)
return {'changed': True, 'password_changed': not used_existing_password} return {'changed': True, 'password_changed': not used_existing_password}

View file

@ -19,7 +19,7 @@ options:
description: description:
- Limit the collected information by comma separated string or YAML list. - Limit the collected information by comma separated string or YAML list.
- Allowable values are C(version), C(databases), C(settings), C(global_status), - Allowable values are C(version), C(databases), C(settings), C(global_status),
C(users), C(engines), C(master_status), C(slave_status), C(slave_hosts). C(users), C(users_privs), C(engines), C(master_status), C(slave_status), C(slave_hosts).
- By default, collects all subsets. - By default, collects all subsets.
- You can use '!' before value (for example, C(!settings)) to exclude it from the information. - You can use '!' before value (for example, C(!settings)) to exclude it from the information.
- If you pass including and excluding values to the filter, for example, I(filter=!settings,version), - If you pass including and excluding values to the filter, for example, I(filter=!settings,version),
@ -74,6 +74,9 @@ EXAMPLES = r'''
# Display only databases and users info: # Display only databases and users info:
# ansible mysql-hosts -m mysql_info -a 'filter=databases,users' # ansible mysql-hosts -m mysql_info -a 'filter=databases,users'
# Display all users privileges:
# ansible mysql-hosts -m mysql_info -a 'filter=users_privs'
# Display only slave status: # Display only slave status:
# ansible standby -m mysql_info -a 'filter=slave_status' # ansible standby -m mysql_info -a 'filter=slave_status'
@ -186,6 +189,12 @@ users:
type: dict type: dict
sample: sample:
- { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } } - { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } }
users_privs:
description: Users privileges.
returned: if not excluded by filter
type: dict
sample:
- { "name": "user1", "host": "host.com", "priv": 'db1.*':'ALL' 'db2.tb1':'SELECT', pass_hash: '*1234567', resource_limits: { MAX_USER_CONNECTIONS: 100 } }
engines: engines:
description: Information about the server's storage engines. description: Information about the server's storage engines.
returned: if not excluded by filter returned: if not excluded by filter
@ -237,6 +246,10 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_driver_fail_msg, mysql_driver_fail_msg,
get_connector_name, get_connector_name,
get_connector_version, get_connector_version,
get_server_version,
)
from ansible_collections.community.mysql.plugins.module_utils.user import (
get_grants,
) )
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
@ -274,6 +287,7 @@ class MySQL_Info(object):
'global_status': {}, 'global_status': {},
'engines': {}, 'engines': {},
'users': {}, 'users': {},
'users_privs': {},
'master_status': {}, 'master_status': {},
'slave_hosts': {}, 'slave_hosts': {},
'slave_status': {}, 'slave_status': {},
@ -342,6 +356,9 @@ class MySQL_Info(object):
if 'users' in wanted: if 'users' in wanted:
self.__get_users() self.__get_users()
if 'users_privs' in wanted:
self.__get_users_privs()
if 'master_status' in wanted: if 'master_status' in wanted:
self.__get_master_status() self.__get_master_status()
@ -480,6 +497,29 @@ class MySQL_Info(object):
if vname not in ('Host', 'User'): if vname not in ('Host', 'User'):
self.info['users'][host][user][vname] = self.__convert(val) self.info['users'][host][user][vname] = self.__convert(val)
def __get_users_privs(self):
"""Get user privileges."""
try:
user = self.__exec_sql('SELECT * FROM mysql.user')
except Exception as e:
self.fail_json(
msg="mysql_info failed to retrieve the users: %s" % e)
for line in user:
u = line['User']
h = line['Host']
key = u + '_' + h
privs = get_grants(self.module, self.cursor, u, h)
if not privs:
self.module.warn(
'Fail to get privileges for user %s on host %s.' % (u, h))
privs = {}
self.info['users_privs'][key] = {
'user': u, 'host': h, 'privs': privs}
def __get_databases(self, exclude_fields, return_empty_dbs): def __get_databases(self, exclude_fields, return_empty_dbs):
"""Get info about databases.""" """Get info about databases."""
if not exclude_fields: if not exclude_fields:

View file

@ -525,7 +525,7 @@ def main():
if subtract_privs: if subtract_privs:
priv = None # avoid granting unwanted privileges priv = None # avoid granting unwanted privileges
reuse_existing_password = update_password == 'on_new_username' reuse_existing_password = update_password == 'on_new_username'
result = user_add(cursor, user, host, host_all, password, encrypted, result = user_add(module, cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, plugin, plugin_hash_string, plugin_auth_string,
priv, tls_requires, module.check_mode, reuse_existing_password) priv, tls_requires, module.check_mode, reuse_existing_password)
changed = result['changed'] changed = result['changed']

View file

@ -0,0 +1,13 @@
---
- name: Collect info about users_privs
community.mysql.mysql_info:
login_user: '{{ mysql_user }}'
login_password: '{{ mysql_password }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
filter:
- users_privs
register: result
- debug: var=result

View file

@ -21,201 +21,205 @@
block: block:
# Create default MySQL config file with credentials # # Create default MySQL config file with credentials
- name: mysql_info - create default config file # - name: mysql_info - create default config file
template: # template:
src: my.cnf.j2 # src: my.cnf.j2
dest: "{{ playbook_dir }}/root/.my.cnf" # dest: "{{ playbook_dir }}/root/.my.cnf"
mode: '0400' # mode: '0400'
# Create non-default MySQL config file with credentials # # Create non-default MySQL config file with credentials
- name: mysql_info - create non-default config file # - name: mysql_info - create non-default config file
template: # template:
src: my.cnf.j2 # src: my.cnf.j2
dest: "{{ playbook_dir }}/root/non-default_my.cnf" # dest: "{{ playbook_dir }}/root/non-default_my.cnf"
mode: '0400' # mode: '0400'
############### # ###############
# Do tests # # Do tests
# Access by default cred file # # Access by default cred file
- name: mysql_info - collect default cred file # - name: mysql_info - collect default cred file
mysql_info: # mysql_info:
login_user: '{{ mysql_user }}' # login_user: '{{ mysql_user }}'
login_host: '{{ mysql_host }}' # login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}' # login_port: '{{ mysql_primary_port }}'
config_file: "{{ playbook_dir }}/root/.my.cnf" # config_file: "{{ playbook_dir }}/root/.my.cnf"
register: result # register: result
- assert: # - assert:
that: # that:
- result is not changed # - result is not changed
- db_version in result.version.full # - db_version in result.version.full
- result.settings != {} # - result.settings != {}
- result.global_status != {} # - result.global_status != {}
- result.databases != {} # - result.databases != {}
- result.engines != {} # - result.engines != {}
- result.users != {} # - result.users != {}
- name: mysql_info - Test connector informations display # - name: mysql_info - Test connector informations display
# ansible.builtin.import_tasks:
# file: connector_info.yml
# # Access by non-default cred file
# - name: mysql_info - check non-default cred file
# mysql_info:
# login_user: '{{ mysql_user }}'
# login_host: '{{ mysql_host }}'
# login_port: '{{ mysql_primary_port }}'
# config_file: "{{ playbook_dir }}/root/non-default_my.cnf"
# register: result
# - assert:
# that:
# - result is not changed
# - result.version != {}
# # Remove cred files
# - name: mysql_info - remove cred files
# file:
# path: '{{ item }}'
# state: absent
# loop:
# - "{{ playbook_dir }}/.my.cnf"
# - "{{ playbook_dir }}/non-default_my.cnf"
# # Access with password
# - name: mysql_info - check access with password
# mysql_info:
# <<: *mysql_params
# register: result
# - assert:
# that:
# - result is not changed
# - result.version != {}
# # Test excluding
# - name: Collect all info except settings and users
# mysql_info:
# <<: *mysql_params
# filter: '!settings,!users'
# register: result
# - assert:
# that:
# - result is not changed
# - result.version != {}
# - result.global_status != {}
# - result.databases != {}
# - result.engines != {}
# - result.settings is not defined
# - result.users is not defined
# # Test including
# - name: Collect info only about version and databases
# mysql_info:
# <<: *mysql_params
# filter:
# - version
# - databases
# register: result
# - assert:
# that:
# - result is not changed
# - result.version != {}
# - result.databases != {}
# - result.engines is not defined
# - result.settings is not defined
# - result.global_status is not defined
# - result.users is not defined
# # Test exclude_fields: db_size
# # 'unsupported' element is passed to check that an unsupported value
# # won't break anything (will be ignored regarding to the module's documentation).
# - name: Collect info about databases excluding their sizes
# mysql_info:
# <<: *mysql_params
# filter:
# - databases
# exclude_fields:
# - db_size
# - unsupported
# register: result
# - assert:
# that:
# - result is not changed
# - result.databases != {}
# - result.databases.mysql == {}
# ########################################################
# # Issue #65727, empty databases must be in returned dict
# #
# - name: Create empty database acme
# mysql_db:
# <<: *mysql_params
# name: acme
# - name: Collect info about databases
# mysql_info:
# <<: *mysql_params
# filter:
# - databases
# return_empty_dbs: true
# register: result
# # Check acme is in returned dict
# - assert:
# that:
# - result is not changed
# - result.databases.acme.size == 0
# - result.databases.mysql != {}
# - name: Collect info about databases excluding their sizes
# mysql_info:
# <<: *mysql_params
# filter:
# - databases
# exclude_fields:
# - db_size
# return_empty_dbs: true
# register: result
# # Check acme is in returned dict
# - assert:
# that:
# - result is not changed
# - result.databases.acme == {}
# - result.databases.mysql == {}
# - name: Remove acme database
# mysql_db:
# <<: *mysql_params
# name: acme
# state: absent
# - include_tasks: issue-28.yml
# # https://github.com/ansible-collections/community.mysql/issues/204
# - name: Create database containing only views
# mysql_db:
# <<: *mysql_params
# name: allviews
# - name: Create view
# mysql_query:
# <<: *mysql_params
# login_db: allviews
# query: 'CREATE VIEW v_today (today) AS SELECT CURRENT_DATE'
# - name: Fetch info
# mysql_info:
# <<: *mysql_params
# register: result
# - name: Check
# assert:
# that:
# - result.databases.allviews.size == 0
- name: Inport tasks file to tests users_privs filter
ansible.builtin.import_tasks: ansible.builtin.import_tasks:
file: connector_info.yml file: filter_users_privs.yml
# Access by non-default cred file
- name: mysql_info - check non-default cred file
mysql_info:
login_user: '{{ mysql_user }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
config_file: "{{ playbook_dir }}/root/non-default_my.cnf"
register: result
- assert:
that:
- result is not changed
- result.version != {}
# Remove cred files
- name: mysql_info - remove cred files
file:
path: '{{ item }}'
state: absent
loop:
- "{{ playbook_dir }}/.my.cnf"
- "{{ playbook_dir }}/non-default_my.cnf"
# Access with password
- name: mysql_info - check access with password
mysql_info:
<<: *mysql_params
register: result
- assert:
that:
- result is not changed
- result.version != {}
# Test excluding
- name: Collect all info except settings and users
mysql_info:
<<: *mysql_params
filter: '!settings,!users'
register: result
- assert:
that:
- result is not changed
- result.version != {}
- result.global_status != {}
- result.databases != {}
- result.engines != {}
- result.settings is not defined
- result.users is not defined
# Test including
- name: Collect info only about version and databases
mysql_info:
<<: *mysql_params
filter:
- version
- databases
register: result
- assert:
that:
- result is not changed
- result.version != {}
- result.databases != {}
- result.engines is not defined
- result.settings is not defined
- result.global_status is not defined
- result.users is not defined
# Test exclude_fields: db_size
# 'unsupported' element is passed to check that an unsupported value
# won't break anything (will be ignored regarding to the module's documentation).
- name: Collect info about databases excluding their sizes
mysql_info:
<<: *mysql_params
filter:
- databases
exclude_fields:
- db_size
- unsupported
register: result
- assert:
that:
- result is not changed
- result.databases != {}
- result.databases.mysql == {}
########################################################
# Issue #65727, empty databases must be in returned dict
#
- name: Create empty database acme
mysql_db:
<<: *mysql_params
name: acme
- name: Collect info about databases
mysql_info:
<<: *mysql_params
filter:
- databases
return_empty_dbs: true
register: result
# Check acme is in returned dict
- assert:
that:
- result is not changed
- result.databases.acme.size == 0
- result.databases.mysql != {}
- name: Collect info about databases excluding their sizes
mysql_info:
<<: *mysql_params
filter:
- databases
exclude_fields:
- db_size
return_empty_dbs: true
register: result
# Check acme is in returned dict
- assert:
that:
- result is not changed
- result.databases.acme == {}
- result.databases.mysql == {}
- name: Remove acme database
mysql_db:
<<: *mysql_params
name: acme
state: absent
- include_tasks: issue-28.yml
# https://github.com/ansible-collections/community.mysql/issues/204
- name: Create database containing only views
mysql_db:
<<: *mysql_params
name: allviews
- name: Create view
mysql_query:
<<: *mysql_params
login_db: allviews
query: 'CREATE VIEW v_today (today) AS SELECT CURRENT_DATE'
- name: Fetch info
mysql_info:
<<: *mysql_params
register: result
- name: Check
assert:
that:
- result.databases.allviews.size == 0