userborn: init at 0.1.0 (#332719)
This commit is contained in:
commit
c169763c30
@ -100,6 +100,9 @@ modified using `usermod`. Unix groups can be managed using `groupadd`,
|
||||
|
||||
::: {.note}
|
||||
This is experimental.
|
||||
|
||||
Please consider using [Userborn](#sec-userborn) over systemd-sysusers as it's
|
||||
more feature complete.
|
||||
:::
|
||||
|
||||
Instead of using a custom perl script to create users and groups, you can use
|
||||
@ -112,3 +115,43 @@ systemd-sysusers:
|
||||
```
|
||||
|
||||
The primary benefit of this is to remove a dependency on perl.
|
||||
|
||||
## Manage users and groups with `userborn` {#sec-userborn}
|
||||
|
||||
::: {.note}
|
||||
This is experimental.
|
||||
:::
|
||||
|
||||
Like systemd-sysusers, Userborn adoesn't depend on Perl but offers some more
|
||||
advantages over systemd-sysusers:
|
||||
|
||||
1. It can create "normal" users (with a GID >= 1000).
|
||||
2. It can update some information about users. Most notably it can update their
|
||||
passwords.
|
||||
3. It will warn when users use an insecure or unsupported password hashing
|
||||
scheme.
|
||||
|
||||
Userborn is the recommended way to manage users if you don't want to rely on
|
||||
the Perl script. It aims to eventually replace the Perl script by default.
|
||||
|
||||
You can enable Userborn via:
|
||||
|
||||
```nix
|
||||
services.userborn.enable = true;
|
||||
```
|
||||
|
||||
You can configure Userborn to store the password files
|
||||
(`/etc/{group,passwd,shadow}`) outside of `/etc` and symlink them from this
|
||||
location to `/etc`:
|
||||
|
||||
```nix
|
||||
services.userborn.passwordFilesLocation = "/persistent/etc";
|
||||
```
|
||||
|
||||
This is useful when you store `/etc` on a `tmpfs` or if `/etc` is immutable
|
||||
(e.g. when using `system.etc.overlay.mutable = false;`). In the latter case the
|
||||
original files are by default stored in `/var/lib/nixos`.
|
||||
|
||||
Userborn implements immutable users by re-mounting the password files
|
||||
read-only. This means that unlike when using the Perl script, trying to add a
|
||||
new user (e.g. via `useradd`) will fail right away.
|
||||
|
@ -41,6 +41,13 @@
|
||||
|
||||
- [Quickwit](https://quickwit.io), sub-second search & analytics engine on cloud storage. Available as [services.quickwit](options.html#opt-services.quickwit).
|
||||
|
||||
- [Userborn](https://github.com/nikstur/userborn), a service for declarative
|
||||
user management. This can be used instead of the `update-users-groups.pl`
|
||||
Perl script and instead of systemd-sysusers. To achieve a system without
|
||||
Perl, this is the now recommended tool over systemd-sysusers because it can
|
||||
alos create normal users and change passwords. Available as
|
||||
[services.userborn](#opt-services.userborn.enable)
|
||||
|
||||
- [Flood](https://flood.js.org/), a beautiful WebUI for various torrent clients. Available as [services.flood](options.html#opt-services.flood).
|
||||
|
||||
- [Firefly-iii Data Importer](https://github.com/firefly-iii/data-importer), a data importer for Firefly-III. Available as [services.firefly-iii-data-importer](options.html#opt-services.firefly-iii-data-importer)
|
||||
|
@ -1348,6 +1348,7 @@
|
||||
./services/system/systembus-notify.nix
|
||||
./services/system/systemd-lock-handler.nix
|
||||
./services/system/uptimed.nix
|
||||
./services/system/userborn.nix
|
||||
./services/system/zram-generator.nix
|
||||
./services/torrent/deluge.nix
|
||||
./services/torrent/flexget.nix
|
||||
|
@ -12,7 +12,7 @@
|
||||
# Remove perl from activation
|
||||
boot.initrd.systemd.enable = lib.mkDefault true;
|
||||
system.etc.overlay.enable = lib.mkDefault true;
|
||||
systemd.sysusers.enable = lib.mkDefault true;
|
||||
services.userborn.enable = lib.mkDefault true;
|
||||
|
||||
# Random perl remnants
|
||||
system.disableInstallerTools = lib.mkDefault true;
|
||||
|
183
nixos/modules/services/system/userborn.nix
Normal file
183
nixos/modules/services/system/userborn.nix
Normal file
@ -0,0 +1,183 @@
|
||||
{
|
||||
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 opts.home {
|
||||
d = {
|
||||
mode = opts.homeMode;
|
||||
user = username;
|
||||
inherit (opts) group;
|
||||
};
|
||||
}
|
||||
) (lib.filterAttrs (_username: opts: 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 ];
|
||||
|
||||
}
|
@ -19,8 +19,8 @@
|
||||
message = "`system.etc.overlay.enable` requires `boot.initrd.systemd.enable`";
|
||||
}
|
||||
{
|
||||
assertion = (!config.system.etc.overlay.mutable) -> config.systemd.sysusers.enable;
|
||||
message = "`system.etc.overlay.mutable = false` requires `systemd.sysusers.enable`";
|
||||
assertion = (!config.system.etc.overlay.mutable) -> (config.systemd.sysusers.enable || config.services.userborn.enable);
|
||||
message = "`!system.etc.overlay.mutable` requires `systemd.sysusers.enable` or `services.userborn.enable`";
|
||||
}
|
||||
{
|
||||
assertion = lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.6";
|
||||
|
@ -1060,6 +1060,11 @@ in {
|
||||
uptime-kuma = handleTest ./uptime-kuma.nix {};
|
||||
urn-timer = handleTest ./urn-timer.nix {};
|
||||
usbguard = handleTest ./usbguard.nix {};
|
||||
userborn = runTest ./userborn.nix;
|
||||
userborn-mutable-users = runTest ./userborn-mutable-users.nix;
|
||||
userborn-immutable-users = runTest ./userborn-immutable-users.nix;
|
||||
userborn-mutable-etc = runTest ./userborn-mutable-etc.nix;
|
||||
userborn-immutable-etc = runTest ./userborn-immutable-etc.nix;
|
||||
user-activation-scripts = handleTest ./user-activation-scripts.nix {};
|
||||
user-expiry = runTest ./user-expiry.nix;
|
||||
user-home-mode = handleTest ./user-home-mode.nix {};
|
||||
|
70
nixos/tests/userborn-immutable-etc.nix
Normal file
70
nixos/tests/userborn-immutable-etc.nix
Normal file
@ -0,0 +1,70 @@
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
normaloHashedPassword = "$y$j9T$IEWqhKtWg.r.8fVkSEF56.$iKNxdMC6hOAQRp6eBtYvBk4c7BGpONXeZMqc8I/LM46";
|
||||
|
||||
common = {
|
||||
services.userborn.enable = true;
|
||||
boot.initrd.systemd.enable = true;
|
||||
system.etc.overlay = {
|
||||
enable = true;
|
||||
mutable = false;
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
name = "userborn-immutable-etc";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
|
||||
users = {
|
||||
users = {
|
||||
normalo = {
|
||||
isNormalUser = true;
|
||||
hashedPassword = normaloHashedPassword;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.new-generation = {
|
||||
inheritParentConfig = false;
|
||||
configuration = {
|
||||
nixpkgs = {
|
||||
inherit (config.nixpkgs) hostPlatform;
|
||||
};
|
||||
imports = [ common ];
|
||||
|
||||
users.users = {
|
||||
new-normalo = {
|
||||
isNormalUser = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("userborn.service")
|
||||
|
||||
with subtest("normalo user is created"):
|
||||
assert "${normaloHashedPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct"
|
||||
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||
|
||||
|
||||
with subtest("normalo user is disabled"):
|
||||
print(machine.succeed("getent shadow normalo"))
|
||||
assert "!*" in machine.succeed("getent shadow normalo"), "normalo user is not disabled"
|
||||
|
||||
with subtest("new-normalo user is created after switching to new generation"):
|
||||
print(machine.succeed("getent passwd new-normalo"))
|
||||
'';
|
||||
}
|
75
nixos/tests/userborn-immutable-users.nix
Normal file
75
nixos/tests/userborn-immutable-users.nix
Normal file
@ -0,0 +1,75 @@
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
normaloHashedPassword = "$y$j9T$IEWqhKtWg.r.8fVkSEF56.$iKNxdMC6hOAQRp6eBtYvBk4c7BGpONXeZMqc8I/LM46";
|
||||
|
||||
common = {
|
||||
services.userborn.enable = true;
|
||||
users.mutableUsers = false;
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
name = "userborn-immutable-users";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
|
||||
users = {
|
||||
users = {
|
||||
normalo = {
|
||||
isNormalUser = true;
|
||||
hashedPassword = normaloHashedPassword;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.new-generation = {
|
||||
inheritParentConfig = false;
|
||||
configuration = {
|
||||
nixpkgs = {
|
||||
inherit (config.nixpkgs) hostPlatform;
|
||||
};
|
||||
imports = [ common ];
|
||||
|
||||
users.users = {
|
||||
new-normalo = {
|
||||
isNormalUser = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("userborn.service")
|
||||
|
||||
with subtest("normalo user is created"):
|
||||
assert "${normaloHashedPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct"
|
||||
|
||||
with subtest("Fail to add new user manually"):
|
||||
machine.fail("useradd manual-normalo")
|
||||
|
||||
with subtest("Fail to add delete user manually"):
|
||||
machine.fail("userdel normalo")
|
||||
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||
|
||||
|
||||
with subtest("normalo user is disabled"):
|
||||
print(machine.succeed("getent shadow normalo"))
|
||||
assert "!*" in machine.succeed("getent shadow normalo"), "normalo user is not disabled"
|
||||
|
||||
with subtest("new-normalo user is created after switching to new generation"):
|
||||
print(machine.succeed("getent passwd new-normalo"))
|
||||
|
||||
with subtest("Still fail to add new user manually"):
|
||||
machine.fail("useradd again-normalo")
|
||||
'';
|
||||
}
|
70
nixos/tests/userborn-mutable-etc.nix
Normal file
70
nixos/tests/userborn-mutable-etc.nix
Normal file
@ -0,0 +1,70 @@
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
normaloHashedPassword = "$y$j9T$IEWqhKtWg.r.8fVkSEF56.$iKNxdMC6hOAQRp6eBtYvBk4c7BGpONXeZMqc8I/LM46";
|
||||
|
||||
common = {
|
||||
services.userborn.enable = true;
|
||||
boot.initrd.systemd.enable = true;
|
||||
system.etc.overlay = {
|
||||
enable = true;
|
||||
mutable = true;
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
name = "userborn-mutable-etc";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
|
||||
users = {
|
||||
users = {
|
||||
normalo = {
|
||||
isNormalUser = true;
|
||||
hashedPassword = normaloHashedPassword;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.new-generation = {
|
||||
inheritParentConfig = false;
|
||||
configuration = {
|
||||
nixpkgs = {
|
||||
inherit (config.nixpkgs) hostPlatform;
|
||||
};
|
||||
imports = [ common ];
|
||||
|
||||
users.users = {
|
||||
new-normalo = {
|
||||
isNormalUser = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("userborn.service")
|
||||
|
||||
with subtest("normalo user is created"):
|
||||
assert "${normaloHashedPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct"
|
||||
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||
|
||||
|
||||
with subtest("normalo user is disabled"):
|
||||
print(machine.succeed("getent shadow normalo"))
|
||||
assert "!*" in machine.succeed("getent shadow normalo"), "normalo user is not disabled"
|
||||
|
||||
with subtest("new-normalo user is created after switching to new generation"):
|
||||
print(machine.succeed("getent passwd new-normalo"))
|
||||
'';
|
||||
}
|
76
nixos/tests/userborn-mutable-users.nix
Normal file
76
nixos/tests/userborn-mutable-users.nix
Normal file
@ -0,0 +1,76 @@
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
normaloHashedPassword = "$y$j9T$IEWqhKtWg.r.8fVkSEF56.$iKNxdMC6hOAQRp6eBtYvBk4c7BGpONXeZMqc8I/LM46";
|
||||
|
||||
common = {
|
||||
services.userborn.enable = true;
|
||||
users.mutableUsers = true;
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
name = "userborn-mutable-users";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine =
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
|
||||
users = {
|
||||
mutableUsers = true;
|
||||
users = {
|
||||
normalo = {
|
||||
isNormalUser = true;
|
||||
hashedPassword = normaloHashedPassword;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.new-generation = {
|
||||
inheritParentConfig = false;
|
||||
configuration = {
|
||||
nixpkgs = {
|
||||
inherit (config.nixpkgs) hostPlatform;
|
||||
};
|
||||
imports = [ common ];
|
||||
|
||||
users.users = {
|
||||
new-normalo = {
|
||||
isNormalUser = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("userborn.service")
|
||||
|
||||
with subtest("normalo user is created"):
|
||||
assert 1000 == int(machine.succeed("id --user normalo")), "normalo user doesn't have UID 1000"
|
||||
assert "${normaloHashedPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct"
|
||||
|
||||
with subtest("Add new user manually"):
|
||||
machine.succeed("useradd manual-normalo")
|
||||
assert 1001 == int(machine.succeed("id --user manual-normalo")), "manual-normalo user doesn't have UID 1001"
|
||||
|
||||
with subtest("Delete manual--normalo user manually"):
|
||||
machine.succeed("userdel manual-normalo")
|
||||
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||
|
||||
|
||||
with subtest("normalo user is disabled"):
|
||||
print(machine.succeed("getent shadow normalo"))
|
||||
assert "!*" in machine.succeed("getent shadow normalo"), "normalo user is not disabled"
|
||||
|
||||
with subtest("new-normalo user is created after switching to new generation"):
|
||||
print(machine.succeed("getent passwd new-normalo"))
|
||||
assert 1001 == int(machine.succeed("id --user new-normalo")), "new-normalo user doesn't have UID 1001"
|
||||
'';
|
||||
}
|
127
nixos/tests/userborn.nix
Normal file
127
nixos/tests/userborn.nix
Normal file
@ -0,0 +1,127 @@
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
# All passwords are "test"
|
||||
rootHashedPasswordFile = "$y$j9T$6ueoTO5y7vvFsGvpQJEEa.$vubxgBiMnkTCtRtPD3hNiZHa7Nm1WsJeE9QomYqSRXB";
|
||||
updatedRootHashedPassword = "$y$j9T$pBCO9N1FRF1rSl6V15n9n/$1JmRLEYPO7TRCx43cvLO19u59WA/oqTEhmSR4wrhzr.";
|
||||
|
||||
normaloPassword = "test";
|
||||
updatedNormaloHashedPassword = "$y$j9T$IEWqhKtWg.r.8fVkSEF56.$iKNxdMC6hOAQRp6eBtYvBk4c7BGpONXeZMqc8I/LM46";
|
||||
|
||||
sysuserInitialHashedPassword = "$y$j9T$Kb6jGrk41hudTZpNjazf11$iw7fZXrewC6JxRaGPz7/gPXDZ.Z1VWsupvy81Hi1XiD";
|
||||
updatedSysuserInitialHashedPassword = "$y$j9T$kUBVhgOdSjymSfwfRVja70$eqCwWzVsz0fI0Uc6JsdD2CYMCpfJcErqnIqva2JCi1D";
|
||||
|
||||
newNormaloHashedPassword = "$y$j9T$UFBMWbGjjVola0YE9YCcV/$jRSi5S6lzkcifbuqjMcyXLTwgOGm9BTQk/G/jYaxroC";
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
name = "userborn";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine = {
|
||||
services.userborn.enable = true;
|
||||
|
||||
# Read this password file at runtime from outside the Nix store.
|
||||
environment.etc."rootpw.secret".text = rootHashedPasswordFile;
|
||||
|
||||
users = {
|
||||
users = {
|
||||
root = {
|
||||
# Override the empty root password set by the test instrumentation.
|
||||
hashedPasswordFile = lib.mkForce "/etc/rootpw.secret";
|
||||
};
|
||||
normalo = {
|
||||
isNormalUser = true;
|
||||
password = normaloPassword;
|
||||
};
|
||||
sysuser = {
|
||||
isSystemUser = true;
|
||||
group = "sysusers";
|
||||
initialHashedPassword = sysuserInitialHashedPassword;
|
||||
};
|
||||
};
|
||||
groups = {
|
||||
sysusers = { };
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.new-generation.configuration = {
|
||||
users = {
|
||||
users = {
|
||||
root = {
|
||||
# Forcing this to null simulates removing the config value in a new
|
||||
# generation.
|
||||
hashedPasswordFile = lib.mkOverride 9 null;
|
||||
hashedPassword = updatedRootHashedPassword;
|
||||
};
|
||||
normalo = {
|
||||
hashedPassword = updatedNormaloHashedPassword;
|
||||
};
|
||||
sysuser = {
|
||||
initialHashedPassword = lib.mkForce updatedSysuserInitialHashedPassword;
|
||||
};
|
||||
new-normalo = {
|
||||
isNormalUser = true;
|
||||
hashedPassword = newNormaloHashedPassword;
|
||||
};
|
||||
};
|
||||
groups = {
|
||||
new-group = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("userborn.service")
|
||||
|
||||
with subtest("Correct mode on the password files"):
|
||||
assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n"
|
||||
assert machine.succeed("stat -c '%a' /etc/group") == "644\n"
|
||||
assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n"
|
||||
|
||||
with subtest("root user has correct password"):
|
||||
print(machine.succeed("getent passwd root"))
|
||||
assert "${rootHashedPasswordFile}" in machine.succeed("getent shadow root"), "root user password is not correct"
|
||||
|
||||
with subtest("normalo user is created"):
|
||||
print(machine.succeed("getent passwd normalo"))
|
||||
assert 1000 <= int(machine.succeed("id --user normalo")), "normalo user doesn't have a normal UID"
|
||||
assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n"
|
||||
|
||||
with subtest("system user is created with correct password"):
|
||||
print(machine.succeed("getent passwd sysuser"))
|
||||
assert 1000 > int(machine.succeed("id --user sysuser")), "sysuser user doesn't have a system UID"
|
||||
assert "${sysuserInitialHashedPassword}" in machine.succeed("getent shadow sysuser"), "system user password is not correct"
|
||||
|
||||
with subtest("sysusers group is created"):
|
||||
print(machine.succeed("getent group sysusers"))
|
||||
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||
|
||||
|
||||
with subtest("root user password is updated"):
|
||||
print(machine.succeed("getent passwd root"))
|
||||
assert "${updatedRootHashedPassword}" in machine.succeed("getent shadow root"), "root user password is not updated"
|
||||
|
||||
with subtest("normalo user password is updated"):
|
||||
print(machine.succeed("getent passwd normalo"))
|
||||
assert "${updatedNormaloHashedPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not updated"
|
||||
|
||||
with subtest("system user password is NOT updated"):
|
||||
print(machine.succeed("getent passwd sysuser"))
|
||||
assert "${sysuserInitialHashedPassword}" in machine.succeed("getent shadow sysuser"), "sysuser user password is not updated"
|
||||
|
||||
with subtest("new-normalo user is created after switching to new generation"):
|
||||
print(machine.succeed("getent passwd new-normalo"))
|
||||
assert 1000 <= int(machine.succeed("id --user new-normalo")), "new-normalo user doesn't have a normal UID"
|
||||
assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n"
|
||||
assert "${newNormaloHashedPassword}" in machine.succeed("getent shadow new-normalo"), "new-normalo user password is not correct"
|
||||
|
||||
with subtest("new-group group is created after switching to new generation"):
|
||||
print(machine.succeed("getent group new-group"))
|
||||
'';
|
||||
}
|
43
pkgs/by-name/us/userborn/package.nix
Normal file
43
pkgs/by-name/us/userborn/package.nix
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
lib,
|
||||
rustPlatform,
|
||||
fetchFromGitHub,
|
||||
makeBinaryWrapper,
|
||||
mkpasswd,
|
||||
}:
|
||||
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "userborn";
|
||||
version = "0.1.0";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "nikstur";
|
||||
repo = "userborn";
|
||||
rev = version;
|
||||
hash = "sha256-aptFDrL9RPPTu4wp2ee3LVaEruRdCWtLGIKdOgsR+/s=";
|
||||
};
|
||||
|
||||
sourceRoot = "${src.name}/rust/userborn";
|
||||
|
||||
cargoHash = "sha256-m39AC26E0Pxu1E/ap2kSwr5uznJNgExf5QUrZ+zTNX0=";
|
||||
|
||||
nativeBuildInputs = [ makeBinaryWrapper ];
|
||||
|
||||
buildInputs = [ mkpasswd ];
|
||||
|
||||
nativeCheckInputs = [ mkpasswd ];
|
||||
|
||||
postInstall = ''
|
||||
wrapProgram $out/bin/userborn --prefix PATH : ${lib.makeBinPath [ mkpasswd ]}
|
||||
'';
|
||||
|
||||
stripAllList = [ "bin" ];
|
||||
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/nikstur/userborn";
|
||||
description = "Declaratively bear (manage) Linux users and groups";
|
||||
license = licenses.mit;
|
||||
maintainers = with lib.maintainers; [ nikstur ];
|
||||
mainProgram = "userborn";
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user