-
Notifications
You must be signed in to change notification settings - Fork 40
User management
Currently, aconfmgr does not manage users specially; /etc/passwd & co are treated as regular files.
This is sufficient for simple configurations, and has the benefit that changes in these files will show up in the diff when git-diff-ing a configuration after aconfmgr save
.
This approach has some disadvantages:
- All user information is in one place, and information about system users is removed from the rest of their related configuration (related packages and files).
- Monolithic user/group files also cannot conditionally contain some users on some systems.
- If
/etc/shadow
is managed as-is, hashed passwords will appear in the configuration (and will be readable by all who can access theaconfmgr
configuration).
Another approach is to take advantage of bash scripting in the aconfmgr
configuration, and use some helpers to manage the user list.
Create 10-10-user-helpers.sh
with:
#!/bin/bash
rm -rf "$tmp_dir"/usergroups
rm -rf "$tmp_dir"/groups
# Add user to /etc/{passwd,shadow} and corresponding group.
function AddUser() {
local username=$1
local password=$2
local uid=$3
local gid=$4
local gpassword=$5
local desc=$6
local home=$7
local shell=$8
local groups=$9
if [[ -z "$password" ]]
then
FatalError 'Attempting to create user with blank password\n'
fi
AddGroup "$username" "$gpassword" "$gid"
mkdir -p "$output_dir"/files/etc
printf '%s:%s:%d:%d:%s:%s:%s\n' "$username" x "$uid" "$gid" "$desc" "$home" "$shell" >> "$output_dir"/files/etc/passwd
printf '%s:%s:::::::\n' "$username" "$password" >> "$output_dir"/files/etc/shadow
mkdir -p "$tmp_dir"/usergroups
local grouparr=()
IFS=',' read -ra grouparr <<<"$groups"
local group
for group in "${grouparr[@]}"
do
printf "%s\n" "$username" >> "$tmp_dir"/usergroups/"$group"
done
}
# Add group to /etc/{group,gshadow}.
function AddGroup() {
local name=$1
local password=$2
local gid=$3
mkdir -p "$tmp_dir"/groups
printf '%s:%s:%d:\n' "$name" x "$gid" >> "$tmp_dir"/groups/group
printf '%s:%s::\n' "$name" "$password" >> "$tmp_dir"/groups/gshadow
}
function AddUserToGroup() {
local user=$1
local group=$2
printf "%s\n" "$user" >> "$tmp_dir"/usergroups/"$group"
}
And, 98-10-apply-users.sh
, which will compile the information collected by the above helpers into passwd
/group
files:
#!/bin/bash
function AppendGroupMembers() {
while IFS=: read -r name rest
do
local members=()
if [[ -f "$tmp_dir"/usergroups/"$name" ]]
then
mapfile -t members < "$tmp_dir"/usergroups/"$name"
fi
local memberstr
local oIFS=$IFS
IFS=, memberstr="${members[*]}"
IFS=$oIFS
printf '%s:%s%s\n' "$name" "$rest" "$memberstr"
done
}
AppendGroupMembers < "$tmp_dir"/groups/group > "$output_dir"/files/etc/group
AppendGroupMembers < "$tmp_dir"/groups/gshadow > "$output_dir"/files/etc/gshadow
for f in passwd shadow group gshadow
do
cp "$output_dir"/files/etc/"$f"{,-}
if [[ $f == *shadow ]]
then
SetFileProperty /etc/"$f" mode 600
SetFileProperty /etc/"$f"- mode 600
fi
done
Then, throughout your configuration, you can use the helpers when necessary:
AddPackage util-linux # Miscellaneous system utilities for Linux
AddUser uuidd x 68 68 x uuidd / /usr/bin/nologin ''
AddGroup rfkill x 24
AddUserToGroup alice audio
Don't forget to create system users. E.g. for the root user:
AddUser root "$(GetPassword root)" 0 0 '' root /root /bin/bash root,bin,daemon,sys,adm,disk,wheel,log
To convert your existing password/group files to the above helpers, you can use this script:
#!/bin/bash
set -euo pipefail
tmp_dir=tmp
function Die() {
local fmt=$1
shift
# shellcheck disable=SC2059
printf "$fmt\n" "$@" 1>&2
exit 1
}
if [[ -r /etc/shadow ]]
then
printf 'Importing from system configuration.\n' 1>&2
fn_passwd=/etc/passwd
fn_shadow=/etc/shadow
fn_group=/etc/group
fn_gshadow=/etc/gshadow
else
for base in . config ~/.config/aconfmgr
do
if [[ -r "$base"/files/etc/passwd ]]
then
printf 'Importing from configuration in %s.\n' "$base" 1>&2
fn_passwd="$base"/files/etc/passwd
fn_group="$base"/files/etc/group
fn_gshadow="$base"/files/etc/gshadow
if [[ -f "$base"/passwd/shadow ]]
then
fn_shadow="$base"/passwd/shadow
else
fn_shadow="$base"/files/etc/gshadow
fi
break
fi
done
if [[ ! -v fn_passwd ]]
then
Die "Can't find import location - re-run as root to import from system or run from your aconfmgr config dir."
fi
fi
# Read users
declare -A users
user_order=()
while IFS=: read -r username ppassword uid gid desc home shell
do
test -z "${users[$username:seen]+x}" || Die 'Duplicate user in passwd file: %s' "$username"
user_order+=("$username")
users[$username:seen]=y
users[$username:ppassword]=$ppassword
users[$username:uid]=$uid
users[$username:gid]=$gid
users[$username:desc]=$desc
users[$username:home]=$home
users[$username:shell]=$shell
done < $fn_passwd
while IFS=: read -r username password last minage maxage pwwarn pwinact acctexp res
do
test -n "${users[$username:seen]+x}" || Die 'Unknown user in shadow file: %s' "$username"
users[$username:password]=$password
users[$username:last]=$last
users[$username:minage]=$minage
users[$username:maxage]=$maxage
users[$username:pwwarn]=$pwwarn
users[$username:pwinact]=$pwinact
users[$username:acctexp]=$acctexp
users[$username:res]=$res
done < $fn_shadow
# Read groups
declare -A groups
group_order=()
rm -rf "$tmp_dir"/usergroups
mkdir -p "$tmp_dir"/usergroups
while IFS=: read -r gname gpassword gid gmembers
do
test -z "${groups[$gname:seen]+x}" || Die 'Duplicate group in group file %s' "$gname"
group_order+=("$gname")
groups[$gname:seen]=y
groups[$gname:gpassword]=$gpassword
groups[$gname:gid]=$gid
groups[$gname:gmembers]=$gmembers
gmembersarr=()
IFS=, read -ra gmembersarr <<<"$gmembers"
for member in "${gmembersarr[@]}"
do
printf '%s\n' "$gname" >> "$tmp_dir"/usergroups/"$member"
done
done < $fn_group
while IFS=: read -r gname gspassword gsadmins gsmembers
do
test -n "${groups[$gname:seen]+x}" || Die 'Unknown group in gshadow file: %s' "$gname"
groups[$gname:gspassword]=$gspassword
groups[$gname:gsadmins]=$gsadmins
groups[$gname:gsmembers]=$gsmembers
done < $fn_gshadow
# Convert users
declare -A usergroups
rm -f generated-users.sh
for username in "${user_order[@]}"
do
printf 'Processing user %s\n' "$username" 1>&2
for var in ppassword uid gid desc home shell password last minage maxage pwwarn pwinact acctexp res
do
declare $var="${users[$username:$var]}"
done
[[ "$ppassword" == x ]] || Die 'passwd file password is not x'
[[ "$uid" =~ ^[0-9]+$ ]] || Die 'UID not numeric'
[[ "$gid" =~ ^[0-9]+$ ]] || Die 'GID not numeric'
[[ "$home" == /* ]] || Die 'Home directory not absolute path'
[[ "$shell" == /* ]] || Die 'Shell not absolute path'
[[ "$last" =~ ^[0-9]*$ ]] || Die 'Last password change not empty or numeric'
[[ "$minage" =~ ^0?$ ]] || Die 'Minimum password age not empty or 0'
[[ "$maxage" =~ ^(99999|)$ ]] || Die 'Maximum password age not empty or 99999'
[[ "$pwwarn" =~ ^7?$ ]] || Die 'Password warning period not empty or 7'
[[ "$pwinact" == '' ]] || Die 'Password inactivity period not empty'
[[ "$acctexp" == '' ]] || Die 'Account expiration date not empty'
[[ "$res" == '' ]] || Die 'Reserved field not empty'
grouplist=()
if [[ -f "$tmp_dir"/usergroups/"$username" ]]
then
mapfile -t grouplist < "$tmp_dir"/usergroups/"$username"
fi
IFS=, groupstr="${grouplist[*]}"
gpassword="${groups[$username:gspassword]}"
env printf 'AddUser %q %q %d %d %q %q %q %q %q\n' \
"$username" \
"$password" \
"$uid" \
"$gid" \
"$gpassword" \
"$desc" \
"$home" \
"$shell" \
"$groupstr" \
>> generated-users.sh
usergroups["$username"]="$gid"
done
# Convert groups
for gname in "${group_order[@]}"
do
printf 'Processing group %s\n' "$gname" 1>&2
for var in gpassword gid gmembers gspassword gsadmins gsmembers
do
declare $var="${groups[$gname:$var]}"
done
[[ "$gpassword" == x ]] || Die 'group file password is not x'
[[ "$gid" =~ ^[0-9]+$ ]] || Die 'GID not numeric'
[[ "$gspassword" =~ ^(|!|!!|x)$ ]] || Die 'Group password not empty, !, !!, or x: %q' "$gspassword"
[[ "$gsadmins" == '' ]] || Die 'Group administrators not empty'
[[ "$gsmembers" == "$gmembers" ]] || Die 'group/gshadow group members mismatch'
if [[ -n "${usergroups[$gname]+x}" ]]
then
[[ "$gid" == "${usergroups[$gname]}" ]] || Die 'User GID and user'\''s group GID mismatch: %s/%s' "$gid" "${usergroups[$gname]}"
else
env printf 'AddGroup %q %q %d\n' \
"$gname" \
"$gspassword" \
"$gid" \
>> generated-users.sh
fi
done
The script will create a generated-users.sh
file in the current directory, which you can add to your aconfmgr
configuration.
If you plan to push your configuration repository to other machines, you may not want to keep passwords in it. To keep passwords separate from your configuration, you can use a helper such as the following:
# Read the password from a separate file, which is not under version control.
function GetPassword() {
local username=$1
if [[ -f "$config_dir"/data/passwords/"$username" ]]
then
cat "$config_dir"/data/passwords/"$username"
else
# Get password from current machine
sudo cat /etc/shadow | grep "^$username:" | cut -d : -f 2
fi
}
Given a $username
, the function will read the password from $config_dir/data/passwords/$username
(as it appears in /etc/shadow
).
Do not add the passwords
directory to version control (add it to .gitignore
).
If the file is not present, it will attempt to use the current password from the current machine, which will work if you've already manually created all pertinent users and their passwords.