Skip to content

Commit

Permalink
Merge pull request #18 from kysrpex/remap_user_enhancement
Browse files Browse the repository at this point in the history
Enhance remap user tasks from os_setup role
  • Loading branch information
kysrpex authored Dec 12, 2023
2 parents f217355 + 2abfffb commit 01fc46a
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 51 deletions.
168 changes: 168 additions & 0 deletions roles/os_setup/files/remap_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/usr/bin/env python
"""Script used by the `remap_user.yml` task to remap users and groups.
Given a list of existing user or group ids and desired user or group ids,
compute a mapping from existing user or group ids to new user or group ids that
do not conflict with other existing ids nor the desired ids.
For example, assume service "fwupd" has created a user named "fwupd" and the
operating system has assigned UID 999 to it. Furthermore, assume that two new
applications need to run on the server (for example, Galaxy and HTCondor) with
UIDs 999 and 3 respectively (for example, because they need to access a remote
filesystem using NFS and the server has already allocated UIDs 999 and 3 to the
applications, meaning that existing files belonging to the applications are
already owned by UIDs 999 and 3 respectively). In addition, assume the
/etc/passwd file looks as follows:
```
root:x:0:0::/root:/bin/bash
bin:x:1:1::/:/usr/bin/nologin
daemon:x:2:2::/:/usr/bin/nologin
mail:x:8:12::/var/spool/mail:/usr/bin/nologin
fwupd:x:999:962:Firmware update daemon:/var/lib/fwupd:/usr/bin/nologin
```
It is then necessary to remap the "fwupd" user to a different UID so that 999
becomes free for the first application (e.g. Galaxy). This script can find a
new UID for "fwupd" that is different both from 999 and 3. To do so, call it
passing as arguments the existing and desired UIDs as demonstrated below.
```
./remap_user.py \
-e 0:root \
-e 1:bin \
-e 2:daemon \
-e 8:mail \
-d 999:galaxy \
-d 3:condor
```
The output of the call is shown below. Notice that user 999 ("fwupd") does not
get mapped to UID 3, despite it being free, since it is meant to belong to user
"condor".
```
EXISTING TARGET
999 4
```
"""

from argparse import ArgumentParser
from os.path import basename, splitext
from sys import stdout
from typing import Iterator, Mapping


def ordered_complement(integers: set[int]) -> Iterator[int]:
"""Yield from the complement of a set of nonnegative integers.
Yield from the complement of a set of nonnegative integers in ascending
order.
Args:
integers: Set of nonnegative integers to yield the complement of.
Yields:
Nonnegative integers from the complement of the set passed as input, in
ascending order.
"""
i = 0
while True:
if i not in integers:
yield i
i += 1


def resolve_conflicts(
mapping_existing: Mapping[int, str], mapping_desired: Mapping[int, str]
) -> Mapping[int, int]:
"""Resolve conflicts between existing and desired user or group ids.
Given a mapping defining the existing group or user ids and desired user or
group ids, compute a mapping from existing group or user ids to new ids
that do not conflict with other existing ids nor the desired ids.
Args:
mapping_existing: Already defined user or group ids.
mapping_desired: Desired arrangement of user or group ids
(non-exhaustive).
Returns:
Mapping from existing user or group ids to new user or group ids that
resolves the conflicts between existing and desired ids.
"""
conflicts = {
id_
for id_ in mapping_existing.keys() & mapping_desired.keys()
if mapping_existing[id_] != mapping_desired[id_]
}
resolution = {
id_existing: id_target
for id_existing, id_target in zip(
conflicts,
ordered_complement(
mapping_existing.keys() | mapping_desired.keys()
),
)
}
return resolution


def make_parser() -> ArgumentParser:
"""Command line interface for this script."""
parser = ArgumentParser(
prog=splitext(basename(__file__))[0],
description="Remap user or group ids",
)

def convert_argument(string: str) -> tuple[int, str]:
"""Convert a user or group id in the form "ID:NAME" to a tuple."""
id_, name = string.split(":")
id_ = int(id_)
return id_, name

parser.add_argument(
"-e",
"--existing",
dest="existing",
metavar="ID:NAME",
action="extend",
default=[],
nargs="+",
required=True,
type=convert_argument,
help="existing user/group id and name (separated by a colon)",
)
parser.add_argument(
"-d",
"--desired",
dest="desired",
metavar="ID:NAME",
action="extend",
default=[],
nargs="+",
required=True,
type=convert_argument,
help="desired user/group id and name (separated by a colon)",
)

return parser


if __name__ == "__main__":
command_parser = make_parser()
command_args = command_parser.parse_args()

existing = {id_: name for id_, name in command_args.existing}
desired = {id_: name for id_, name in command_args.desired}
mapping = resolve_conflicts(existing, desired)

col_length = max(
len(str(x))
for x in ("EXISTING", "TARGET", *mapping.keys(), *mapping.values())
)
row_fmt = "\t".join((f"{{:>{col_length}}}",) * 2)
if mapping:
print(row_fmt.format("EXISTING", "TARGET"), file=stdout)
for existing_id, target_id in mapping.items():
print(row_fmt.format(existing_id, target_id), file=stdout)
152 changes: 101 additions & 51 deletions roles/os_setup/tasks/remap_user.yml
Original file line number Diff line number Diff line change
@@ -1,74 +1,124 @@
# This role provides a collection of tasks "create_user.yml" that creates the
# users and groups defined in `handy_users` and `handy_groups`. Said tasks
# allow to choose the UID and GID of the users and groups to be created.
#
# However, the specified UIDs and GIDs may already have been taken by other
# users or groups. The tasks in this file, "remap_user.yml", map those users
# and groups to different UIDs and GIDs so that the ones specified in
# `handy_users` and `handy_groups` become available to the new users.
#
# For example, in CentOS 8, UID 999 is allocated to user `systemd-coredump`,
# but in usegalaxy.eu's infrastructure, UID 999 is meant to be assigned to
# user `galaxy` (the Galaxy application). The tasks in this file can remap user
# `systemd-coredump` to another UID so that 999 becomes free to the `galaxy`
# user, that would be created afterward by the tasks in create_user.yml.
#
# A much more detailed example is available in the file
# roles/os_setup/files/remap_user.py.
---
# RHEL uses GID 999 and UID 999, both of which need to be
# mapped to the galaxy user and group.

- name: Get all groups
getent:
- name: Get all groups.
ansible.builtin.getent:
database: group
split: ':'

- name: Get all users
getent:
- name: Get all users.
ansible.builtin.getent:
database: passwd
split: ':'

- name: Remap GID 999 if Galaxy group is not present
- name: Compute user mapping.
become: false
block:
- name: Check for GID 999 group
- name: Compute user mapping using script.
ansible.builtin.shell:
cmd: grep 999 /etc/group
register: check_group
chdir: "{{ role_path }}/files"
cmd: |
set -o pipefail;
./remap_user.py \
{% for name, data in getent_passwd.items() %} -e {{ data[1] }}:{{ name }}{% endfor %} \
{% for user in handy_users %} -d {{ user.user_uid }}:{{ user.user_name }}{% endfor %} \
| tail -n +2
register: os_setup_user_mapping_script
changed_when: false
failed_when: check_group.rc not in (0, 1)
delegate_to: localhost

- name: Print return information from the previous task
ansible.builtin.debug:
var: check_group
when: debug
- name: Process script output.
ansible.builtin.set_fact:
os_setup_user_mapping: "{{ os_setup_user_mapping | default([]) + [item | split('\t') | map('trim') | list] }}"
loop: "{{ os_setup_user_mapping_script.stdout_lines }}"
# Each line of output is of the form below
# " 998\t 7"
# " 999\t 13"
# and the final outcome is a list of lists.
# [["998", "7"], ["999", "9"]]

- name: Replace in group file
ansible.builtin.replace:
path: /etc/group
regexp: '999'
replace: "500"
when: (not ansible_check_mode and check_group.rc == 0 and not check_group.stdout == '')

- name: Search and replace 999 group files
- name: Compute group mapping.
become: false
block:
- name: Compute group mapping using script.
ansible.builtin.shell:
cmd: "find / -mount -gid 999 -exec chgrp 500 '{}' +"
ignore_errors: true
when: check_group.rc == 0
tags:
- ignore_errors
when: (not "galaxy" in getent_group.keys())
chdir: "{{ role_path }}/files"
cmd: |
set -o pipefail;
./remap_user.py \
{% for name, data in getent_group.items() %} -e {{ data[1] }}:{{ name }}{% endfor %} \
{% for group in handy_groups %} -d {{ group.group_gid }}:{{ group.group_name }}{% endfor %} \
| tail -n +2
register: os_setup_group_mapping_script
changed_when: false
delegate_to: localhost

- name: Process script output.
ansible.builtin.set_fact:
os_setup_group_mapping: "{{ os_setup_group_mapping | default([]) + [item | split('\t') | map('trim') | list] }}"
loop: "{{ os_setup_group_mapping_script.stdout_lines }}"
# Each line of output is of the form below
# " 998\t 7"
# " 999\t 13"
# and the final outcome is a list of lists.
# [["998", "7"], ["999", "9"]]

- name: Remap UID 999 if Galaxy user is not present
- name: Replace users.
become: true
when: os_setup_user_mapping is defined
block:
- name: Check for UID 999 in user file
- name: Replace user in users file.
ansible.builtin.replace:
path: /etc/passwd
regexp: "^((?:[^:]*:){2})({{ item.0 }})((?::[^:]*){4})$"
replace: '\g<1>{{ item.1 }}\g<3>'
validate: /usr/sbin/pwck --read-only %s
loop: "{{ os_setup_user_mapping }}"

- name: Search and replace owner of user's files.
ansible.builtin.shell:
cmd: grep 999 /etc/passwd
register: check_user
changed_when: false
failed_when: check_user.rc not in (0, 1)
cmd: "find / -mount -uid {{ item.0 }} -exec chown {{ item.1 }} '{}' +"
loop: "{{ os_setup_user_mapping }}"
changed_when: true

- name: Print return information from the previous task
ansible.builtin.debug:
var: check_user
when: debug
- name: Replace groups.
become: true
when: os_setup_group_mapping is defined
block:
- name: Replace group in groups file.
ansible.builtin.replace:
path: /etc/group
regexp: "^((?:[^:]*:){2})({{ item.0 }})((?::[^:]*){1})$"
replace: '\g<1>{{ item.1 }}\g<3>'
validate: /usr/sbin/grpck --read-only %s
loop: "{{ os_setup_group_mapping }}"

- name: Replace in passwd file
- name: Replace group in users file.
ansible.builtin.replace:
path: /etc/passwd
regexp: '999'
replace: "500"
validate: /usr/sbin/pwck %s
when: (not ansible_check_mode and check_user.rc == 0 and not check_user.stdout == '')
regexp: "^((?:[^:]*:){3})({{ item.0 }})((?::[^:]*){3})$"
replace: '\g<1>{{ item.1 }}\g<3>'
validate: /usr/sbin/pwck --read-only %s
loop: "{{ os_setup_group_mapping }}"

- name: Search and replace 999 user files
- name: Search and replace group of group's files.
ansible.builtin.shell:
cmd: "find / -mount -uid 999 -exec chown 500 '{}' +"
ignore_errors: true
when: check_user.rc == 0
tags:
- ignore_errors
when: (not "galaxy" in getent_passwd.keys())
cmd: "find / -mount -gid {{ item.0 }} -exec chgrp {{ item.1 }} '{}' +"
loop: "{{ os_setup_group_mapping }}"
changed_when: true

0 comments on commit 01fc46a

Please sign in to comment.