nixpkgs/nixos/modules/services/system/userborn.nix
2024-10-05 21:40:09 -04:00

184 lines
5.2 KiB
Nix

{
utils,
config,
lib,
pkgs,
...
}:
let
cfg = config.services.userborn;
userCfg = config.users;
userbornConfig = {
groups = lib.mapAttrsToList (username: opts: {
inherit (opts) name gid members;
}) config.users.groups;
users = lib.mapAttrsToList (username: opts: {
inherit (opts)
name
uid
group
description
home
password
hashedPassword
hashedPasswordFile
initialPassword
initialHashedPassword
;
isNormal = opts.isNormalUser;
shell = utils.toShellPath opts.shell;
}) config.users.users;
};
userbornConfigJson = pkgs.writeText "userborn.json" (builtins.toJSON userbornConfig);
immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
# The filenames created by userborn.
passwordFiles = [
"group"
"passwd"
"shadow"
];
in
{
options.services.userborn = {
enable = lib.mkEnableOption "userborn";
package = lib.mkPackageOption pkgs "userborn" { };
passwordFilesLocation = lib.mkOption {
type = lib.types.str;
default = if immutableEtc then "/var/lib/nixos" else "/etc";
defaultText = lib.literalExpression ''if immutableEtc then "/var/lib/nixos" else "/etc"'';
description = ''
The location of the original password files.
If this is not `/etc`, the files are symlinked from this location to `/etc`.
The primary motivation for this is an immutable `/etc`, where we cannot
write the files directly to `/etc`.
However this an also serve other use cases, e.g. when `/etc` is on a `tmpfs`.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !(config.systemd.sysusers.enable && cfg.enable);
message = "You cannot use systemd-sysusers and Userborn at the same time";
}
{
assertion = config.system.activationScripts.users == "";
message = "system.activationScripts.users has to be empty to use userborn";
}
{
assertion = immutableEtc -> (cfg.passwordFilesLocation != "/etc");
message = "When `system.etc.overlay.mutable = false`, `services.userborn.passwordFilesLocation` cannot be set to `/etc`";
}
];
system.activationScripts.users = lib.mkForce "";
system.activationScripts.hashes = lib.mkForce "";
systemd = {
# Create home directories, do not create /var/empty even if that's a user's
# home.
tmpfiles.settings.home-directories = lib.mapAttrs' (
username: opts:
lib.nameValuePair (toString opts.home) {
d = {
mode = opts.homeMode;
user = opts.name;
inherit (opts) group;
};
}
) (lib.filterAttrs (_username: opts: opts.createHome && opts.home != "/var/empty") userCfg.users);
services.userborn = {
wantedBy = [ "sysinit.target" ];
requiredBy = [ "sysinit-reactivation.target" ];
after = [
"systemd-remount-fs.service"
"systemd-tmpfiles-setup-dev-early.service"
];
before = [
"systemd-tmpfiles-setup-dev.service"
"sysinit.target"
"shutdown.target"
"sysinit-reactivation.target"
];
conflicts = [ "shutdown.target" ];
restartTriggers = [
userbornConfigJson
cfg.passwordFilesLocation
];
# This way we don't have to re-declare all the dependencies to other
# services again.
aliases = [ "systemd-sysusers.service" ];
unitConfig = {
Description = "Manage Users and Groups";
DefaultDependencies = false;
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
TimeoutSec = "90s";
ExecStart = "${lib.getExe cfg.package} ${userbornConfigJson} ${cfg.passwordFilesLocation}";
ExecStartPre = lib.mkMerge [
(lib.mkIf (!config.system.etc.overlay.mutable) [
"${pkgs.coreutils}/bin/mkdir -p ${cfg.passwordFilesLocation}"
])
# Make the source files writable before executing userborn.
(lib.mkIf (!userCfg.mutableUsers) (
lib.map (file: "-${pkgs.util-linux}/bin/umount ${cfg.passwordFilesLocation}/${file}") passwordFiles
))
];
# Make the source files read-only after userborn has finished.
ExecStartPost = lib.mkIf (!userCfg.mutableUsers) (
lib.map (
file:
"${pkgs.util-linux}/bin/mount --bind -o ro ${cfg.passwordFilesLocation}/${file} ${cfg.passwordFilesLocation}/${file}"
) passwordFiles
);
};
};
};
# Statically create the symlinks to passwordFilesLocation when they're not
# inside /etc because we will not be able to do it at runtime in case of an
# immutable /etc!
environment.etc = lib.mkIf (cfg.passwordFilesLocation != "/etc") (
lib.listToAttrs (
lib.map (
file:
lib.nameValuePair file {
source = "${cfg.passwordFilesLocation}/${file}";
mode = "direct-symlink";
}
) passwordFiles
)
);
};
meta.maintainers = with lib.maintainers; [ nikstur ];
}