main
 1{
 2  lib,
 3  config,
 4  ...
 5}: {
 6  # Share a portion of the user schema across every host that declares
 7  # the same user. Hosts without the user stay unaffected.
 8  options.den.users = lib.mkOption {
 9    type = lib.types.attrsOf (
10      lib.types.submodule ({name, ...}: {
11        freeformType = lib.types.attrsOf lib.types.anything;
12        options = {
13          admin = lib.mkEnableOption "Whether to add user to admin user";
14          identity = lib.mkOption {
15            type = lib.types.submodule {
16              options = {
17                displayName = lib.mkOption {
18                  type = lib.types.str;
19                  default = name;
20                  description = "Display name for the user";
21                };
22                email = lib.mkOption {
23                  type = lib.types.nullOr lib.types.str;
24                  default = null;
25                  description = "Email address for the user";
26                };
27                avatar = lib.mkOption {
28                  type = lib.types.nullOr lib.types.path;
29                  default = null;
30                  description = "Avatar for the user";
31                };
32                sshKeys = lib.mkOption {
33                  type = lib.types.listOf lib.types.str;
34                  default = [];
35                };
36                gpgKeys = lib.mkOption {
37                  type = lib.types.listOf (lib.types.either lib.types.path lib.types.str);
38                  default = [];
39                  description = "OpenPGP public keys (paths or armored key text) for this user";
40                };
41              };
42            };
43            default = {};
44          };
45          system = lib.mkOption {
46            type = lib.types.submodule {
47              options = {
48                hashedPasswordAged = lib.mkOption {
49                  type = lib.types.path;
50                  description = "Age encryped hashed password file for user";
51                };
52                settings = lib.mkOption {
53                  type = lib.types.anything;
54                };
55              };
56            };
57            default = {};
58          };
59        };
60      })
61    );
62    default = {};
63    description = ''
64      Shared user definitions, keyed by user name.
65
66      Merged into the user schema of every host that declares `users.<name>`
67      in its `den.hosts` entry; host-specific values override shared ones.
68    '';
69  };
70
71  config.den.schema.user = {
72    # `config` referencing the instance's own args are forced while the
73    # fixpoint is still being built and recurse, so the per-user name can only
74    # be recovered from the freeform merge's option path.
75    options._module.freeformType = lib.mkOption {
76      apply = t:
77        if t == null
78        then null
79        else
80          lib.mkOptionType {
81            name = "den-shared-user-freeform";
82            check = builtins.isAttrs;
83            # If without any freeform definitions, the module system will skips
84            # the freeform merge entirely, so the shared registry would never
85            # reach hosts that declare `users.<name> = {}`.
86            merge = loc: defs:
87              lib.removeAttrs (
88                lib.recursiveUpdate (config.den.users.${lib.last loc} or {}) (t.merge loc defs)
89              ) ["ixSharedSentinel"];
90          };
91    };
92    config.ixSharedSentinel = true;
93  };
94}