From 723c8f06abfa96d45cf4c0d817091d5fe1ac4879 Mon Sep 17 00:00:00 2001 From: Reilly Herrewig-Pope Date: Fri, 26 May 2017 12:56:42 -0400 Subject: [PATCH] Allow setting/unsetting BYPASSRLS Postgres role attribute (#24625) * Allow setting/unsetting BYPASSRLS role attr * Build valid role attrs against version * Add integration tests --- .../database/postgresql/postgresql_user.py | 62 +++++++++--- .../targets/postgresql/tasks/main.yml | 97 +++---------------- .../tasks/postgresql_user_9.5_or_greater.yml | 90 +++++++++++++++++ .../tasks/postgresql_user_less_than_9.5.yml | 87 +++++++++++++++++ 4 files changed, 243 insertions(+), 93 deletions(-) create mode 100644 test/integration/targets/postgresql/tasks/postgresql_user_9.5_or_greater.yml create mode 100644 test/integration/targets/postgresql/tasks/postgresql_user_less_than_9.5.yml diff --git a/lib/ansible/modules/database/postgresql/postgresql_user.py b/lib/ansible/modules/database/postgresql/postgresql_user.py index 48d5799eca..89847322c3 100644 --- a/lib/ansible/modules/database/postgresql/postgresql_user.py +++ b/lib/ansible/modules/database/postgresql/postgresql_user.py @@ -103,7 +103,7 @@ options: required: false default: "" choices: [ "[NO]SUPERUSER","[NO]CREATEROLE", "[NO]CREATEUSER", "[NO]CREATEDB", - "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION" ] + "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION", "[NO]BYPASSRLS" ] state: description: - The user (role) state @@ -210,6 +210,8 @@ EXAMPLES = ''' import re import itertools +from distutils.version import StrictVersion + try: import psycopg2 import psycopg2.extras @@ -220,7 +222,7 @@ else: from ansible.module_utils.six import iteritems _flags = ('SUPERUSER', 'CREATEROLE', 'CREATEUSER', 'CREATEDB', 'INHERIT', 'LOGIN', 'REPLICATION') -VALID_FLAGS = frozenset(itertools.chain(_flags, ('NO%s' % f for f in _flags))) +_flags_by_version = {'BYPASSRLS': '9.5.0'} VALID_PRIVS = dict(table=frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'ALL')), database=frozenset(('CREATE', 'CONNECT', 'TEMPORARY', 'TEMP', 'ALL')), @@ -230,7 +232,7 @@ VALID_PRIVS = dict(table=frozenset(('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRU PRIV_TO_AUTHID_COLUMN = dict(SUPERUSER='rolsuper', CREATEROLE='rolcreaterole', CREATEUSER='rolcreateuser', CREATEDB='rolcreatedb', INHERIT='rolinherit', LOGIN='rolcanlogin', - REPLICATION='rolreplication') + REPLICATION='rolreplication', BYPASSRLS='rolbypassrls') class InvalidFlagsError(Exception): pass @@ -558,7 +560,7 @@ def grant_privileges(cursor, user, privs): changed = True return changed -def parse_role_attrs(role_attr_flags): +def parse_role_attrs(cursor, role_attr_flags): """ Parse role attributes string for user creation. Format: @@ -569,18 +571,24 @@ def parse_role_attrs(role_attr_flags): attributes := CREATEDB,CREATEROLE,NOSUPERUSER,... [ "[NO]SUPERUSER","[NO]CREATEROLE", "[NO]CREATEUSER", "[NO]CREATEDB", - "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION" ] + "[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION", + "[NO]BYPASSRLS" ] + + Note: "[NO]BYPASSRLS" role attribute introduced in 9.5 """ + flags = frozenset(itertools.chain(_flags, get_valid_flags_by_version(cursor))) + valid_flags = frozenset(itertools.chain(flags, ('NO%s' % f for f in flags))) + if ',' in role_attr_flags: flag_set = frozenset(r.upper() for r in role_attr_flags.split(",")) elif role_attr_flags: flag_set = frozenset((role_attr_flags.upper(),)) else: flag_set = frozenset() - if not flag_set.issubset(VALID_FLAGS): + if not flag_set.issubset(valid_flags): raise InvalidFlagsError('Invalid role_attr_flags specified: %s' % - ' '.join(flag_set.difference(VALID_FLAGS))) + ' '.join(flag_set.difference(valid_flags))) o_flags = ' '.join(flag_set) return o_flags @@ -633,6 +641,35 @@ def parse_privs(privs, db): return o_privs +def get_pg_server_version(cursor): + """ + Queries Postgres for its server version. + + server_version should be just the server version itself: + + postgres=# SHOW SERVER_VERSION; + server_version + ---------------- + 9.6.2 + (1 row) + """ + cursor.execute("SHOW SERVER_VERSION") + return cursor.fetchone()['server_version'] + +def get_valid_flags_by_version(cursor): + """ + Some role attributes were introduced after certain versions. We want to + compile a list of valid flags against the current Postgres version. + """ + current_version = StrictVersion(get_pg_server_version(cursor)) + + return [ + flag + for flag, version_introduced in _flags_by_version.items() + if current_version >= StrictVersion(version_introduced) + ] + + # =========================================== # Module execution. # @@ -671,11 +708,6 @@ def main(): privs = parse_privs(module.params["priv"], db) port = module.params["port"] no_password_changes = module.params["no_password_changes"] - try: - role_attr_flags = parse_role_attrs(module.params["role_attr_flags"]) - except InvalidFlagsError: - e = get_exception() - module.fail_json(msg=str(e)) if module.params["encrypted"]: encrypted = "ENCRYPTED" else: @@ -723,6 +755,12 @@ def main(): e = get_exception() module.fail_json(msg="unable to connect to database: %s" % e) + try: + role_attr_flags = parse_role_attrs(cursor, module.params["role_attr_flags"]) + except InvalidFlagsError: + e = get_exception() + module.fail_json(msg=str(e)) + kw = dict(user=user) changed = False user_removed = False diff --git a/test/integration/targets/postgresql/tasks/main.yml b/test/integration/targets/postgresql/tasks/main.yml index b6cad4106d..5b858f8bfe 100644 --- a/test/integration/targets/postgresql/tasks/main.yml +++ b/test/integration/targets/postgresql/tasks/main.yml @@ -268,92 +268,27 @@ that: - "result.changed == False" -- name: Create a user with all role attributes +# BYPASSRLS role attribute was introduced in Postgres 9.5, so +# we want to test atrribute management differently depending +# on the version. See https://github.com/ansible/ansible/pull/24625 +# for more details. +- name: Get Postgres version become_user: "{{ pg_user }}" become: True - postgresql_user: - name: "{{ db_user1 }}" - state: "present" - role_attr_flags: "SUPERUSER,CREATEROLE,CREATEDB,INHERIT,login" - login_user: "{{ pg_user }}" - db: postgres + shell: echo 'SHOW SERVER_VERSION' | psql -d postgres + register: postgres_version_resp -- name: Check that the user has the requested role attributes - become_user: "{{ pg_user }}" - become: True - shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres - register: result +- name: Print Postgres server version + debug: + msg: "{{ postgres_version_resp.stdout_lines[-2] | trim }}" -- assert: - that: - - "result.stdout_lines[-1] == '(1 row)'" - - "'super:t' in result.stdout_lines[-2]" - - "'createrole:t' in result.stdout_lines[-2]" - - "'create:t' in result.stdout_lines[-2]" - - "'inherit:t' in result.stdout_lines[-2]" - - "'login:t' in result.stdout_lines[-2]" +- name: Role attribute testing for Postgres 9.5+ + include: postgresql_user_9.5_or_greater.yml + when: (postgres_version_resp.stdout_lines[-2] | trim) | version_compare('9.5.0', '>=') -- name: Modify a user to have no role attributes - become_user: "{{ pg_user }}" - become: True - postgresql_user: - name: "{{ db_user1 }}" - state: "present" - role_attr_flags: "NOSUPERUSER,NOCREATEROLE,NOCREATEDB,noinherit,NOLOGIN" - login_user: "{{ pg_user }}" - db: postgres - register: result - -- name: Check that ansible reports it modified the role - assert: - that: - - "result.changed == True" - -- name: Check that the user has the requested role attributes - become_user: "{{ pg_user }}" - become: True - shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres - register: result - -- assert: - that: - - "result.stdout_lines[-1] == '(1 row)'" - - "'super:f' in result.stdout_lines[-2]" - - "'createrole:f' in result.stdout_lines[-2]" - - "'create:f' in result.stdout_lines[-2]" - - "'inherit:f' in result.stdout_lines[-2]" - - "'login:f' in result.stdout_lines[-2]" - -- name: Modify a single role attribute on a user - become_user: "{{ pg_user }}" - become: True - postgresql_user: - name: "{{ db_user1 }}" - state: "present" - role_attr_flags: "LOGIN" - login_user: "{{ pg_user }}" - db: postgres - register: result - -- name: Check that ansible reports it modified the role - assert: - that: - - "result.changed == True" - -- name: Check that the user has the requested role attributes - become_user: "{{ pg_user }}" - become: True - shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres - register: result - -- assert: - that: - - "result.stdout_lines[-1] == '(1 row)'" - - "'super:f' in result.stdout_lines[-2]" - - "'createrole:f' in result.stdout_lines[-2]" - - "'create:f' in result.stdout_lines[-2]" - - "'inherit:f' in result.stdout_lines[-2]" - - "'login:t' in result.stdout_lines[-2]" +- name: Role attribute testing for Postgres versions below 9.5 + include: postgresql_user_less_than_9.5.yml + when: (postgres_version_resp.stdout_lines[-2] | trim) | version_compare('9.5.0', '<') - name: Cleanup the user become_user: "{{ pg_user }}" diff --git a/test/integration/targets/postgresql/tasks/postgresql_user_9.5_or_greater.yml b/test/integration/targets/postgresql/tasks/postgresql_user_9.5_or_greater.yml new file mode 100644 index 0000000000..e028cc8096 --- /dev/null +++ b/test/integration/targets/postgresql/tasks/postgresql_user_9.5_or_greater.yml @@ -0,0 +1,90 @@ +--- +- name: Create a user with all role attributes + become_user: "{{ pg_user }}" + become: True + postgresql_user: + name: "{{ db_user1 }}" + state: "present" + role_attr_flags: "SUPERUSER,CREATEROLE,CREATEDB,INHERIT,login,BYPASSRLS" + login_user: "{{ pg_user }}" + db: postgres + +- name: Check that the user has the requested role attributes + become_user: "{{ pg_user }}" + become: True + shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin, 'bypassrls:'||rolbypassrls from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres + register: result + +- assert: + that: + - "result.stdout_lines[-1] == '(1 row)'" + - "'super:t' in result.stdout_lines[-2]" + - "'createrole:t' in result.stdout_lines[-2]" + - "'create:t' in result.stdout_lines[-2]" + - "'inherit:t' in result.stdout_lines[-2]" + - "'login:t' in result.stdout_lines[-2]" + - "'bypassrls:t' in result.stdout_lines[-2]" + +- name: Modify a user to have no role attributes + become_user: "{{ pg_user }}" + become: True + postgresql_user: + name: "{{ db_user1 }}" + state: "present" + role_attr_flags: "NOSUPERUSER,NOCREATEROLE,NOCREATEDB,noinherit,NOLOGIN,NOBYPASSRLS" + login_user: "{{ pg_user }}" + db: postgres + register: result + +- name: Check that ansible reports it modified the role + assert: + that: + - "result.changed == True" + +- name: Check that the user has the requested role attributes + become_user: "{{ pg_user }}" + become: True + shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin, 'bypassrls:'||rolbypassrls from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres + register: result + +- assert: + that: + - "result.stdout_lines[-1] == '(1 row)'" + - "'super:f' in result.stdout_lines[-2]" + - "'createrole:f' in result.stdout_lines[-2]" + - "'create:f' in result.stdout_lines[-2]" + - "'inherit:f' in result.stdout_lines[-2]" + - "'login:f' in result.stdout_lines[-2]" + - "'bypassrls:f' in result.stdout_lines[-2]" + +- name: Modify a single role attribute on a user + become_user: "{{ pg_user }}" + become: True + postgresql_user: + name: "{{ db_user1 }}" + state: "present" + role_attr_flags: "LOGIN" + login_user: "{{ pg_user }}" + db: postgres + register: result + +- name: Check that ansible reports it modified the role + assert: + that: + - "result.changed == True" + +- name: Check that the user has the requested role attributes + become_user: "{{ pg_user }}" + become: True + shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin, 'bypassrls:'||rolbypassrls from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres + register: result + +- assert: + that: + - "result.stdout_lines[-1] == '(1 row)'" + - "'super:f' in result.stdout_lines[-2]" + - "'createrole:f' in result.stdout_lines[-2]" + - "'create:f' in result.stdout_lines[-2]" + - "'inherit:f' in result.stdout_lines[-2]" + - "'login:t' in result.stdout_lines[-2]" + - "'bypassrls:f' in result.stdout_lines[-2]" diff --git a/test/integration/targets/postgresql/tasks/postgresql_user_less_than_9.5.yml b/test/integration/targets/postgresql/tasks/postgresql_user_less_than_9.5.yml new file mode 100644 index 0000000000..4f335391ec --- /dev/null +++ b/test/integration/targets/postgresql/tasks/postgresql_user_less_than_9.5.yml @@ -0,0 +1,87 @@ +--- +- name: Create a user with all role attributes + become_user: "{{ pg_user }}" + become: True + postgresql_user: + name: "{{ db_user1 }}" + state: "present" + role_attr_flags: "SUPERUSER,CREATEROLE,CREATEDB,INHERIT,login" + login_user: "{{ pg_user }}" + db: postgres + +- name: Check that the user has the requested role attributes + become_user: "{{ pg_user }}" + become: True + shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres + register: result + +- assert: + that: + - "result.stdout_lines[-1] == '(1 row)'" + - "'super:t' in result.stdout_lines[-2]" + - "'createrole:t' in result.stdout_lines[-2]" + - "'create:t' in result.stdout_lines[-2]" + - "'inherit:t' in result.stdout_lines[-2]" + - "'login:t' in result.stdout_lines[-2]" + +- name: Modify a user to have no role attributes + become_user: "{{ pg_user }}" + become: True + postgresql_user: + name: "{{ db_user1 }}" + state: "present" + role_attr_flags: "NOSUPERUSER,NOCREATEROLE,NOCREATEDB,noinherit,NOLOGIN" + login_user: "{{ pg_user }}" + db: postgres + register: result + +- name: Check that ansible reports it modified the role + assert: + that: + - "result.changed == True" + +- name: Check that the user has the requested role attributes + become_user: "{{ pg_user }}" + become: True + shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres + register: result + +- assert: + that: + - "result.stdout_lines[-1] == '(1 row)'" + - "'super:f' in result.stdout_lines[-2]" + - "'createrole:f' in result.stdout_lines[-2]" + - "'create:f' in result.stdout_lines[-2]" + - "'inherit:f' in result.stdout_lines[-2]" + - "'login:f' in result.stdout_lines[-2]" + +- name: Modify a single role attribute on a user + become_user: "{{ pg_user }}" + become: True + postgresql_user: + name: "{{ db_user1 }}" + state: "present" + role_attr_flags: "LOGIN" + login_user: "{{ pg_user }}" + db: postgres + register: result + +- name: Check that ansible reports it modified the role + assert: + that: + - "result.changed == True" + +- name: Check that the user has the requested role attributes + become_user: "{{ pg_user }}" + become: True + shell: echo "select 'super:'||rolsuper, 'createrole:'||rolcreaterole, 'create:'||rolcreatedb, 'inherit:'||rolinherit, 'login:'||rolcanlogin from pg_roles where rolname='{{ db_user1 }}';" | psql -d postgres + register: result + +- assert: + that: + - "result.stdout_lines[-1] == '(1 row)'" + - "'super:f' in result.stdout_lines[-2]" + - "'createrole:f' in result.stdout_lines[-2]" + - "'create:f' in result.stdout_lines[-2]" + - "'inherit:f' in result.stdout_lines[-2]" + - "'login:t' in result.stdout_lines[-2]"