1{
2 utils,
3 config,
4 lib,
5 pkgs,
6 ...
7}:
8
9let
10
11 cfg = config.services.userborn;
12 userCfg = config.users;
13
14 userbornConfig = {
15 groups = lib.mapAttrsToList (username: opts: {
16 inherit (opts) name gid members;
17 }) config.users.groups;
18
19 users = lib.mapAttrsToList (username: opts: {
20 inherit (opts)
21 name
22 uid
23 group
24 description
25 home
26 password
27 hashedPassword
28 hashedPasswordFile
29 initialPassword
30 initialHashedPassword
31 ;
32 isNormal = opts.isNormalUser;
33 shell = utils.toShellPath opts.shell;
34 }) (lib.filterAttrs (_: u: u.enable) config.users.users);
35 };
36
37 userbornConfigJson = pkgs.writeText "userborn.json" (builtins.toJSON userbornConfig);
38
39 immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
40 # The filenames created by userborn.
41 passwordFiles = [
42 "group"
43 "passwd"
44 "shadow"
45 ];
46
47in
48{
49
50 options.services.userborn = {
51
52 enable = lib.mkEnableOption "userborn";
53
54 package = lib.mkPackageOption pkgs "userborn" { };
55
56 passwordFilesLocation = lib.mkOption {
57 type = lib.types.str;
58 default = if immutableEtc then "/var/lib/nixos" else "/etc";
59 defaultText = lib.literalExpression ''if immutableEtc then "/var/lib/nixos" else "/etc"'';
60 description = ''
61 The location of the original password files.
62
63 If this is not `/etc`, the files are symlinked from this location to `/etc`.
64
65 The primary motivation for this is an immutable `/etc`, where we cannot
66 write the files directly to `/etc`.
67
68 However this an also serve other use cases, e.g. when `/etc` is on a `tmpfs`.
69 '';
70 };
71
72 };
73
74 config = lib.mkIf cfg.enable {
75
76 assertions = [
77 {
78 assertion = !(config.systemd.sysusers.enable && cfg.enable);
79 message = "You cannot use systemd-sysusers and Userborn at the same time";
80 }
81 {
82 assertion = config.system.activationScripts.users == "";
83 message = "system.activationScripts.users has to be empty to use userborn";
84 }
85 {
86 assertion = immutableEtc -> (cfg.passwordFilesLocation != "/etc");
87 message = "When `system.etc.overlay.mutable = false`, `services.userborn.passwordFilesLocation` cannot be set to `/etc`";
88 }
89 ];
90
91 system.activationScripts.users = lib.mkForce "";
92 system.activationScripts.hashes = lib.mkForce "";
93
94 systemd = {
95
96 # Create home directories, do not create /var/empty even if that's a user's
97 # home.
98 tmpfiles.settings.home-directories =
99 lib.mapAttrs'
100 (
101 username: opts:
102 lib.nameValuePair (toString opts.home) {
103 d = {
104 mode = opts.homeMode;
105 user = opts.name;
106 inherit (opts) group;
107 };
108 }
109 )
110 (
111 lib.filterAttrs (
112 _username: opts: opts.enable && opts.createHome && opts.home != "/var/empty"
113 ) userCfg.users
114 );
115
116 services.userborn = {
117 wantedBy = [ "sysinit.target" ];
118 requiredBy = [ "sysinit-reactivation.target" ];
119 after = [
120 "systemd-remount-fs.service"
121 "systemd-tmpfiles-setup-dev-early.service"
122 ];
123 before = [
124 "systemd-tmpfiles-setup-dev.service"
125 "sysinit.target"
126 "shutdown.target"
127 "sysinit-reactivation.target"
128 ];
129 conflicts = [ "shutdown.target" ];
130 restartTriggers = [
131 userbornConfigJson
132 cfg.passwordFilesLocation
133 ];
134 # This way we don't have to re-declare all the dependencies to other
135 # services again.
136 aliases = [ "systemd-sysusers.service" ];
137
138 unitConfig = {
139 Description = "Manage Users and Groups";
140 DefaultDependencies = false;
141 };
142
143 serviceConfig = {
144 Type = "oneshot";
145 RemainAfterExit = true;
146 TimeoutSec = "90s";
147
148 ExecStart = "${lib.getExe cfg.package} ${userbornConfigJson} ${cfg.passwordFilesLocation}";
149
150 ExecStartPre = lib.mkMerge [
151 (lib.mkIf (cfg.passwordFilesLocation != "/etc") [
152 "${pkgs.coreutils}/bin/mkdir -p ${cfg.passwordFilesLocation}"
153 ])
154
155 # Make the source files writable before executing userborn.
156 (lib.mkIf (!userCfg.mutableUsers) (
157 lib.map (file: "-${pkgs.util-linux}/bin/umount ${cfg.passwordFilesLocation}/${file}") passwordFiles
158 ))
159 ];
160
161 # Make the source files read-only after userborn has finished.
162 ExecStartPost = lib.mkIf (!userCfg.mutableUsers) (
163 lib.map (
164 file:
165 "${pkgs.util-linux}/bin/mount --bind -o ro ${cfg.passwordFilesLocation}/${file} ${cfg.passwordFilesLocation}/${file}"
166 ) passwordFiles
167 );
168 };
169 };
170 };
171
172 # Statically create the symlinks to passwordFilesLocation when they're not
173 # inside /etc because we will not be able to do it at runtime in case of an
174 # immutable /etc!
175 environment.etc = lib.mkIf (cfg.passwordFilesLocation != "/etc") (
176 lib.listToAttrs (
177 lib.map (
178 file:
179 lib.nameValuePair file {
180 source = "${cfg.passwordFilesLocation}/${file}";
181 mode = "direct-symlink";
182 }
183 ) passwordFiles
184 )
185 );
186 };
187
188 meta.maintainers = with lib.maintainers; [ nikstur ];
189
190}