384839bfe1
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.
255 lines
8.1 KiB
Python
Executable file
255 lines
8.1 KiB
Python
Executable file
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
try:
|
|
import psycopg2
|
|
except ImportError:
|
|
postgresqldb_found = False
|
|
else:
|
|
postgresqldb_found = True
|
|
|
|
# ===========================================
|
|
# PostgreSQL module specific support methods.
|
|
#
|
|
|
|
|
|
def user_exists(cursor, user):
|
|
query = "SELECT rolname FROM pg_roles WHERE rolname=%(user)s"
|
|
cursor.execute(query, {'user': user})
|
|
return cursor.rowcount > 0
|
|
|
|
|
|
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})
|
|
return True
|
|
|
|
def user_chpass(cursor, user, password):
|
|
"""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
|
|
|
|
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 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(
|
|
login_user=dict(default="postgres"),
|
|
login_password=dict(default=""),
|
|
login_host=dict(default=""),
|
|
user=dict(required=True, aliases=['name']),
|
|
password=dict(default=None),
|
|
state=dict(default="present", choices=["absent", "present"]),
|
|
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")
|
|
|
|
# To use defaults values, keyword arguments must be absent, so
|
|
# check which values are empty and don't include in the **kw
|
|
# dictionary
|
|
params_map = {
|
|
"login_host":"host",
|
|
"login_user":"user",
|
|
"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 != "" )
|
|
try:
|
|
db_connection = psycopg2.connect(database=db, **kw)
|
|
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_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)
|
|
changed = grant_privileges(cursor, user, privs) or changed
|
|
else:
|
|
if user_exists(cursor, 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
|
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
|
main()
|