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

postgresql_membership: add the exact state value to be able to specify a list of only groups a user must be a member of #293

Merged
merged 3 commits into from
Jun 21, 2022
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- postgresql_membership - add the ``exact`` state value to be able to specify a list of only groups a user must be a member of (https://github.com/ansible-collections/community.postgresql/issues/277).
93 changes: 64 additions & 29 deletions plugins/module_utils/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,29 @@ def get_conn_params(module, params_dict, warn_db_default=True):
return kw


class PgRole():
def __init__(self, module, cursor, name):
self.module = module
self.cursor = cursor
self.name = name
self.memberof = self.__fetch_members()

def __fetch_members(self):
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 = %(dst_role)s")

res = exec_sql(self, query, query_params={'dst_role': self.name},
add_to_executed=False)
if res:
return res[0][0]
else:
return []


class PgMembership(object):
def __init__(self, module, cursor, groups, target_roles, fail_on_role=True):
self.module = module
Expand All @@ -245,8 +268,9 @@ def grant(self):
self.granted[group] = []

for role in self.target_roles:
role_obj = PgRole(self.module, self.cursor, role)
# If role is in a group now, pass:
if self.__check_membership(group, role):
if group in role_obj.memberof:
continue

query = 'GRANT "%s" TO "%s"' % (group, role)
Expand All @@ -262,8 +286,9 @@ def revoke(self):
self.revoked[group] = []

for role in self.target_roles:
role_obj = PgRole(self.module, self.cursor, role)
# If role is not in a group now, pass:
if not self.__check_membership(group, role):
if group not in role_obj.memberof:
continue

query = 'REVOKE "%s" FROM "%s"' % (group, role)
Expand All @@ -274,39 +299,48 @@ def revoke(self):

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 = %(dst_role)s")

res = exec_sql(self, query, query_params={'dst_role': dst_role}, add_to_executed=False)
membership = []
if res:
membership = res[0][0]
def match(self):
for role in self.target_roles:
role_obj = PgRole(self.module, self.cursor, role)

if not membership:
return False
desired_groups = set(self.groups)
current_groups = set(role_obj.memberof)
# 1. Get groups that the role is member of but not in self.groups and revoke them
groups_to_revoke = current_groups - desired_groups
for group in groups_to_revoke:
query = 'REVOKE "%s" FROM "%s"' % (group, role)
self.changed = exec_sql(self, query, return_bool=True)
if group in self.revoked:
self.revoked[group].append(role)
else:
self.revoked[group] = [role]

if src_role in membership:
return True
# 2. Filter out groups that in self.groups and
# the role is already member of and grant the rest
groups_to_grant = desired_groups - current_groups
for group in groups_to_grant:
query = 'GRANT "%s" TO "%s"' % (group, role)
self.changed = exec_sql(self, query, return_bool=True)
if group in self.granted:
self.granted[group].append(role)
else:
self.granted[group] = [role]

return False
return self.changed

def __check_roles_exist(self):
existent_groups = self.__roles_exist(self.groups)
existent_roles = self.__roles_exist(self.target_roles)
if self.groups:
existent_groups = self.__roles_exist(self.groups)

for group in self.groups:
if group not in existent_groups:
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 group in self.groups:
if group not in existent_groups:
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)

existent_roles = self.__roles_exist(self.target_roles)
for role in self.target_roles:
if role not in existent_roles:
if self.fail_on_role:
Expand All @@ -324,7 +358,8 @@ def __check_roles_exist(self):
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]
if self.groups:
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]

Expand Down
33 changes: 31 additions & 2 deletions plugins/modules/postgresql_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@
- Membership state.
- I(state=present) implies the I(groups)must be granted to I(target_roles).
- I(state=absent) implies the I(groups) must be revoked from I(target_roles).
- I(state=exact) implies that I(target_roles) will be members of only the I(groups)
(available since community.postgresql 2.2.0).
Any other groups will be revoked from I(target_roles).
type: str
default: present
choices: [ absent, present ]
choices: [ absent, exact, present ]
db:
description:
- Name of database to connect to.
Expand Down Expand Up @@ -111,6 +114,26 @@
target_role: bob
fail_on_role: no
state: absent

- name: >
Make sure alice and bob are members only of marketing and sales.
If they are members of other groups, they will be removed from those groups
community.postgresql.postgresql_membership:
group:
- marketing
- sales
target_roles:
- alice
- bob
state: exact

- name: Make sure alice and bob do not belong to any groups
community.postgresql.postgresql_membership:
group: []
target_roles:
- alice
- bob
state: exact
'''

RETURN = r'''
Expand Down Expand Up @@ -164,7 +187,7 @@ def main():
groups=dict(type='list', elements='str', required=True, aliases=['group', 'source_role', 'source_roles']),
target_roles=dict(type='list', elements='str', required=True, aliases=['target_role', 'user', 'users']),
fail_on_role=dict(type='bool', default=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
state=dict(type='str', default='present', choices=['absent', 'exact', 'present']),
db=dict(type='str', aliases=['login_db']),
session_role=dict(type='str'),
trust_input=dict(type='bool', default=True),
Expand Down Expand Up @@ -199,6 +222,9 @@ def main():
if state == 'present':
pg_membership.grant()

elif state == 'exact':
pg_membership.match()

elif state == 'absent':
pg_membership.revoke()

Expand All @@ -224,6 +250,9 @@ def main():
return_dict['granted'] = pg_membership.granted
elif state == 'absent':
return_dict['revoked'] = pg_membership.revoked
elif state == 'exact':
return_dict['granted'] = pg_membership.granted
return_dict['revoked'] = pg_membership.revoked

module.exit_json(**return_dict)

Expand Down
Loading