# User and group management tasks - name: Ensure admin group exists when defined ansible.builtin.group: name: "{{ admin_group }}" when: admin_group | default('') | length > 0 - name: Ensure managed groups exist ansible.builtin.group: name: "{{ item.name }}" gid: "{{ item.gid | default(omit) }}" state: "{{ item.state | default('present') }}" loop: "{{ managed_groups | default([]) }}" loop_control: label: "{{ item.name }}" - name: Ensure primary groups for managed users exist ansible.builtin.group: name: "{{ item.primary_group }}" gid: "{{ item.primary_gid | default(omit) }}" state: present loop: "{{ managed_users | default([]) }}" loop_control: label: "{{ item.name }}" when: item.primary_group is defined - name: Ensure managed users are present ansible.builtin.user: name: "{{ item.name }}" uid: "{{ item.uid | default(omit) }}" shell: "{{ item.shell | default('/bin/bash') }}" home: "{{ item.home | default(omit) }}" group: "{{ item.primary_group | default(omit) }}" groups: "{{ (user_groups | join(',')) if user_groups else omit }}" append: "{{ true if user_groups else omit }}" create_home: "{{ item.create_home | default(true) }}" state: "{{ item.state | default('present') }}" vars: user_groups: >- {{ ((item.groups | default([])) + ([admin_group] if item.admin | default(false) and admin_group | default('') | length > 0 else [])) | unique }} loop: "{{ managed_users | default([]) }}" loop_control: label: "{{ item.name }}" when: item.state | default('present') != 'absent' register: user_result failed_when: - user_result is failed - '"currently used by process" not in user_result.msg' - '"does not exist in /etc/passwd" not in user_result.msg' - name: Remove managed users marked absent ansible.builtin.user: name: "{{ item.name }}" state: absent remove: "{{ item.remove | default(false) }}" loop: "{{ managed_users | default([]) }}" loop_control: label: "{{ item.name }}" when: item.state | default('present') == 'absent' - name: Collect unique GitHub usernames for key fetch ansible.builtin.set_fact: _gh_usernames: >- {{ managed_users | default([]) | map(attribute='authorized_keys', default=[]) | flatten | selectattr('type', 'defined') | selectattr('type', 'equalto', 'github') | map(attribute='value') | unique | list }} when: managed_users | default([]) | length > 0 - name: Fetch GitHub SSH keys ansible.builtin.uri: url: "https://github.com/{{ item }}.keys" return_content: true loop: "{{ _gh_usernames | default([]) }}" register: _gh_fetched delegate_to: localhost run_once: true become: false when: _gh_usernames | default([]) | length > 0 - name: Build GitHub keys lookup dict ansible.builtin.set_fact: _gh_keys: "{{ _gh_keys | default({}) | combine({item.item: item.content}) }}" loop: "{{ _gh_fetched.results | default([]) }}" when: _gh_fetched.results | default([]) | length > 0 no_log: true - name: Install authorized keys from GitHub ansible.posix.authorized_key: user: "{{ item.0.name }}" state: "{{ item.1.state | default('present') }}" key: "{{ _gh_keys[item.1.value] }}" manage_dir: "{{ item.1.manage_dir | default(true) }}" exclusive: "{{ item.1.exclusive | default(false) }}" loop: "{{ managed_users | default([]) | subelements('authorized_keys', skip_missing=True) }}" loop_control: label: "{{ item.0.name }} (github: {{ item.1.value }})" when: - item.1.type | default('github') == 'github' - item.0.state | default('present') != 'absent' - name: Install authorized keys from files ansible.posix.authorized_key: user: "{{ item.0.name }}" state: "{{ item.1.state | default('present') }}" key: "{{ lookup('file', item.1.value) }}" manage_dir: "{{ item.1.manage_dir | default(true) }}" exclusive: "{{ item.1.exclusive | default(false) }}" loop: "{{ managed_users | default([]) | subelements('authorized_keys', skip_missing=True) }}" loop_control: label: "{{ item.0.name }} (file: {{ item.1.value }})" when: - item.1.type | default('github') == 'file' - item.0.state | default('present') != 'absent' - name: Install authorized keys from inline strings ansible.posix.authorized_key: user: "{{ item.0.name }}" state: "{{ item.1.state | default('present') }}" key: "{{ item.1.value }}" manage_dir: "{{ item.1.manage_dir | default(true) }}" exclusive: "{{ item.1.exclusive | default(false) }}" loop: "{{ managed_users | default([]) | subelements('authorized_keys', skip_missing=True) }}" loop_control: label: "{{ item.0.name }} (string key)" when: - item.1.type | default('github') == 'string' - item.0.state | default('present') != 'absent' - name: Configure sudo for ansible user block: - name: Render sudoers content for ansible ansible.builtin.set_fact: _sudoers_ansible_content: "{{ lookup('template', 'sudoers_ansible.j2') }}" - name: Check current sudoers for ansible ansible.builtin.slurp: path: "{{ sudoers_dir }}/ansible" register: _sudoers_ansible_current failed_when: false - name: Write and validate sudoers for ansible ansible.builtin.copy: content: "{{ _sudoers_ansible_content }}" dest: "{{ sudoers_dir }}/ansible" mode: '0440' validate: "{{ visudo_path }} -cf %s" when: - _sudoers_ansible_current.content | default('') | b64decode != _sudoers_ansible_content - name: Configure sudo for graham user block: - name: Render sudoers content for graham ansible.builtin.set_fact: _sudoers_graham_content: "{{ lookup('template', 'sudoers_graham.j2') }}" - name: Check current sudoers for graham ansible.builtin.slurp: path: "{{ sudoers_dir }}/graham" register: _sudoers_graham_current failed_when: false - name: Write and validate sudoers for graham ansible.builtin.copy: content: "{{ _sudoers_graham_content }}" dest: "{{ sudoers_dir }}/graham" mode: '0440' validate: "{{ visudo_path }} -cf %s" when: - _sudoers_graham_current.content | default('') | b64decode != _sudoers_graham_content - name: Enable passwordless sudo for admin group ansible.builtin.lineinfile: path: "{{ sudoers_path }}" state: present regexp: '^%{{ admin_group }}' line: '{{ admin_group }} ALL=(ALL) NOPASSWD: ALL' validate: "{{ visudo_path }} -cf %s"