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' \
docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld
podman run \
--detach \
--replace \
--name replica1 \
--env MARIADB_ROOT_PASSWORD=msandbox \
--env MYSQL_ROOT_PASSWORD=msandbox \
--network podman \
--publish 3308:3306 \
--health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \
docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld
podman run \
--detach \
--replace \
--name replica2 \
--env MARIADB_ROOT_PASSWORD=msandbox \
--env MYSQL_ROOT_PASSWORD=msandbox \
--network podman \
--publish 3309:3306 \
--health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \
docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld
# 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 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'
# Don't restart a container unless it is healthy
while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
podman restart -t 30 primary
while ! podman healthcheck run replica1 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
podman restart -t 30 replica1
while ! podman healthcheck run replica2 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
podman restart -t 30 replica2
while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
mkdir -p .venv/$(ansible)
python$(local_python_version) -m venv .venv/$(ansible)
# podman run \
# --detach \
# --replace \
# --name replica1 \
# --env MARIADB_ROOT_PASSWORD=msandbox \
# --env MYSQL_ROOT_PASSWORD=msandbox \
# --network podman \
# --publish 3308:3306 \
# --health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \
# docker.io/library/$(db_engine_name):$(db_engine_version) \
# mysqld
# podman run \
# --detach \
# --replace \
# --name replica2 \
# --env MARIADB_ROOT_PASSWORD=msandbox \
# --env MYSQL_ROOT_PASSWORD=msandbox \
# --network podman \
# --publish 3309:3306 \
# --health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \
# docker.io/library/$(db_engine_name):$(db_engine_version) \
# mysqld
# # 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 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'
# # Don't restart a container unless it is healthy
# while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
# podman restart -t 30 primary
# while ! podman healthcheck run replica1 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
# podman restart -t 30 replica1
# while ! podman healthcheck run replica2 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
# podman restart -t 30 replica2
# while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
# mkdir -p .venv/$(ansible)
# python$(local_python_version) -m venv .venv/$(ansible)
# 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; \
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
rm tests/integration/db_engine_name
rm tests/integration/db_engine_version

View file

@ -104,9 +104,13 @@ def get_tls_requires(cursor, user, host):
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))
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))"
grants = re.search(pattern, grants_line[0]).group().strip()
return grants.split(", ")
@ -132,7 +136,7 @@ def get_existing_authentication(cursor, user):
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,
tls_requires, check_mode, reuse_existing_password):
# 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):
privileges_grant(cursor, user, host, db_table, priv, tls_requires)
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}

View file

@ -19,7 +19,7 @@ options:
description:
- Limit the collected information by comma separated string or YAML list.
- 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.
- 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),
@ -74,6 +74,9 @@ EXAMPLES = r'''
# Display only databases and users info:
# 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:
# ansible standby -m mysql_info -a 'filter=slave_status'
@ -186,6 +189,12 @@ users:
type: dict
sample:
- { "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:
description: Information about the server's storage engines.
returned: if not excluded by filter
@ -237,6 +246,10 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_driver_fail_msg,
get_connector_name,
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._text import to_native
@ -274,6 +287,7 @@ class MySQL_Info(object):
'global_status': {},
'engines': {},
'users': {},
'users_privs': {},
'master_status': {},
'slave_hosts': {},
'slave_status': {},
@ -342,6 +356,9 @@ class MySQL_Info(object):
if 'users' in wanted:
self.__get_users()
if 'users_privs' in wanted:
self.__get_users_privs()
if 'master_status' in wanted:
self.__get_master_status()
@ -480,6 +497,29 @@ class MySQL_Info(object):
if vname not in ('Host', 'User'):
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):
"""Get info about databases."""
if not exclude_fields:

View file

@ -525,7 +525,7 @@ def main():
if subtract_privs:
priv = None # avoid granting unwanted privileges
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,
priv, tls_requires, module.check_mode, reuse_existing_password)
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:
# Create default MySQL config file with credentials
- name: mysql_info - create default config file
template:
src: my.cnf.j2
dest: "{{ playbook_dir }}/root/.my.cnf"
mode: '0400'
# # Create default MySQL config file with credentials
# - name: mysql_info - create default config file
# template:
# src: my.cnf.j2
# dest: "{{ playbook_dir }}/root/.my.cnf"
# mode: '0400'
# Create non-default MySQL config file with credentials
- name: mysql_info - create non-default config file
template:
src: my.cnf.j2
dest: "{{ playbook_dir }}/root/non-default_my.cnf"
mode: '0400'
# # Create non-default MySQL config file with credentials
# - name: mysql_info - create non-default config file
# template:
# src: my.cnf.j2
# dest: "{{ playbook_dir }}/root/non-default_my.cnf"
# mode: '0400'
###############
# Do tests
# ###############
# # Do tests
# Access by default cred file
- name: mysql_info - collect default cred file
mysql_info:
login_user: '{{ mysql_user }}'
login_host: '{{ mysql_host }}'
login_port: '{{ mysql_primary_port }}'
config_file: "{{ playbook_dir }}/root/.my.cnf"
register: result
# # Access by default cred file
# - name: mysql_info - collect default cred file
# mysql_info:
# login_user: '{{ mysql_user }}'
# login_host: '{{ mysql_host }}'
# login_port: '{{ mysql_primary_port }}'
# config_file: "{{ playbook_dir }}/root/.my.cnf"
# register: result
- assert:
that:
- result is not changed
- db_version in result.version.full
- result.settings != {}
- result.global_status != {}
- result.databases != {}
- result.engines != {}
- result.users != {}
# - assert:
# that:
# - result is not changed
# - db_version in result.version.full
# - result.settings != {}
# - result.global_status != {}
# - result.databases != {}
# - result.engines != {}
# - 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:
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
file: filter_users_privs.yml