main
  1{
  2  lib,
  3  den,
  4  config,
  5  ...
  6}: let
  7  inherit (lib) mkOption types;
  8
  9  # Submodule for group-based access grants.
 10  # Per-host classes override the registry-level defaults.
 11  accessGrantType = types.submodule {
 12    options = {
 13      groups = mkOption {
 14        type = types.listOf types.str;
 15        default = [];
 16        description = "Groups granted access";
 17      };
 18      classes = mkOption {
 19        type = types.nullOr (types.listOf types.str);
 20        default = null;
 21        description = "Per-host nix classes override (null = use registry default)";
 22      };
 23    };
 24  };
 25
 26  # Registry entry type — mirrors the standard user entity shape so that
 27  # pipeline self-provide, define-user, and other batteries find the
 28  # expected attributes (userName, aspect, classes).
 29  registryUserType = types.submodule (
 30    {
 31      name,
 32      config,
 33      ...
 34    }: {
 35      freeformType = types.attrsOf types.anything;
 36      options = {
 37        name = mkOption {
 38          type = types.str;
 39          default = name;
 40          description = "User name (from attrset key)";
 41        };
 42        userName = mkOption {
 43          type = types.str;
 44          default = name;
 45          description = "User account name";
 46        };
 47        classes = mkOption {
 48          type = types.listOf types.str;
 49          default = ["user"];
 50          description = "Home management nix classes";
 51        };
 52        aspect = mkOption {
 53          type = types.raw;
 54          default = lib.mkDefault {};
 55          defaultText = "den.aspects.<name>";
 56          description = "Aspect that configures this user";
 57        };
 58        groups = mkOption {
 59          type = types.listOf types.str;
 60          default = [];
 61          description = "Group memberships for access policy selection";
 62        };
 63      };
 64    }
 65  );
 66in {
 67  # User registry option (under den.users.list for schema-compatibility).
 68  options.den.users = mkOption {
 69    type = types.submodule {
 70      options.registry = mkOption {
 71        type = types.attrsOf registryUserType;
 72        default = {};
 73        description = "User registry with extended schema for access policy resolution";
 74      };
 75      options.access = {
 76        by-host = mkOption {
 77          type = types.attrsOf accessGrantType;
 78          default = {};
 79          description = "Grant user groups access to a specific host";
 80        };
 81      };
 82    };
 83    default = {};
 84    description = "Den users registry and access control";
 85  };
 86
 87  config = let
 88    registry = config.den.users.registry;
 89
 90    matchRegistryUsers = grantedGroups:
 91      lib.filter (
 92        name: let
 93          userGroups = registry.${name}.groups or [];
 94        in
 95          builtins.any (g: lib.elem g grantedGroups) userGroups
 96      ) (builtins.attrNames registry);
 97
 98    resolveUser = hostClasses: name: let
 99      user = registry.${name};
100      userAspect = den.aspects.${name} or {};
101    in
102      user
103      // {aspect = userAspect;}
104      // lib.optionalAttrs (hostClasses != null) {classes = hostClasses;};
105  in {
106    den.policies.env-users = {host, ...}: let
107      hostName = host.name or host.hostName;
108      hostAccess = config.den.users.access.by-host.${hostName} or {};
109      accessGroups = hostAccess.groups or [];
110      hostClasses = hostAccess.classes or null;
111    in
112      map (name:
113        den.lib.policy.resolve.shared.to "user" {user = resolveUser hostClasses name;}) (
114        matchRegistryUsers accessGroups
115      );
116
117    den.hosts =
118      lib.mapAttrs (hostName: hostAccess: let
119        accessGroups = hostAccess.groups;
120        hostClasses = hostAccess.classes;
121      in {
122        users = lib.genAttrs (matchRegistryUsers accessGroups) (resolveUser hostClasses);
123      })
124      config.den.users.access.by-host;
125
126    den.schema.host = {
127      includes = [den.policies.env-users];
128      excludes = [den.policies.host-to-users];
129    };
130  };
131}