Commit 76e2e7f

HPCesia <me@hpcesia.com>
2026-06-16 15:28:35
Add user registry
1 parent f6cefc0
Changed files (10)
modules/core/openssh.nix
@@ -22,7 +22,7 @@
     includes = [
       ({user, ...}: {
         name = "core.openssh.access.user";
-        user.openssh.authorizedKeys.keys = user.authorizedKeys;
+        user.openssh.authorizedKeys.keys = map (k: k.key) user.identity.sshKeys;
       })
     ];
 
modules/hosts/hyacine/default.nix
@@ -6,15 +6,14 @@
       Based on a KVM virtual host,
       with AMD EPYC 7K62 2 core @ 2.6GHz, 2GB RAM and 30GB SSD storage.
     '';
-    users.hpcesia = {
-      authorizedKeys = [
-        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFMKaGC2I3an4AJDeWzVx5vhm63+kxi6zJNdh7yEp6CK hpcesia@kevin"
-      ];
-    };
 
     pubKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINy57YGTUNEbBVs1YHD6m/zhJnJKnicNEaLB5fEcJln6 root@hyacine";
   };
 
+  den.users.access.by-host.hyacine.groups = [
+    "admins"
+  ];
+
   den.aspects.hyacine = {
     includes = [
       den.aspects.roles.default
modules/hosts/kevin/default.nix
@@ -6,16 +6,19 @@
       Based on a Lenovo ThinkBook 16P G5 IRX,
       with Intel Core i7-14650HX, 32GB RAM and 2TB NVMe storage.
     '';
-    users.hpcesia = {
-      classes = [
-        "user"
-        "homeManager"
-      ];
-    };
-
     pubKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOO9CyAqGo/WbJkncrt1a9jxS1E+hd550SC4A08I/l0/ root@kevin";
   };
 
+  den.users.access.by-host.kevin = {
+    groups = [
+      "admins"
+    ];
+    classes = [
+      "user"
+      "homeManager"
+    ];
+  };
+
   den.aspects.kevin = {
     includes = [
       den.aspects.roles.default
modules/hosts/mobius/default.nix
@@ -6,18 +6,14 @@
       Based on a Topfell DeskOne T2 mini PC,
       with AMD Ryzen 7 H255, 16GB RAM and 1TB NVMe storage.
     '';
-    users.hpcesia = {
-      classes = [
-        "user"
-      ];
-      authorizedKeys = [
-        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFMKaGC2I3an4AJDeWzVx5vhm63+kxi6zJNdh7yEp6CK hpcesia@kevin"
-      ];
-    };
 
     pubKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKSHlIARpQYDi744NML9QmFMxOEZwShfyj/ovQh6lCt7 root@mobius";
   };
 
+  den.users.access.by-host.mobius.groups = [
+    "admins"
+  ];
+
   den.aspects.mobius = {
     includes = [
       den.aspects.roles.default
modules/hosts/tribios/default.nix
@@ -6,15 +6,13 @@
       Based on a Friendly Nano Pi R6S board,
       with Rockchip RK3588S, 4GB RAM and 64GB eMMC storage.
     '';
-    users.hpcesia = {
-      authorizedKeys = [
-        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFMKaGC2I3an4AJDeWzVx5vhm63+kxi6zJNdh7yEp6CK hpcesia@kevin"
-      ];
-    };
-
     pubKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMQTx9RhZJa4WOAJyAUzlQLaFExcDv921sVsfM02vB9h root@tribios";
   };
 
+  den.users.access.by-host.tribios.groups = [
+    "admins"
+  ];
+
   den.aspects.tribios = {
     includes = [
       den.aspects.roles.default
modules/hosts/schema.nix
@@ -85,12 +85,6 @@
       in
         lib.types.submodule (nodeModule (den.aspects or {}));
     in {
-      description = lib.mkOption {
-        description = "Description of the host.";
-        type = lib.types.str;
-        default = "";
-      };
-
       settings = lib.mkOption {
         description = "Per-aspect typed settings";
         type = settingsType;
modules/users/hpcesia/default.nix
@@ -5,4 +5,20 @@
       den.batteries.primary-user
     ];
   };
+
+  den.users.registry.hpcesia = {
+    groups = [
+      "admins"
+    ];
+    identity = {
+      displayName = "HPCesia";
+      email = "me@hpcesia.com";
+      sshKeys = [
+        {
+          tag = "hpcesia@kevin";
+          key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFMKaGC2I3an4AJDeWzVx5vhm63+kxi6zJNdh7yEp6CK";
+        }
+      ];
+    };
+  };
 }
modules/users/default.nix
@@ -3,7 +3,7 @@
     den.batteries.define-user
     ({user, ...}: {
       name = "users/description";
-      user.description = user.description;
+      user.description = user.identity.displayName;
     })
   ];
 }
modules/users/registry.nix
@@ -0,0 +1,131 @@
+{
+  lib,
+  den,
+  config,
+  ...
+}: let
+  inherit (lib) mkOption types;
+
+  # Submodule for group-based access grants.
+  # Per-host classes override the registry-level defaults.
+  accessGrantType = types.submodule {
+    options = {
+      groups = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Groups granted access";
+      };
+      classes = mkOption {
+        type = types.nullOr (types.listOf types.str);
+        default = null;
+        description = "Per-host nix classes override (null = use registry default)";
+      };
+    };
+  };
+
+  # Registry entry type — mirrors the standard user entity shape so that
+  # pipeline self-provide, define-user, and other batteries find the
+  # expected attributes (userName, aspect, classes).
+  registryUserType = types.submodule (
+    {
+      name,
+      config,
+      ...
+    }: {
+      freeformType = types.attrsOf types.anything;
+      options = {
+        name = mkOption {
+          type = types.str;
+          default = name;
+          description = "User name (from attrset key)";
+        };
+        userName = mkOption {
+          type = types.str;
+          default = name;
+          description = "User account name";
+        };
+        classes = mkOption {
+          type = types.listOf types.str;
+          default = ["user"];
+          description = "Home management nix classes";
+        };
+        aspect = mkOption {
+          type = types.raw;
+          default = lib.mkDefault {};
+          defaultText = "den.aspects.<name>";
+          description = "Aspect that configures this user";
+        };
+        groups = mkOption {
+          type = types.listOf types.str;
+          default = [];
+          description = "Group memberships for access policy selection";
+        };
+      };
+    }
+  );
+in {
+  # User registry option (under den.users.list for schema-compatibility).
+  options.den.users = mkOption {
+    type = types.submodule {
+      options.registry = mkOption {
+        type = types.attrsOf registryUserType;
+        default = {};
+        description = "User registry with extended schema for access policy resolution";
+      };
+      options.access = {
+        by-host = mkOption {
+          type = types.attrsOf accessGrantType;
+          default = {};
+          description = "Grant user groups access to a specific host";
+        };
+      };
+    };
+    default = {};
+    description = "Den users registry and access control";
+  };
+
+  config = let
+    registry = config.den.users.registry;
+
+    matchRegistryUsers = grantedGroups:
+      lib.filter (
+        name: let
+          userGroups = registry.${name}.groups or [];
+        in
+          builtins.any (g: lib.elem g grantedGroups) userGroups
+      ) (builtins.attrNames registry);
+
+    resolveUser = hostClasses: name: let
+      user = registry.${name};
+      userAspect = den.aspects.${name} or {};
+    in
+      user
+      // {aspect = userAspect;}
+      // lib.optionalAttrs (hostClasses != null) {classes = hostClasses;};
+  in {
+    den.policies.env-users = {host, ...}: let
+      hostName = host.name or host.hostName;
+      hostAccess = config.den.users.access.by-host.${hostName} or {};
+      accessGroups = hostAccess.groups or [];
+      hostClasses = hostAccess.classes or null;
+    in
+      map (name:
+        den.lib.policy.resolve.shared.to "user" {user = resolveUser hostClasses name;}) (
+        matchRegistryUsers accessGroups
+      );
+
+    den.hosts =
+      lib.mapAttrs (hostName: hostAccess: let
+        accessGroups = hostAccess.groups;
+        hostClasses = hostAccess.classes;
+      in {
+        users = lib.genAttrs (matchRegistryUsers accessGroups) (resolveUser hostClasses);
+      })
+      config.den.users.access.by-host;
+
+    den.schema.host = {
+      includes = [den.policies.env-users];
+      excludes = [den.policies.host-to-users];
+    };
+  };
+}
modules/users/schema.nix
@@ -1,16 +1,66 @@
-{lib, ...}: {
-  den.schema.user = {config, ...}: {
+{lib, ...}: let
+  inherit (lib) mkOption types;
+
+  sshKeyType = types.submodule {
     options = {
-      description = lib.mkOption {
-        description = "Description of the user.";
-        type = lib.types.str;
-        default = config.userName;
+      tag = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Tag to categorize the SSH key (e.g., 'laptop', 'workstation', 'yubikey')";
       };
-      authorizedKeys = lib.mkOption {
-        type = lib.types.listOf lib.types.str;
-        description = "List of authorized keys for the user.";
-        default = [];
+      key = mkOption {
+        type = types.str;
+        description = "SSH public key string";
       };
     };
   };
+in {
+  den.schema.user.imports = [
+    (_: {
+      options = {
+        identity = mkOption {
+          type = types.submodule {
+            options = {
+              displayName = mkOption {
+                type = types.str;
+                default = "";
+                description = "Display name for the user";
+              };
+              email = mkOption {
+                type = types.nullOr types.str;
+                default = null;
+                description = "Email address for the user";
+              };
+              gpgKey = mkOption {
+                type = types.nullOr types.str;
+                default = null;
+                description = "GPG key ID for the user";
+              };
+              sshKeys = mkOption {
+                type = types.listOf sshKeyType;
+                default = [];
+                description = "SSH public keys for the user, each with an optional tag";
+              };
+            };
+          };
+          default = {};
+          description = "User identity information";
+        };
+
+        system = mkOption {
+          type = types.submodule {
+            options = {
+              settings = mkOption {
+                type = types.attrsOf (types.attrsOf types.anything);
+                default = {};
+                description = "Per-user feature settings (freeform nested namespace)";
+              };
+            };
+          };
+          default = {};
+          description = "Unix account defaults and system configuration";
+        };
+      };
+    })
+  ];
 }