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}