From 4e833cf50668e9fbb298c605922d6bfda66ef484 Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Tue, 21 Aug 2012 11:20:16 -0500 Subject: [PATCH] Initial commit of change of semantics for module The postgresql_user module has several drawbacks: * No granularity for privileges * PostgreSQL semantics force working on one database at time, at least for Tables. Which means that a single call can't remove all the privileges for a user, and a user can't be removed until all the privileges are removed, forcing a module failure with no way to work around the issue. Changes: * Added the ability to specify granular privileges for database and tables within the database * Report if user was removed, and add an option to disable failing if user is not removed. --- library/postgresql_user | 191 ++++++++++++++++++++++++++++++---------- 1 file changed, 143 insertions(+), 48 deletions(-) diff --git a/library/postgresql_user b/library/postgresql_user index 56b0abd58e..51d971db96 100755 --- a/library/postgresql_user +++ b/library/postgresql_user @@ -34,35 +34,14 @@ def user_exists(cursor, user): return cursor.rowcount > 0 -def user_add(cursor, user, password, db): +def user_add(cursor, user, password): """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}) - grant_privileges(cursor, user, db) return True - -def has_privileges(cursor, user, db): - """Check if the user has create privileges on the database""" - query = "SELECT has_database_privilege(%(user)s, %(db)s, 'CREATE')" - cursor.execute(query, {'user': user, 'db': db}) - return cursor.fetchone()[0] - - -def grant_privileges(cursor, user, db): - """Grant all privileges on the database""" - query = "GRANT ALL PRIVILEGES ON DATABASE %(db)s TO %(user)s" - cursor.execute(query % {'user': user, 'db': db}) - - -def revoke_privileges(cursor, user, db): - """Revoke all privileges on the database""" - query = "REVOKE ALL PRIVILEGES ON DATABASE %(db)s FROM %(user)s" - cursor.execute(query % {'user': user, 'db': db}) - - -def user_mod(cursor, user, password, db): - """Update password and permissions""" +def user_chpass(cursor, user, password): + """Change user password""" changed = False # Handle passwords. @@ -79,28 +58,131 @@ def user_mod(cursor, user, password, db): if current_pass_hash != new_pass_hash: changed = True - # Handle privileges. - # For now, we just check if the user has access to the database - if not has_privileges(cursor, user, db): - grant_privileges(cursor, user, db) - changed = True - return changed +def user_delete(cursor, user): + """Try to remove a user. Returns True if successful otherwise False""" + cursor.execute("SAVEPOINT ansible_pgsql_user_delete") + try: + cursor.execute("DROP USER %s" % user) + except: + cursor.execute("ROLLBACK TO SAVEPOINT ansible_pgsql_user_delete") + cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") + return False + + cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") + return True -def user_delete(cursor, user, db): - """Delete a user, first revoking privileges""" - revoke_privileges(cursor, user, db) - cursor.execute("DROP USER %(user)s" % {'user': user}) +def has_table_privilege(cursor, user, table, priv): + query = 'SELECT has_table_privilege(%s, %s, %s)' + cursor.execute(query, user, table, priv) + return cursor.fetchone()[0] + +def grant_table_privilege(cursor, user, table, priv): + if has_table_privilege(cursor, user, table, priv): + return False + query = 'GRANT %s ON TABLE %s TO %s' % (priv, table, user) + cursor.execute(query) + return True + +def revoke_table_privilege(cursor, user, table, priv): + if not has_table_privilege(cursor, user, table, priv): + return False + query = 'REVOKE %s ON TABLE %s FROM %s' % (priv, table, user) + cursor.execute(query) return True +def has_database_privilege(cursor, user, db, priv): + query = 'SELECT has_database_privilege(%s, %s, %s)' + cursor.execute(query, user, db, priv) + return cursor.fetchone()[0] + +def grant_database_privilege(cursor, user, db, priv): + if has_database_privilege(cursor, user, db, priv): + return False + query = 'GRANT %s ON DATABASE %s TO %s' % (priv, db, user) + cursor.execute(query) + return True + +def revoke_database_privilege(cursor, user, db, priv): + if not has_database_privilege(cursor, user, db, priv): + return False + query = 'REVOKE %s ON DATABASE %s FROM %s' % (priv, db, user) + cursor.execute(query) + return True + +def revoke_privileges(cursor, user, privs): + if privs is None: + return False + + changed = False + for type_ in privs: + revoke_func = { + 'table':revoke_table_privilege, + 'database':revoke_database_privilege + }[type_] + for name, privileges in privs[type_].iteritem(): + for privilege in privileges: + changed = revoke_func(cursor, user, name, privilege)\ + or changed + + return changed + +def grant_privileges(cursor, user, privs): + if privs is None: + return False + + changed = False + for type_ in privs: + grant_func = { + 'table':grant_table_privilege, + 'database':grant_database_privilege + }[type_] + for name, privileges in privs[type_].iteritem(): + for privilege in privileges: + changed = grant_func(cursor, user, name, privilege)\ + or changed + + return changed + +def parse_privs(privs, db): + """ + Parse privilege string to determine permissions for database db. + Format: + + privileges[/privileges/...] + + Where: + + privileges := DATABASE_PRIVILEGES[,DATABASE_PRIVILEGES,...] | + TABLE_NAME:TABLE_PRIVILEGES[,TABLE_PRIVILEGES,...] + """ + if privs is None: + return privs + + privs = { + 'database':{}, + 'table':{} + } + for token in privs.split('/'): + if ':' not in token: + type_ = 'database' + name = db + privileges = token + else: + type_ = 'table' + name, privileges = token.split(':', 1) + privileges = privileges.split(',') + + privs[type_][name] = privileges + + return privs # =========================================== # Module execution. # - def main(): module = AnsibleModule( argument_spec=dict( @@ -110,13 +192,19 @@ def main(): user=dict(required=True, aliases=['name']), password=dict(default=None), state=dict(default="present", choices=["absent", "present"]), - db=dict(required=True), + priv=dict(default=None), + db=dict(default=''), + fail_on_user=dict(default=True) ) ) user = module.params["user"] password = module.params["password"] state = module.params["state"] + fail_on_user = module.params["fail_on_user"] db = module.params["db"] + if db == '' and module.params["priv"] is not None: + module.fail_json(msg="privileges require a database to be specified") + privs = parse_privs(module.params["priv"], db) if not postgresqldb_found: module.fail_json(msg="the python psycopg2 module is required") @@ -127,7 +215,8 @@ def main(): params_map = { "login_host":"host", "login_user":"user", - "login_password":"password" + "login_password":"password", + "db":"database" } kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() if k in params_map and v != "" ) @@ -136,24 +225,30 @@ def main(): cursor = db_connection.cursor() except Exception, e: module.fail_json(msg="unable to connect to database: %s" % e) - + + changed = False if state == "present": if user_exists(cursor, user): - changed = user_mod(cursor, user, password, db) + changed = user_chpass(cursor, user, password) else: if password is None: msg = "password parameter required when adding a user" module.fail_json(msg=msg) - changed = user_add(cursor, user, password, db) - - elif state == "absent": + changed = user_add(cursor, user, password) + changed = grant_privileges(cursor, user, privs) or changed + else: if user_exists(cursor, user): - changed = user_delete(cursor, user, db) - else: - changed = False - # Commit the database changes - db_connection.commit() - module.exit_json(changed=changed, user=user) + changed = revoke_privileges(cursor, user, privs) + user_removed = user_delete(cursor, user) + changed = changed or user_removed + + if fail_on_user and not user_removed: + msg = "unabel to remove user" + module.fail_json(msg=msg) + + if changed: + db_connection.commit() + module.exit_json(changed=changed, user=user, user_removed=user_removed) # this is magic, see lib/ansible/module_common.py #<>