Mdd psql user aws fix (#23988)
* postgresql_user module - transaction logic hacks to allow recovery from failed select * postgresql_user - PEP8 and style fixes to make debugging easier * postgresql_user - move password changing logic to separate function * postgresql_user - trap failure in case where there is no access to pg_authid * postgresql_user - further PEP8 fixes * postgresql_user - Simplify password change logic and improve imports according to suggestions from PR review * postgresql_user - Eliminate pep8/blank line errors introduced in merge * Check behaviour when pg_authid relation isn't readable TASK [postgresql : Normal user isn't allowed to access pg_authid relation: password comparison will fail, password will be updated] *** An exception occurred during task execution. To see the full traceback, use -vvv. The error was: psycopg2.ProgrammingError: permission denied for relation pg_authid * Don't reintroduce passlib, remove useless query
This commit is contained in:
parent
800de2b0ce
commit
3c4db1e8dd
3 changed files with 147 additions and 50 deletions
|
@ -212,6 +212,10 @@ import re
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import get_exception, AnsibleModule
|
||||||
|
from ansible.module_utils.database import pg_quote_identifier, SQLParseError
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
|
@ -227,7 +231,9 @@ FLAGS = ('SUPERUSER', 'CREATEROLE', 'CREATEUSER', 'CREATEDB', 'INHERIT', 'LOGIN'
|
||||||
FLAGS_BY_VERSION = {'BYPASSRLS': '9.5.0'}
|
FLAGS_BY_VERSION = {'BYPASSRLS': '9.5.0'}
|
||||||
|
|
||||||
VALID_PRIVS = dict(table=frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'ALL')),
|
VALID_PRIVS = dict(table=frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'ALL')),
|
||||||
database=frozenset(('CREATE', 'CONNECT', 'TEMPORARY', 'TEMP', 'ALL')),)
|
database=frozenset(
|
||||||
|
('CREATE', 'CONNECT', 'TEMPORARY', 'TEMP', 'ALL')),
|
||||||
|
)
|
||||||
|
|
||||||
# map to cope with idiosyncracies of SUPERUSER and LOGIN
|
# map to cope with idiosyncracies of SUPERUSER and LOGIN
|
||||||
PRIV_TO_AUTHID_COLUMN = dict(SUPERUSER='rolsuper', CREATEROLE='rolcreaterole',
|
PRIV_TO_AUTHID_COLUMN = dict(SUPERUSER='rolsuper', CREATEROLE='rolcreaterole',
|
||||||
|
@ -259,9 +265,11 @@ def user_exists(cursor, user):
|
||||||
|
|
||||||
def user_add(cursor, user, password, role_attr_flags, encrypted, expires):
|
def user_add(cursor, user, password, role_attr_flags, encrypted, expires):
|
||||||
"""Create a new database user (role)."""
|
"""Create a new database user (role)."""
|
||||||
# Note: role_attr_flags escaped by parse_role_attrs and encrypted is a literal
|
# Note: role_attr_flags escaped by parse_role_attrs and encrypted is a
|
||||||
|
# literal
|
||||||
query_password_data = dict(password=password, expires=expires)
|
query_password_data = dict(password=password, expires=expires)
|
||||||
query = ['CREATE USER %(user)s' % {"user": pg_quote_identifier(user, 'role')}]
|
query = ['CREATE USER %(user)s' %
|
||||||
|
{"user": pg_quote_identifier(user, 'role')}]
|
||||||
if password is not None:
|
if password is not None:
|
||||||
query.append("WITH %(crypt)s" % {"crypt": encrypted})
|
query.append("WITH %(crypt)s" % {"crypt": encrypted})
|
||||||
query.append("PASSWORD %(password)s")
|
query.append("PASSWORD %(password)s")
|
||||||
|
@ -273,26 +281,18 @@ def user_add(cursor, user, password, role_attr_flags, encrypted, expires):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def user_alter(cursor, module, user, password, role_attr_flags, encrypted, expires, no_password_changes):
|
def user_should_we_change_password(current_role_attrs, user, password, encrypted):
|
||||||
"""Change user password and/or attributes. Return True if changed, False otherwise."""
|
"""Check if we should change the user's password.
|
||||||
changed = False
|
|
||||||
|
|
||||||
# Note: role_attr_flags escaped by parse_role_attrs and encrypted is a literal
|
Compare the proposed password with the existing one, comparing
|
||||||
if user == 'PUBLIC':
|
hashes if encrypted. If we can't access it assume yes.
|
||||||
if password is not None:
|
"""
|
||||||
module.fail_json(msg="cannot change the password for PUBLIC user")
|
|
||||||
elif role_attr_flags != '':
|
|
||||||
module.fail_json(msg="cannot change the role_attr_flags for PUBLIC user")
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Handle passwords.
|
if current_role_attrs is None:
|
||||||
if not no_password_changes and (password is not None or role_attr_flags != '' or expires is not None):
|
# on some databases, E.g. AWS RDS instances, there is no access to
|
||||||
# Select password and all flag-like columns in order to verify changes.
|
# the pg_authid relation to check the pre-existing password, so we
|
||||||
select = "SELECT * FROM pg_authid where rolname=%(user)s"
|
# just assume password is different
|
||||||
cursor.execute(select, {"user": user})
|
return True
|
||||||
# Grab current role attributes.
|
|
||||||
current_role_attrs = cursor.fetchone()
|
|
||||||
|
|
||||||
# Do we actually need to do anything?
|
# Do we actually need to do anything?
|
||||||
pwchanging = False
|
pwchanging = False
|
||||||
|
@ -309,6 +309,38 @@ def user_alter(cursor, module, user, password, role_attr_flags, encrypted, expir
|
||||||
if hashed_password != current_role_attrs['rolpassword']:
|
if hashed_password != current_role_attrs['rolpassword']:
|
||||||
pwchanging = True
|
pwchanging = True
|
||||||
|
|
||||||
|
return pwchanging
|
||||||
|
|
||||||
|
|
||||||
|
def user_alter(db_connection, module, user, password, role_attr_flags, encrypted, expires, no_password_changes):
|
||||||
|
"""Change user password and/or attributes. Return True if changed, False otherwise."""
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
cursor = db_connection.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||||
|
# Note: role_attr_flags escaped by parse_role_attrs and encrypted is a
|
||||||
|
# literal
|
||||||
|
if user == 'PUBLIC':
|
||||||
|
if password is not None:
|
||||||
|
module.fail_json(msg="cannot change the password for PUBLIC user")
|
||||||
|
elif role_attr_flags != '':
|
||||||
|
module.fail_json(msg="cannot change the role_attr_flags for PUBLIC user")
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Handle passwords.
|
||||||
|
if not no_password_changes and (password is not None or role_attr_flags != '' or expires is not None):
|
||||||
|
# Select password and all flag-like columns in order to verify changes.
|
||||||
|
try:
|
||||||
|
select = "SELECT * FROM pg_authid where rolname=%(user)s"
|
||||||
|
cursor.execute(select, {"user": user})
|
||||||
|
# Grab current role attributes.
|
||||||
|
current_role_attrs = cursor.fetchone()
|
||||||
|
except psycopg2.ProgrammingError:
|
||||||
|
current_role_attrs = None
|
||||||
|
db_connection.rollback()
|
||||||
|
|
||||||
|
pwchanging = user_should_we_change_password(current_role_attrs, user, password, encrypted)
|
||||||
|
|
||||||
role_attr_flags_changing = False
|
role_attr_flags_changing = False
|
||||||
if role_attr_flags:
|
if role_attr_flags:
|
||||||
role_attr_flags_dict = {}
|
role_attr_flags_dict = {}
|
||||||
|
@ -381,7 +413,8 @@ def user_alter(cursor, module, user, password, role_attr_flags, encrypted, expir
|
||||||
if not role_attr_flags_changing:
|
if not role_attr_flags_changing:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
alter = ['ALTER USER %(user)s' % {"user": pg_quote_identifier(user, 'role')}]
|
alter = ['ALTER USER %(user)s' %
|
||||||
|
{"user": pg_quote_identifier(user, 'role')}]
|
||||||
if role_attr_flags:
|
if role_attr_flags:
|
||||||
alter.append('WITH %s' % role_attr_flags)
|
alter.append('WITH %s' % role_attr_flags)
|
||||||
|
|
||||||
|
@ -533,8 +566,10 @@ def revoke_privileges(cursor, user, privs):
|
||||||
if privs is None:
|
if privs is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
revoke_funcs = dict(table=revoke_table_privileges, database=revoke_database_privileges)
|
revoke_funcs = dict(table=revoke_table_privileges,
|
||||||
check_funcs = dict(table=has_table_privileges, database=has_database_privileges)
|
database=revoke_database_privileges)
|
||||||
|
check_funcs = dict(table=has_table_privileges,
|
||||||
|
database=has_database_privileges)
|
||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
for type_ in privs:
|
for type_ in privs:
|
||||||
|
@ -552,8 +587,10 @@ def grant_privileges(cursor, user, privs):
|
||||||
if privs is None:
|
if privs is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
grant_funcs = dict(table=grant_table_privileges, database=grant_database_privileges)
|
grant_funcs = dict(table=grant_table_privileges,
|
||||||
check_funcs = dict(table=has_table_privileges, database=has_database_privileges)
|
database=grant_database_privileges)
|
||||||
|
check_funcs = dict(table=has_table_privileges,
|
||||||
|
database=has_database_privileges)
|
||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
for type_ in privs:
|
for type_ in privs:
|
||||||
|
@ -631,11 +668,13 @@ def parse_privs(privs, db):
|
||||||
if ':' not in token:
|
if ':' not in token:
|
||||||
type_ = 'database'
|
type_ = 'database'
|
||||||
name = db
|
name = db
|
||||||
priv_set = frozenset(x.strip().upper() for x in token.split(',') if x.strip())
|
priv_set = frozenset(x.strip().upper()
|
||||||
|
for x in token.split(',') if x.strip())
|
||||||
else:
|
else:
|
||||||
type_ = 'table'
|
type_ = 'table'
|
||||||
name, privileges = token.split(':', 1)
|
name, privileges = token.split(':', 1)
|
||||||
priv_set = frozenset(x.strip().upper() for x in privileges.split(',') if x.strip())
|
priv_set = frozenset(x.strip().upper()
|
||||||
|
for x in privileges.split(',') if x.strip())
|
||||||
|
|
||||||
if not priv_set.issubset(VALID_PRIVS[type_]):
|
if not priv_set.issubset(VALID_PRIVS[type_]):
|
||||||
raise InvalidPrivsError('Invalid privs specified for %s: %s' %
|
raise InvalidPrivsError('Invalid privs specified for %s: %s' %
|
||||||
|
@ -681,6 +720,7 @@ def get_valid_flags_by_version(cursor):
|
||||||
# Module execution.
|
# Module execution.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
|
@ -699,7 +739,8 @@ def main():
|
||||||
encrypted=dict(type='bool', default='no'),
|
encrypted=dict(type='bool', default='no'),
|
||||||
no_password_changes=dict(type='bool', default='no'),
|
no_password_changes=dict(type='bool', default='no'),
|
||||||
expires=dict(default=None),
|
expires=dict(default=None),
|
||||||
ssl_mode=dict(default='prefer', choices=['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full']),
|
ssl_mode=dict(default='prefer', choices=[
|
||||||
|
'disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full']),
|
||||||
ssl_rootcert=dict(default=None)
|
ssl_rootcert=dict(default=None)
|
||||||
),
|
),
|
||||||
supports_check_mode=True
|
supports_check_mode=True
|
||||||
|
@ -745,16 +786,19 @@ def main():
|
||||||
kw["host"] = module.params["login_unix_socket"]
|
kw["host"] = module.params["login_unix_socket"]
|
||||||
|
|
||||||
if psycopg2.__version__ < '2.4.3' and sslrootcert is not None:
|
if psycopg2.__version__ < '2.4.3' and sslrootcert is not None:
|
||||||
module.fail_json(msg='psycopg2 must be at least 2.4.3 in order to user the ssl_rootcert parameter')
|
module.fail_json(
|
||||||
|
msg='psycopg2 must be at least 2.4.3 in order to user the ssl_rootcert parameter')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_connection = psycopg2.connect(**kw)
|
db_connection = psycopg2.connect(**kw)
|
||||||
cursor = db_connection.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
cursor = db_connection.cursor(
|
||||||
|
cursor_factory=psycopg2.extras.DictCursor)
|
||||||
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
e = get_exception()
|
e = get_exception()
|
||||||
if 'sslrootcert' in e.args[0]:
|
if 'sslrootcert' in e.args[0]:
|
||||||
module.fail_json(msg='Postgresql server must be at least version 8.4 to support sslrootcert')
|
module.fail_json(
|
||||||
|
msg='Postgresql server must be at least version 8.4 to support sslrootcert')
|
||||||
module.fail_json(msg="unable to connect to database: %s" % e)
|
module.fail_json(msg="unable to connect to database: %s" % e)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -774,13 +818,15 @@ def main():
|
||||||
if state == "present":
|
if state == "present":
|
||||||
if user_exists(cursor, user):
|
if user_exists(cursor, user):
|
||||||
try:
|
try:
|
||||||
changed = user_alter(cursor, module, user, password, role_attr_flags, encrypted, expires, no_password_changes)
|
changed = user_alter(db_connection, module, user, password,
|
||||||
|
role_attr_flags, encrypted, expires, no_password_changes)
|
||||||
except SQLParseError:
|
except SQLParseError:
|
||||||
e = get_exception()
|
e = get_exception()
|
||||||
module.fail_json(msg=str(e))
|
module.fail_json(msg=str(e))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
changed = user_add(cursor, user, password, role_attr_flags, encrypted, expires)
|
changed = user_add(cursor, user, password,
|
||||||
|
role_attr_flags, encrypted, expires)
|
||||||
except SQLParseError:
|
except SQLParseError:
|
||||||
e = get_exception()
|
e = get_exception()
|
||||||
module.fail_json(msg=str(e))
|
module.fail_json(msg=str(e))
|
||||||
|
@ -817,9 +863,5 @@ def main():
|
||||||
module.exit_json(**kw)
|
module.exit_json(**kw)
|
||||||
|
|
||||||
|
|
||||||
# import module snippets
|
|
||||||
from ansible.module_utils.basic import *
|
|
||||||
from ansible.module_utils.database import *
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -336,17 +336,22 @@
|
||||||
become: True
|
become: True
|
||||||
shell: echo "create table test_table2 (field text);" | psql {{ db_name }}
|
shell: echo "create table test_table2 (field text);" | psql {{ db_name }}
|
||||||
|
|
||||||
- name: Create a user with some permissions on the db
|
- vars:
|
||||||
|
db_password: 'secretù' # use UTF-8
|
||||||
|
block:
|
||||||
|
- name: Create a user with some permissions on the db
|
||||||
become_user: "{{ pg_user }}"
|
become_user: "{{ pg_user }}"
|
||||||
become: True
|
become: True
|
||||||
postgresql_user:
|
postgresql_user:
|
||||||
name: "{{ db_user1 }}"
|
name: "{{ db_user1 }}"
|
||||||
encrypted: 'yes'
|
encrypted: 'yes'
|
||||||
password: "md55c8ccfd9d6711fc69a7eae647fc54f51"
|
password: "md5{{ (db_password ~ db_user1) | hash('md5')}}"
|
||||||
db: "{{ db_name }}"
|
db: "{{ db_name }}"
|
||||||
priv: 'test_table1:INSERT,SELECT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER/test_table2:INSERT/CREATE,CONNECT,TEMP'
|
priv: 'test_table1:INSERT,SELECT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER/test_table2:INSERT/CREATE,CONNECT,TEMP'
|
||||||
login_user: "{{ pg_user }}"
|
login_user: "{{ pg_user }}"
|
||||||
|
|
||||||
|
- include: pg_authid_not_readable.yml
|
||||||
|
|
||||||
- name: Check that the user has the requested permissions (table1)
|
- name: Check that the user has the requested permissions (table1)
|
||||||
become_user: "{{ pg_user }}"
|
become_user: "{{ pg_user }}"
|
||||||
become: True
|
become: True
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
- name: "Admin user is allowed to access pg_authid relation: password comparison will succeed, password won't be updated"
|
||||||
|
become_user: "{{ pg_user }}"
|
||||||
|
become: True
|
||||||
|
postgresql_user:
|
||||||
|
name: "{{ db_user1 }}"
|
||||||
|
encrypted: 'yes'
|
||||||
|
password: "md5{{ (db_password ~ db_user1) | hash('md5')}}"
|
||||||
|
db: "{{ db_name }}"
|
||||||
|
priv: 'test_table1:INSERT,SELECT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER/test_table2:INSERT/CREATE,CONNECT,TEMP'
|
||||||
|
login_user: "{{ pg_user }}"
|
||||||
|
register: redo_as_admin
|
||||||
|
|
||||||
|
- name: "Check that task succeeded without any change"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- 'not redo_as_admin|failed'
|
||||||
|
- 'not redo_as_admin|changed'
|
||||||
|
- 'redo_as_admin|success'
|
||||||
|
|
||||||
|
- name: "Check that normal user isn't allowed to access pg_authid"
|
||||||
|
shell: 'psql -c "select * from pg_authid;" {{ db_name }} {{ db_user1 }}'
|
||||||
|
environment:
|
||||||
|
PGPASSWORD: '{{ db_password }}'
|
||||||
|
ignore_errors: True
|
||||||
|
register: pg_authid
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- 'pg_authid|failed'
|
||||||
|
- '"permission denied for relation pg_authid" in pg_authid.stderr'
|
||||||
|
|
||||||
|
- name: "Normal user isn't allowed to access pg_authid relation: password comparison will fail, password will be updated"
|
||||||
|
become_user: "{{ pg_user }}"
|
||||||
|
become: True
|
||||||
|
postgresql_user:
|
||||||
|
name: "{{ db_user1 }}"
|
||||||
|
encrypted: 'yes'
|
||||||
|
password: "md5{{ (db_password ~ db_user1) | hash('md5')}}"
|
||||||
|
db: "{{ db_name }}"
|
||||||
|
priv: 'test_table1:INSERT,SELECT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER/test_table2:INSERT/CREATE,CONNECT,TEMP'
|
||||||
|
login_user: "{{ db_user1 }}"
|
||||||
|
login_password: "{{ db_password }}"
|
||||||
|
register: redo_as_normal_user
|
||||||
|
|
||||||
|
- name: "Check that task succeeded and that result is changed"
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- 'not redo_as_normal_user|failed'
|
||||||
|
- 'redo_as_normal_user|changed'
|
||||||
|
- 'redo_as_normal_user|success'
|
Loading…
Reference in a new issue