diff --git a/changelogs/fragments/60638-postgresql_user_add_groups_parameter.yml b/changelogs/fragments/60638-postgresql_user_add_groups_parameter.yml new file mode 100644 index 00000000000..ea77a74b61e --- /dev/null +++ b/changelogs/fragments/60638-postgresql_user_add_groups_parameter.yml @@ -0,0 +1,2 @@ +minor_changes: +- postgresql_user - Add the new parameter ``groups`` (https://github.com/ansible/ansible/pull/60638). diff --git a/lib/ansible/modules/database/postgresql/postgresql_user.py b/lib/ansible/modules/database/postgresql/postgresql_user.py index 95f97ee4b82..b1b4483f81e 100644 --- a/lib/ansible/modules/database/postgresql/postgresql_user.py +++ b/lib/ansible/modules/database/postgresql/postgresql_user.py @@ -147,6 +147,11 @@ options: type: str aliases: [ ssl_rootcert ] version_added: '2.3' + groups: + description: + - The list of groups (roles) that need to be granted to the user. + type: list + version_added: '2.9' notes: - The module creates a user (role) with login privilege by default. Use NOLOGIN role_attr_flags to change this behaviour. @@ -205,6 +210,13 @@ EXAMPLES = r''' db: test user: test password: "" + +- name: Create user test and grant group user_ro and user_rw to it + postgresql_user: + name: test + groups: + - user_ro + - user_rw ''' RETURN = r''' @@ -233,6 +245,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.database import pg_quote_identifier, SQLParseError from ansible.module_utils.postgres import ( connect_to_db, + exec_sql, get_conn_params, postgres_common_argument_spec, ) @@ -750,6 +763,91 @@ def get_valid_flags_by_version(cursor): ] +class PgMembership(): + def __init__(self, module, cursor, target_roles, groups, fail_on_role=True): + self.module = module + self.cursor = cursor + self.target_roles = [r.strip() for r in target_roles] + self.groups = groups + self.granted = {} + self.fail_on_role = fail_on_role + self.non_existent_roles = [] + self.changed = False + self.__check_roles_exist() + + def grant(self): + for group in self.groups: + self.granted[group] = [] + + for role in self.target_roles: + # If role is in a group now, pass: + if self.__check_membership(group, role): + continue + + query = "GRANT %s TO %s" % ((pg_quote_identifier(group, 'role'), + (pg_quote_identifier(role, 'role')))) + self.changed = exec_sql(self, query, ddl=True, add_to_executed=False) + executed_queries.append(query) + + if self.changed: + self.granted[group].append(role) + + return self.changed + + def __check_membership(self, src_role, dst_role): + query = ("SELECT ARRAY(SELECT b.rolname FROM " + "pg_catalog.pg_auth_members m " + "JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) " + "WHERE m.member = r.oid) " + "FROM pg_catalog.pg_roles r " + "WHERE r.rolname = '%s'" % dst_role) + + res = exec_sql(self, query, add_to_executed=False) + membership = [] + if res: + membership = res[0][0] + + if not membership: + return False + + if src_role in membership: + return True + + return False + + def __check_roles_exist(self): + for group in self.groups: + if not self.__role_exists(group): + if self.fail_on_role: + self.module.fail_json(msg="Role %s does not exist" % group) + else: + self.module.warn("Role %s does not exist, pass" % group) + self.non_existent_roles.append(group) + + for role in self.target_roles: + if not self.__role_exists(role): + if self.fail_on_role: + self.module.fail_json(msg="Role %s does not exist" % role) + else: + self.module.warn("Role %s does not exist, pass" % role) + + if role not in self.groups: + self.non_existent_roles.append(role) + + else: + if self.fail_on_role: + self.module.exit_json(msg="Role role '%s' is a member of role '%s'" % (role, role)) + else: + self.module.warn("Role role '%s' is a member of role '%s', pass" % (role, role)) + + # Update role lists, excluding non existent roles: + self.groups = [g for g in self.groups if g not in self.non_existent_roles] + + self.target_roles = [r for r in self.target_roles if r not in self.non_existent_roles] + + def __role_exists(self, role): + return exec_sql(self, "SELECT 1 FROM pg_roles WHERE rolname = '%s'" % role, add_to_executed=False) + # =========================================== # Module execution. # @@ -770,6 +868,7 @@ def main(): expires=dict(type='str', default=None), conn_limit=dict(type='int', default=None), session_role=dict(type='str'), + groups=dict(type='list'), ) module = AnsibleModule( argument_spec=argument_spec, @@ -791,6 +890,9 @@ def main(): expires = module.params["expires"] conn_limit = module.params["conn_limit"] role_attr_flags = module.params["role_attr_flags"] + groups = module.params["groups"] + if groups: + groups = [e.strip() for e in groups] conn_params = get_conn_params(module, module.params, warn_db_default=False) db_connection = connect_to_db(module, conn_params) @@ -826,6 +928,13 @@ def main(): changed = grant_privileges(cursor, user, privs) or changed except SQLParseError as e: module.fail_json(msg=to_native(e), exception=traceback.format_exc()) + + if groups: + target_roles = [] + target_roles.append(user) + pg_membership = PgMembership(module, cursor, target_roles, groups) + changed = pg_membership.grant() + else: if user_exists(cursor, user): if module.check_mode: diff --git a/test/integration/targets/postgresql/tasks/postgresql_user.yml b/test/integration/targets/postgresql/tasks/postgresql_user.yml index 36a93465722..aae57b2429c 100644 --- a/test/integration/targets/postgresql/tasks/postgresql_user.yml +++ b/test/integration/targets/postgresql/tasks/postgresql_user.yml @@ -4,6 +4,8 @@ - vars: test_user: hello_user + test_group1: group1 + test_group2: group2 test_table: test task_parameters: &task_parameters become_user: '{{ pg_user }}' @@ -519,6 +521,132 @@ that: - result is not changed + # + # Test groups parameter + # + - name: Create test group + <<: *task_parameters + postgresql_user: + <<: *pg_parameters + name: '{{ test_group2 }}' + role_attr_flags: NOLOGIN + + - name: Create role test_group1 and grant test_group2 to test_group1 in check_mode + <<: *task_parameters + postgresql_user: + <<: *pg_parameters + name: '{{ test_group1 }}' + groups: '{{ test_group2 }}' + role_attr_flags: NOLOGIN + check_mode: yes + + - assert: + that: + - result is changed + - result.user == '{{ test_group1 }}' + - result.queries == ['CREATE USER "{{ test_group1 }}" NOLOGIN', 'GRANT "{{ test_group2 }}" TO "{{ test_group1 }}"'] + + - name: check that the user doesn't exist + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT rolname FROM pg_roles WHERE rolname = '{{ test_group1 }}'" + + - assert: + that: + - result.rowcount == 0 + + - name: check membership + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT grolist FROM pg_group WHERE groname = '{{ test_group2 }}' AND grolist != '{}'" + + - assert: + that: + - result.rowcount == 0 + + - name: Create role test_group1 and grant test_group2 to test_group1 + <<: *task_parameters + postgresql_user: + <<: *pg_parameters + name: '{{ test_group1 }}' + groups: '{{ test_group2 }}' + role_attr_flags: NOLOGIN + + - assert: + that: + - result is changed + - result.user == '{{ test_group1 }}' + - result.queries == ['CREATE USER "{{ test_group1 }}" NOLOGIN', 'GRANT "{{ test_group2 }}" TO "{{ test_group1 }}"'] + + - name: check that the user exists + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT rolname FROM pg_roles WHERE rolname = '{{ test_group1 }}'" + + - assert: + that: + - result.rowcount == 1 + + - name: check membership + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT grolist FROM pg_group WHERE groname = '{{ test_group2 }}' AND grolist != '{}'" + + - assert: + that: + - result.rowcount == 1 + + - name: Grant test_group2 to test_group1 again + <<: *task_parameters + postgresql_user: + <<: *pg_parameters + name: '{{ test_group1 }}' + groups: '{{ test_group2 }}' + + - assert: + that: + - result is not changed + - result.user == '{{ test_group1 }}' + + - name: check membership + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT grolist FROM pg_group WHERE groname = '{{ test_group2 }}' AND grolist != '{}'" + + - assert: + that: + - result.rowcount == 1 + + - name: Grant groups to existent role + <<: *task_parameters + postgresql_user: + <<: *pg_parameters + name: '{{ test_user }}' + groups: + - '{{ test_group1 }}' + - '{{ test_group2 }}' + + - assert: + that: + - result is changed + - result.user == '{{ test_user }}' + - result.queries == ['GRANT "{{ test_group1 }}" TO "{{ test_user }}"', 'GRANT "{{ test_group2 }}" TO "{{ test_user }}"'] + + - name: check membership + <<: *task_parameters + postgresql_query: + <<: *pg_parameters + query: "SELECT * FROM pg_group WHERE groname in ('{{ test_group1 }}', '{{ test_group2 }}') AND grolist != '{}'" + + - assert: + that: + - result.rowcount == 2 + always: # # Clean up @@ -534,5 +662,9 @@ <<: *task_parameters postgresql_user: <<: *pg_parameters - name: '{{ test_user }}' + name: '{{ item }}' state: absent + loop: + - '{{ test_user }}' + - '{{ test_group1 }}' + - '{{ test_group2 }}'