From 3d1c5218f7c028cc321cfcb46b323f32fcb5060f Mon Sep 17 00:00:00 2001 From: Nils Philippsen Date: Tue, 9 Feb 2021 17:54:24 +0100 Subject: [PATCH] ipa/client: Combine operations on the IPA server The reason for this is to avoid having to do the same or similar things over and over again for each host in the play, especially since these operations are delegated to the IPA server, i.e. had to run sequentially host after host in order to avoid race conditions. To achieve this, the IPA client related group variables are prepared in suitable structures in `prepare-ipa-info.yml` and consumed by `common.yml`, `hbac.yml` and `sudo.yml`, which do most operations in one go per e.g. host group on the affected IPA server(s). Additionally: - Remove compat for legacy `fas_client_groups`, only check for its presence and warn. - Remove the prepared but masked out task to manage password-less sudo access. - Make yamllint a little happier on the changed files. Signed-off-by: Nils Philippsen --- roles/ipa/client/tasks/cleanup.yml | 1 + roles/ipa/client/tasks/common.yml | 38 ++++-- roles/ipa/client/tasks/hbac.yml | 91 ++++++++----- roles/ipa/client/tasks/main.yml | 19 ++- roles/ipa/client/tasks/prepare-ipa-info.yml | 137 ++++++++++++++++++++ roles/ipa/client/tasks/sudo.yml | 40 ++---- 6 files changed, 248 insertions(+), 78 deletions(-) create mode 100644 roles/ipa/client/tasks/prepare-ipa-info.yml diff --git a/roles/ipa/client/tasks/cleanup.yml b/roles/ipa/client/tasks/cleanup.yml index 984f05a65b..8a4a70a5bd 100644 --- a/roles/ipa/client/tasks/cleanup.yml +++ b/roles/ipa/client/tasks/cleanup.yml @@ -1,3 +1,4 @@ +--- # REMOVEME: As soon as all (affected) hosts have been migrated over from fas_client, 2fa_client to # ipa/client, this can go away. # diff --git a/roles/ipa/client/tasks/common.yml b/roles/ipa/client/tasks/common.yml index 1cb244a1b3..f5fd4d10a6 100644 --- a/roles/ipa/client/tasks/common.yml +++ b/roles/ipa/client/tasks/common.yml @@ -1,22 +1,38 @@ +--- +## This will only run once per play (as per `main.yml`), so needs to consider all affected hosts. + ## Cluster-wide rules -- name: Check that sysadmin-main group exists - command: "getent group sysadmin-main" - changed_when: False +# This is supposed to fail if a configured group doesn't exist. In this case, either add the group +# manually or remove the reference from configuration. + +- name: Check that sysadmin-main and referenced groups exist on IPA server(s) + delegate_to: "{{ item[0] }}" + command: "getent group {{ item[1] }}" + changed_when: false + loop: >- + {{ + ipa_server_user_groups + + (ipa_server_all_groups_hosts_dict | list | product(['sysadmin-main']) | list) + }} ## Rules specific to hosts, host groups -- name: Ensure IPA host group exists +- name: Ensure IPA host groups exist on IPA server(s) + delegate_to: "{{ item[0] }}" ipahostgroup: - name: "{{ ipa_host_group }}" - description: "{{ ipa_host_group_desc | default(omit) }}" - ipaadmin_password: "{{ ipa_admin_password }}" + name: "{{ item[1] }}" + description: "{{ ipa_server_host_groups_dict[item[1]].desc | default(omit) }}" + ipaadmin_password: "{{ ipa_server_admin_passwords[item[0]] }}" state: present + loop: "{{ ipa_server_host_groups }}" -- name: Ensure host is in IPA host group +- name: Ensure hosts are in IPA host groups + delegate_to: "{{ item[0] }}" ipahostgroup: - name: "{{ ipa_host_group }}" - ipaadmin_password: "{{ ipa_admin_password }}" + name: "{{ item[1] }}" + ipaadmin_password: "{{ ipa_server_admin_passwords[item[0]] }}" action: member state: present - host: "{{ inventory_hostname }}" + host: "{{ item[2] | list }}" + loop: "{{ ipa_server_host_groups_hosts }}" diff --git a/roles/ipa/client/tasks/hbac.yml b/roles/ipa/client/tasks/hbac.yml index 72ed8b48f8..ad3aebef27 100644 --- a/roles/ipa/client/tasks/hbac.yml +++ b/roles/ipa/client/tasks/hbac.yml @@ -1,80 +1,101 @@ +--- +## This will only run once per play (as per `main.yml`), so needs to consider all affected hosts. + ## Cluster-wide rules - name: "Give members of group sysadmin-main access to anything, anywhere" + delegate_to: "{{ item.key }}" ipahbacrule: name: "usergroup/sysadmin-main" description: "Give members of group sysadmin-main access to anything, anywhere" hostcategory: "all" servicecategory: "all" - ipaadmin_password: "{{ ipa_admin_password }}" + ipaadmin_password: "{{ item.value }}" state: present group: - sysadmin-main + loop: "{{ ipa_server_admin_passwords | dict2items }}" - name: "Enable usergroup/sysadmin-main HBAC rule" + delegate_to: "{{ item.key }}" ipahbacrule: name: "usergroup/sysadmin-main" - ipaadmin_password: "{{ ipa_admin_password }}" + ipaadmin_password: "{{ item.value }}" state: enabled + loop: "{{ ipa_server_admin_passwords | dict2items }}" - name: "Disable allow_all HBAC rule" + delegate_to: "{{ item.key }}" ipahbacrule: name: allow_all - ipaadmin_password: "{{ ipa_admin_password }}" + ipaadmin_password: "{{ item.value }}" state: disabled + loop: "{{ ipa_server_admin_passwords | dict2items }}" - name: "Let everybody run sudo" + delegate_to: "{{ item.key }}" ipahbacrule: name: "all-users/sudo" description: "Allow all users to execute the sudo command" state: present - ipaadmin_password: "{{ ipa_admin_password }}" + ipaadmin_password: "{{ item.value }}" hostcategory: "all" usercategory: "all" hbacsvcgroup: - Sudo + loop: "{{ ipa_server_admin_passwords | dict2items }}" -## Host-specific rules +- name: Add the sshd HBAC service in IPA + delegate_to: "{{ item.key }}" + ipahbacsvc: + name: sshd + description: SSH daemon + ipaadmin_password: "{{ item.value }}" + loop: "{{ ipa_server_admin_passwords | dict2items }}" + +- name: Add the shell-access service group in IPA + delegate_to: "{{ item.key }}" + ipahbacsvcgroup: + name: shell-access + description: Group of shell access services + ipaadmin_password: "{{ item.value }}" + hbacsvc: + - sshd + loop: "{{ ipa_server_admin_passwords | dict2items }}" + +## Host group- & host-specific rules # shell access - name: "Warn if `fas_client_groups` is set" fail: - msg: "`fas_client_groups` is defined, should be converted to a list and named `ipa_client_shell_groups`" + msg: >- + `fas_client_groups` is defined, please convert to a (group var) list named + `ipa_client_shell_groups` ignore_errors: true when: fas_client_groups is defined -- name: "Convert `fas_client_groups` string to `ipa_client_shell_groups` list if missing" - set_fact: - ipa_client_shell_groups: "{{ fas_client_groups.split(',') | list }}" - when: fas_client_groups is defined and ipa_client_shell_groups is not defined +- name: "Warn if IPA client variables are unset" + fail: + msg: "`{{ item }}` is not defined" + ignore_errors: true + when: lookup('vars', item, default="") == "" + loop: + - ipa_host_group + - ipa_host_group_desc + - ipa_client_shell_groups + - ipa_client_sudo_groups -- name: Add the sshd HBAC service in IPA - ipahbacsvc: - name: sshd - description: SSH daemon - ipaadmin_password: "{{ ipa_admin_password }}" - -- name: Add the shell-access service group in IPA - ipahbacsvcgroup: - name: shell-access - description: Group of shell access services - ipaadmin_password: "{{ ipa_admin_password }}" - hbacsvc: - - sshd - -- name: Check that shell access user groups exist - command: "getent group {{ item }}" - changed_when: False - loop: "{{ (ipa_client_shell_groups | default([])) | list }}" - -- name: "Give certain groups shell access on host group {{ ipa_host_group }}" +- name: Give certain groups shell access per host group + delegate_to: "{{ item[0] }}" ipahbacrule: - name: "hostgroup/{{ ipa_host_group }}/shell-access" - description: "Give certain groups shell access on host group {{ ipa_host_group }}" - ipaadmin_password: "{{ ipa_admin_password }}" + name: "hostgroup/{{ item[1] }}/shell-access" + description: "Grant shell access on host group {{ item[1] }}" + ipaadmin_password: "{{ ipa_server_admin_passwords[item[0]] }}" + action: member hbacsvcgroup: - shell-access state: present - group: "{{ ipa_client_shell_groups | default([]) | list }}" - hostgroup: "{{ ipa_host_group }}" + group: "{{ ipa_server_host_groups_dict[item[0]][item[1]]['shell_groups'] }}" + hostgroup: "{{ item[1] }}" + loop: "{{ ipa_server_host_groups }}" diff --git a/roles/ipa/client/tasks/main.yml b/roles/ipa/client/tasks/main.yml index e0d591b4d1..7a530bd784 100644 --- a/roles/ipa/client/tasks/main.yml +++ b/roles/ipa/client/tasks/main.yml @@ -1,3 +1,4 @@ +--- - name: Install IPA client packages package: name: @@ -29,15 +30,23 @@ - ipa/client - fas-client-cleanup -- name: Basic configuration for client on IPA cluster - delegate_to: "{{ ipa_server }}" +- name: Prepare IPA-related information to make the following more efficient + delegate_to: localhost + import_tasks: prepare-ipa-info.yml + tags: + - ipa/client + - config + run_once: yes + +- name: Basic configuration for clients on IPA cluster + delegate_to: localhost import_tasks: common.yml # don't muck with prod for now when: env == 'staging' tags: - ipa/client - config - throttle: 1 + run_once: yes - name: Configure HBAC on IPA cluster delegate_to: "{{ ipa_server }}" @@ -47,7 +56,7 @@ tags: - ipa/client - config - throttle: 1 + run_once: yes - name: Configure sudo on IPA cluster delegate_to: "{{ ipa_server }}" @@ -57,4 +66,4 @@ tags: - ipa/client - config - throttle: 1 + run_once: yes diff --git a/roles/ipa/client/tasks/prepare-ipa-info.yml b/roles/ipa/client/tasks/prepare-ipa-info.yml new file mode 100644 index 0000000000..cfb8835c52 --- /dev/null +++ b/roles/ipa/client/tasks/prepare-ipa-info.yml @@ -0,0 +1,137 @@ +--- +# NOTE: configuration is based on host groups, i.e. set the ipa_* vars only in group_vars + +# Thanks to having two environments, staging and prod, this has to deal with the "responsible" IPA +# server for individual hosts. + +# ipa_server_host_groups_dict -> +# { +# "ipa_server_1": { +# "host_group_1": { +# "shell_groups": [...], +# "sudo_groups": [...], +# "hosts": { # <-- This could be a list with Ansible >= 2.10 +# "host_1": true, +# ..., +# } +# }, ... +# }, ... +# } +# +# ipa_server_all_groups_hosts_dict -> +# { +# "ipa_server_1": { +# groups: [...], +# hosts: { +# "host_1": true, +# ..., +# } +# }, ... +# } +# +# ipa_server_passwords: -> +# { +# "ipa_server_1": "ipa_password_1", +# "ipa_server_2": "ipa_password_2", +# ... +# } +- name: Create dictionary about IPA servers, host groups, their IPA vars and contained hosts + set_fact: + ipa_server_host_groups_dict: >- + {{ + ipa_server_host_groups_dict | default({}) | combine( + { + hostvars[item]['ipa_server']: { + hostvars[item]['ipa_host_group']: { + 'desc': hostvars[item]['ipa_host_group_desc'] | default(omit), + 'shell_groups': hostvars[item]['ipa_client_shell_groups'] | default(omit), + 'sudo_groups': hostvars[item]['ipa_client_sudo_groups'] | default(omit), + 'hosts': {item: true}, + } + } + }, + recursive=True + ) + }} + ipa_server_all_groups_hosts_dict: >- + {{ + (ipa_server_all_groups_hosts_dict | default({})) | combine( + { + hostvars[item]['ipa_server']: { + 'groups': hostvars[item]['ipa_client_shell_groups'] | default([]) | union( + hostvars[item]['ipa_client_sudo_groups'] | default([]) + ), + 'hosts': {item: True}, + } + }, + recursive=True + ) + }} + ipa_server_admin_passwords: >- + {{ + (ipa_server_admin_passwords | default({})) | combine( + {hostvars[item]['ipa_server']: hostvars[item]['ipa_admin_password']} + ) + }} + loop: "{{ ansible_play_hosts }}" + when: hostvars[item]['ipa_server'] is defined and hostvars[item]['ipa_host_group'] is defined + +# ipa_server_host_groups -> +# [ +# ["ipa_server_1", "host_group_1"], +# ["ipa_server_1", "host_group_2"], +# ... +# ["ipa_server_2", "host_group_1"], +# ... +# ] +- name: Transform ipa_server_host_groups_dict into an iterable list + set_fact: + ipa_server_host_groups: >- + {{ + (ipa_server_host_groups | default([])) + + ([item.key] | product(item.value | list) | list) + }} + loop: "{{ ipa_server_host_groups_dict | dict2items }}" + +# ipa_server_host_user_groups_hosts -> +# [ +# [ +# "ipa_server_1", +# "host_group_1", +# ["user_group_1", ...], # <-- shell access user groups +# ["user_group_2", ...], # <-- sudo access user groups +# ["host_1", ...], +# ], +# [ +# "ipa_server_1", +# "host_group_2", +# ["user_group_3", ...], +# ["user_group_4", ...], +# ["host_2", ...], +# ], +# ... +# [ +# "ipa_server_2", +# "host_group_1", +# ["user_group_5", ...], +# ["user_group_6", ...], +# ["host_3", ...], +# ], +# ... +# ] +- name: Make semi-flat list of IPA servers, host groups and the user groups and hosts contained + set_fact: + ipa_server_host_user_groups_hosts: >- + {{ + ipa_server_host_user_groups_hosts | default([]) + + [ + [ + item[0], + item[1], + ipa_server_host_groups_dict[item[0]][item[1]]['shell_groups'], + ipa_server_host_groups_dict[item[0]][item[1]]['sudo_groups'], + ipa_server_host_groups_dict[item[0]][item[1]]['hosts'] | list, + ] + ] + }} + loop: "{{ ipa_server_host_groups }}" diff --git a/roles/ipa/client/tasks/sudo.yml b/roles/ipa/client/tasks/sudo.yml index 350594eac4..c2ae4f6f17 100644 --- a/roles/ipa/client/tasks/sudo.yml +++ b/roles/ipa/client/tasks/sudo.yml @@ -1,13 +1,12 @@ -- name: Check that configured sudo groups exist - command: "getent group {{ item }}" - changed_when: False - loop: "{{ (ipa_client_sudo_groups | default([])) + (ipa_client_sudo_nopasswd_groups | default([])) | list }}" +--- +## This will only run once per play (as per `main.yml`), so needs to consider all affected hosts. - name: "Give members of `sysadmin-main` sudo access to anything, anywhere" + delegate_to: "{{ item.key }}" ipasudorule: name: "usergroup/sysadmin-main" description: "Allow members of `sysadmin-main` to use sudo to do anything, anywhere" - ipaadmin_password: "{{ ipa_admin_password }}" + ipaadmin_password: "{{ item.value }}" state: present cmdcategory: "all" hostcategory: "all" @@ -15,31 +14,18 @@ runasgroupcategory: "all" group: - sysadmin-main + loop: "{{ ipa_server_admin_passwords | dict2items }}" -- name: "Give certain groups sudo access to anything on host group {{ ipa_host_group }}" +- name: Give certain groups sudo access to anything per host group + delegate_to: "{{ item[0] }}" ipasudorule: - name: "hostgroup/{{ ipa_host_group }}" - description: "Give members of groups sudo access to anything on host group {{ ipa_host_group }}" - ipaadmin_password: "{{ ipa_admin_password }}" + name: "hostgroup/{{ item[1] }}" + description: "Grant sudo access to anything on host group {{ item[1] }}" + ipaadmin_password: "{{ ipa_server_admin_passwords[item[0]] }}" state: present - group: "{{ ipa_client_sudo_groups | list }}" - hostgroup: "{{ ipa_host_group }}" + group: "{{ ipa_server_host_groups_dict[item[0]][item[1]]['sudo_groups'] }}" + hostgroup: "{{ item[1] }}" cmdcategory: "all" runasusercategory: "all" runasgroupcategory: "all" - when: ipa_client_sudo_groups is defined and ipa_client_sudo_groups | length > 0 - -## Disabled: Remove "False and" from when: to re-enable -- name: "Give certain groups password-less sudo access to anything on host group {{ ipa_host_group }}" - ipasudorule: - name: "hostgroup/{{ ipa_host_group }}/nopasswd" - description: "Give members of groups password-less sudo access to anything on host group {{ ipa_host_group }}" - ipaadmin_password: "{{ ipa_admin_password }}" - state: present - group: "{{ ipa_client_sudo_groups_nopasswd | list }}" - hostgroup: "{{ ipa_host_group }}" - cmdcategory: "all" - runasusercategory: "all" - runasgroupcategory: "all" - options: "!authenticate" - when: False and ipa_client_sudo_groups_nopasswd is defined and ipa_client_sudo_groups_nopasswd | length > 0 + loop: "{{ ipa_server_host_groups }}"