main
  1{
  2  lib,
  3  config,
  4  ...
  5}: let
  6  # Identity fields other than `displayName`, whose default differs between the
  7  # shared registry (the registry key) and the entity (the injected value).
  8  identityFields = {
  9    options = {
 10      email = lib.mkOption {
 11        type = lib.types.nullOr lib.types.str;
 12        default = null;
 13        description = "Email address for the user";
 14      };
 15      avatar = lib.mkOption {
 16        type = lib.types.nullOr lib.types.path;
 17        default = null;
 18        description = "Avatar for the user";
 19      };
 20      sshKeys = lib.mkOption {
 21        type = lib.types.listOf lib.types.str;
 22        default = [];
 23      };
 24      gpgKeys = lib.mkOption {
 25        type = lib.types.listOf (lib.types.either lib.types.path lib.types.str);
 26        default = [];
 27        description = "OpenPGP public keys (paths or armored key text) for this user";
 28      };
 29    };
 30  };
 31
 32  identityType = lib.types.submodule {
 33    imports = [identityFields];
 34    options.displayName = lib.mkOption {
 35      type = lib.types.str;
 36      description = "Display name for the user";
 37    };
 38  };
 39
 40  sharedIdentity = lib.mkOptionType {
 41    name = "den-shared-user-identity";
 42    check = v: builtins.isAttrs v;
 43    merge = loc: defs: let
 44      shared =
 45        if builtins.length loc < 2
 46        then {}
 47        else let
 48          username = lib.elemAt loc (builtins.length loc - 2);
 49        in
 50          (config.den.users.${username} or {}).identity or {displayName = username;};
 51    in
 52      identityType.merge loc ([
 53          {
 54            file = "<den.users>";
 55            value = lib.mkDefault shared;
 56          }
 57        ]
 58        ++ defs);
 59  };
 60in {
 61  # Shared user schema across every host
 62  options.den.users = lib.mkOption {
 63    type = lib.types.attrsOf (
 64      lib.types.submodule {
 65        freeformType = lib.types.attrsOf lib.types.anything;
 66        options = {
 67          admin = lib.mkEnableOption "Whether to add user to admin user";
 68          identity = lib.mkOption {
 69            type = lib.types.submodule (
 70              {name, ...}: let
 71                userName = name;
 72              in {
 73                imports = [identityFields];
 74                options.displayName = lib.mkOption {
 75                  type = lib.types.str;
 76                  default = userName;
 77                  description = "Display name for the user";
 78                };
 79              }
 80            );
 81            default = {};
 82          };
 83          system = lib.mkOption {
 84            type = lib.types.submodule {
 85              options = {
 86                hashedPasswordAged = lib.mkOption {
 87                  type = lib.types.path;
 88                  description = "Age encryped hashed password file for user";
 89                };
 90                settings = lib.mkOption {
 91                  type = lib.types.anything;
 92                };
 93              };
 94            };
 95            default = {};
 96          };
 97        };
 98      }
 99    );
100    default = {};
101    description = ''
102      Shared user definitions, keyed by user name.
103
104      Merged into the user schema of every host that declares `users.<name>`
105      in its `den.hosts` entry; host-specific values override shared ones.
106    '';
107  };
108
109  config.den.schema.user.options.identity = lib.mkOption {
110    type = sharedIdentity;
111    default = {};
112    description = ''
113      User identity fields, merged with the shared `den.users.<name>.identity`
114      registry entry as a lowest-priority definition.
115    '';
116  };
117}