From e99c12460bc47f44fb09d33dd167b4264deebaa1 Mon Sep 17 00:00:00 2001
From: Jeremiah Heller <jeremiah@inertialbit.net>
Date: Mon, 15 Oct 2012 14:42:06 -0700
Subject: [PATCH] add role_attr_flags parameter to postgresql_user

Pass role_attr_flags a list of comma separated role
attributes when creating or updating a user.
---
 postgresql_user | 82 +++++++++++++++++++++++++++++++++++++------------
 1 file changed, 63 insertions(+), 19 deletions(-)

diff --git a/postgresql_user b/postgresql_user
index 5c9c2025d27..c72ff54a6cd 100755
--- a/postgresql_user
+++ b/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):