diff mbox series

[1/3] rootfs-postcommands: change sysusers.d command

Message ID 20230613-sysusersd-v1-1-eaddf3179773@baylibre.com
State Accepted, archived
Commit 0c7e76df68acfeca059a6b906d2a891d56f01e77
Headers show
Series rootfs-postcommands: replace the sysusers.d postcommand | expand

Commit Message

Louis Rannou June 15, 2023, 11:43 a.m. UTC
The configuration in sysusers.d used to be parsed to create users/groups at
build time instead at runtime. This was leading to several conflicts with
users/groups defined in base-passwd recipe and specific definitions in recipes
inheriting the useradd class. Some of those conflicts raised unseen errors in
the do_rootfs command's logs.

As an example, the root home directory is set by default to `/home/root` but
systemd expects it as `/root`.

The new command `systemd_sysusers_check` checks each configuration for
users/groups and compare their properties to what is actually defined in the
`/etc/passwd` and `/etc/group` of the target rootfs.

Signed-off-by: Louis Rannou <lrannou@baylibre.com>
---
 meta/classes-recipe/rootfs-postcommands.bbclass | 133 +++++++++++++++++++-----
 1 file changed, 109 insertions(+), 24 deletions(-)
diff mbox series

Patch

diff --git a/meta/classes-recipe/rootfs-postcommands.bbclass b/meta/classes-recipe/rootfs-postcommands.bbclass
index 690fa976aa..652601b95f 100644
--- a/meta/classes-recipe/rootfs-postcommands.bbclass
+++ b/meta/classes-recipe/rootfs-postcommands.bbclass
@@ -43,7 +43,7 @@  ROOTFS_POSTUNINSTALL_COMMAND =+ "write_image_manifest ; "
 POSTINST_LOGFILE ?= "${localstatedir}/log/postinstall.log"
 # Set default target for systemd images
 SYSTEMD_DEFAULT_TARGET ?= '${@bb.utils.contains_any("IMAGE_FEATURES", [ "x11-base", "weston" ], "graphical.target", "multi-user.target", d)}'
-ROOTFS_POSTPROCESS_COMMAND += '${@bb.utils.contains("DISTRO_FEATURES", "systemd", "set_systemd_default_target; systemd_create_users;", "", d)}'
+ROOTFS_POSTPROCESS_COMMAND += '${@bb.utils.contains("DISTRO_FEATURES", "systemd", "set_systemd_default_target; systemd_sysusers_check;", "", d)}'
 
 ROOTFS_POSTPROCESS_COMMAND += 'empty_var_volatile;'
 
@@ -69,29 +69,114 @@  python () {
     d.appendVar('ROOTFS_POSTPROCESS_COMMAND', 'rootfs_reproducible;')
 }
 
-systemd_create_users () {
-	for conffile in ${IMAGE_ROOTFS}/usr/lib/sysusers.d/*.conf; do
-		[ -e $conffile ] || continue
-		grep -v "^#" $conffile | sed -e '/^$/d' | while read type name id comment; do
-		if [ "$type" = "u" ]; then
-			useradd_params="--shell /sbin/nologin"
-			[ "$id" != "-" ] && useradd_params="$useradd_params --uid $id"
-			[ "$comment" != "-" ] && useradd_params="$useradd_params --comment $comment"
-			useradd_params="$useradd_params --system $name"
-			eval useradd --root ${IMAGE_ROOTFS} $useradd_params || true
-		elif [ "$type" = "g" ]; then
-			groupadd_params=""
-			[ "$id" != "-" ] && groupadd_params="$groupadd_params --gid $id"
-			groupadd_params="$groupadd_params --system $name"
-			eval groupadd --root ${IMAGE_ROOTFS} $groupadd_params || true
-		elif [ "$type" = "m" ]; then
-			group=$id
-			eval groupadd --root ${IMAGE_ROOTFS} --system $group || true
-			eval useradd --root ${IMAGE_ROOTFS} --shell /sbin/nologin --system $name --no-user-group || true
-			eval usermod --root ${IMAGE_ROOTFS} -a -G $group $name
-		fi
-		done
-	done
+# Resolve the ID as described in the sysusers.d(5) manual: ID can be a numeric
+# uid, a couple uid:gid or uid:groupname or it is '-' meaning leaving it
+# automatic or it can be a path. In the latter, the uid/gid matches the
+# user/group owner of that file.
+def resolve_sysusers_id(d, sid):
+    # If the id is a path, the uid/gid matchs to the target's uid/gid in the
+    # rootfs.
+    if '/' in sid:
+        try:
+            osstat = os.stat(os.path.join(d.getVar('IMAGE_ROOTFS'), sid))
+        except FileNotFoundError:
+            bb.error('sysusers.d: file %s is required but it does not exist in the rootfs', sid)
+            return ('-', '-')
+        return (osstat.st_uid, osstat.st_gid)
+    # Else it is a uid:gid or uid:groupname syntax
+    if ':' in sid:
+        return sid.split(':')
+    else:
+        return (sid, '-')
+
+# Check a user exists in the rootfs password file and return its properties
+def check_user_exists(d, uname=None, uid=None):
+    with open(os.path.join(d.getVar('IMAGE_ROOTFS'), 'etc/passwd'), 'r') as pwfile:
+        for line in pwfile:
+            (name, _, u_id, gid, comment, homedir, ushell) = line.strip().split(':')
+            if uname == name or uid == u_id:
+                return (name, u_id, gid, comment or '-', homedir or '/', ushell or '-')
+    return None
+
+# Check a group exists in the rootfs group file and return its properties
+def check_group_exists(d, gname=None, gid=None):
+    with open(os.path.join(d.getVar('IMAGE_ROOTFS'), 'etc/group'), 'r') as gfile:
+        for line in gfile:
+            (name, _, g_id, _) = line.strip().split(':')
+            if name == gname or g_id == gid:
+                return (name, g_id)
+    return None
+
+def compare_users(user, e_user):
+    # user and e_user must not have None values. Unset values must be '-'.
+    (name, uid, gid, comment, homedir, ushell) = user
+    (e_name, e_uid, e_gid, e_comment, e_homedir, e_ushell) = e_user
+    # Ignore 'uid', 'gid' or 'comment' if they are not set
+    # Ignore 'shell' and 'ushell' if one is not set
+    return name == e_name \
+        and (uid == '-' or uid == e_uid) \
+        and (gid == '-' or gid == e_gid) \
+        and (comment == '-' or e_comment == '-' or comment.lower() == e_comment.lower()) \
+        and (homedir == '-' or e_homedir == '-' or homedir == e_homedir) \
+        and (ushell == '-' or e_ushell == '-' or ushell == e_ushell)
+
+# Open sysusers.d configuration files and parse each line to check the users and
+# groups are already defined in /etc/passwd and /etc/groups with similar
+# properties. Refer to the sysusers.d(5) manual for its syntax.
+python systemd_sysusers_check() {
+    import glob
+    import re
+
+    pattern_comment = r'(-|\"[^:\"]+\")'
+    pattern_word    = r'[^\s]+'
+    pattern_line   = r'(' + pattern_word + r')\s+(' + pattern_word + r')\s+(' + pattern_word + r')(\s+' \
+        + pattern_comment + r')?' + r'(\s+(' + pattern_word + r'))?' + r'(\s+(' + pattern_word + r'))?'
+
+    for conffile in glob.glob(os.path.join(d.getVar('IMAGE_ROOTFS'), 'usr/lib/sysusers.d/*.conf')):
+        with open(conffile, 'r') as f:
+            for line in f:
+                line = line.strip()
+                if not len(line) or line[0] == '#': continue
+                ret = re.fullmatch(pattern_line, line.strip())
+                if not ret: continue
+                (stype, sname, sid, _, scomment, _, shomedir, _, sshell) = ret.groups()
+                if stype == 'u':
+                    if sid:
+                        (suid, sgid) = resolve_sysusers_id(d, sid)
+                        if sgid.isalpha():
+                            sgid = check_group_exists(d, gname=sgid)
+                        elif sgid.isdigit():
+                            check_group_exists(d, gid=sgid)
+                        else:
+                            sgid = '-'
+                    else:
+                        suid = '-'
+                        sgid = '-'
+                    scomment = scomment.replace('"', '') if scomment else '-'
+                    shomedir = shomedir or '-'
+                    sshell = sshell or '-'
+                    e_user = check_user_exists(d, uname=sname)
+                    if not e_user:
+                        bb.warn('User %s has never been defined' % sname)
+                    elif not compare_users((sname, suid, sgid, scomment, shomedir, sshell), e_user):
+                        bb.warn('User %s has been defined as (%s) but sysusers.d expects it as (%s)'
+                                % (sname, ', '.join(e_user),
+                                ', '.join((sname, suid, sgid, scomment, shomedir, sshell))))
+                elif stype == 'g':
+                    gid = sid or '-'
+                    if '/' in gid:
+                        (_, gid) = resolve_sysusers_id(d, sid)
+                    e_group = check_group_exists(d, gname=sname)
+                    if not e_group:
+                        bb.warn('Group %s has never been defined' % sname)
+                    elif gid != '-':
+                        (_, e_gid) = e_group
+                        if gid != e_gid:
+                            bb.warn('Group %s has been defined with id (%s) but sysusers.d expects gid (%s)'
+                                    % (sname, e_gid, gid))
+                elif stype == 'm':
+                    check_user_exists(d, sname)
+                    check_group_exists(d, sid)
 }
 
 #