diff --git a/examples/playbooks/postgresql.yml b/examples/playbooks/postgresql.yml index b4e8fc7d3c..c48130ce8f 100644 --- a/examples/playbooks/postgresql.yml +++ b/examples/playbooks/postgresql.yml @@ -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 \ No newline at end of file diff --git a/library/postgresql_user b/library/postgresql_user index 5c9c2025d2..c72ff54a6c 100755 --- a/library/postgresql_user +++ b/library/postgresql_user @@ -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 @@ -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 @@ -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 @@ -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. @@ -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"] @@ -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") @@ -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):