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

add role_attr_flags parameter to postgresql_user #1339

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/playbooks/postgresql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@

- name: ensure user has access to database
action: postgresql_user db=$dbname user=$dbuser password=$dbpassword priv=ALL

- name: ensure user does not have unnecessary privilege
action: postgresql_user user=$dbuser role_attr_flags=NOSUPERUSER,NOCREATEDB
82 changes: 63 additions & 19 deletions library/postgresql_user
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ options:
- "PostgreSQL privileges string in the format: C(table:priv1,priv2)"
required: false
default: null
role_attr_flags:
description:
- "PostgreSQL role attributes string in the format: CREATEDB,CREATEROLE,SUPERUSER"
required: false
default: null
choices: [ "[NO]SUPERUSER","[NO]CREATEROLE", "[NO]CREATEUSER", "[NO]CREATEDB",
"[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION" ]
state:
description:
- The database state
Expand All @@ -86,6 +93,8 @@ options:
examples:
- code: postgresql_user db=acme user=django password=ceec4eif7ya priv=CONNECT/products:ALL
description: Create django user and grant access to database and products table
- code: postgresql_user user=rails password=secret role_attr_flags=CREATEDB,NOSUPERUSER
- description: Create rails user, grant privilege to create other databases and demote rails from super user status
- code: postgresql_user db=acme user=test priv=ALL/products:ALL state=absent fail_on_user=no
description: Remove test user privileges from acme
- code: postgresql_user db=test user=test priv=ALL state=absent
Expand Down Expand Up @@ -125,29 +134,45 @@ def user_exists(cursor, user):
return cursor.rowcount > 0


def user_add(cursor, user, password):
def user_add(cursor, user, password, role_attr_flags):
"""Create a new user with write access to the database"""
query = "CREATE USER %(user)s with PASSWORD '%(password)s'"
cursor.execute(query % {"user": user, "password": password})
query = "CREATE USER %(user)s with PASSWORD '%(password)s' %(role_attr_flags)s"
cursor.execute(query % {"user": user, "password": password, "role_attr_flags": role_attr_flags})
return True

def user_chpass(cursor, user, password):
def user_alter(cursor, user, password, role_attr_flags):
"""Change user password"""
changed = False

# Handle passwords.
if password is not None:
select = "SELECT rolpassword FROM pg_authid where rolname=%(user)s"
cursor.execute(select, {"user": user})
current_pass_hash = cursor.fetchone()[0]
# Not sure how to hash the new password, so we just initiate the
# change and check if the hash changed
alter = "ALTER USER %(user)s WITH PASSWORD '%(password)s'"
cursor.execute(alter % {"user": user, "password": password})
cursor.execute(select, {"user": user})
new_pass_hash = cursor.fetchone()[0]
if current_pass_hash != new_pass_hash:
changed = True
if password is not None or role_attr_flags is not None:
# Define columns for select.
columns = 'rolpassword,rolsuper,rolinherit,rolcreaterole,rolcreatedb,rolcanlogin,rolreplication'
# Select password and all flag-like columns in order to verify changes.
# rolsuper | rolinherit | rolcreaterole | rolcreatedb | rolcatupdate |
# rolcanlogin | rolreplication | rolconnlimit | rolpassword | rolvaliduntil
# Not sure how to interpolate properly in python yet...
select = "SELECT " + columns + " FROM pg_authid where rolname=%(user)s"
cursor.execute(select, {"columns": columns, "user": user})
# Grab current role attributes.
current_role_attrs = cursor.fetchone()

if password is not None:
# Update the role attributes, including password.
alter = "ALTER USER %(user)s WITH PASSWORD '%(password)s' %(role_attr_flags)s"
cursor.execute(alter % {"user": user, "password": password, "role_attr_flags": role_attr_flags})
else:
# Update the role attributes, excluding password.
alter = "ALTER USER %(user)s WITH %(role_attr_flags)s"
cursor.execute(alter % {"user": user, "role_attr_flags": role_attr_flags})
# Grab new role attributes.
cursor.execute(select, {"columns": columns, "user": user})
new_role_attrs = cursor.fetchone()

# Detect any differences between current_ and new_role_attrs.
for i in range(len(current_role_attrs)):
if current_role_attrs[i] != new_role_attrs[i]:
changed = True

return changed

Expand Down Expand Up @@ -267,6 +292,23 @@ def grant_privileges(cursor, user, privs):

return changed

def parse_role_attrs(role_attr_flags):
"""
Parse role attributes string for user creation.
Format:

attributes[,attributes,...]

Where:

attributes := CREATEDB,CREATEROLE,NOSUPERUSER,...
"""
if ',' not in role_attr_flags:
return role_attr_flags
flag_set = role_attr_flags.split(",")
o_flags = " ".join(flag_set)
return o_flags

def parse_privs(privs, db):
"""
Parse privilege string to determine permissions for database db.
Expand Down Expand Up @@ -316,7 +358,8 @@ def main():
priv=dict(default=None),
db=dict(default=''),
port=dict(default='5432'),
fail_on_user=dict(default='yes')
fail_on_user=dict(default='yes'),
role_attr_flags=dict(default='')
)
)
user = module.params["user"]
Expand All @@ -328,6 +371,7 @@ def main():
module.fail_json(msg="privileges require a database to be specified")
privs = parse_privs(module.params["priv"], db)
port = module.params["port"]
role_attr_flags = parse_role_attrs(module.params["role_attr_flags"])

if not postgresqldb_found:
module.fail_json(msg="the python psycopg2 module is required")
Expand Down Expand Up @@ -355,12 +399,12 @@ def main():
user_removed = False
if state == "present":
if user_exists(cursor, user):
changed = user_chpass(cursor, user, password)
changed = user_alter(cursor, user, password, role_attr_flags)
else:
if password is None:
msg = "password parameter required when adding a user"
module.fail_json(msg=msg)
changed = user_add(cursor, user, password)
changed = user_add(cursor, user, password, role_attr_flags)
changed = grant_privileges(cursor, user, privs) or changed
else:
if user_exists(cursor, user):
Expand Down