refactor(ssh): restructure inventory and modularize helper logic
Relocate SSH inventory and rbw socket wiring from `modules/lib/` to dedicated `modules/ssh/` modules to improve logical grouping. - Move `ssh-inventory.nix` to `modules/ssh/inventory.nix`. - Move `rbw-ssh-socket.nix` to `modules/ssh/rbw.nix`. - Relocate `users-merge.nix` logic into `modules/system/users.nix` to reduce dependency on generic library modules. - Update `docs/conventions.md` to reflect new module paths. - Refactor `modules/deploy/navi.nix` to use a local helper for generating Navi hive configurations. - Add Pi5 specific Niri KDL configuration snippets.
This commit is contained in:
+32
-1
@@ -98,9 +98,40 @@ let
|
||||
metaNixpkgs = import inputs.nixpkgs {
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
# Build a raw Navi hive attrset from host specs + deployment targets.
|
||||
# `inputs.navi.lib.makeHive` turns the result into `flake.naviHive`.
|
||||
mkNaviHiveConfig =
|
||||
{
|
||||
metaNixpkgs,
|
||||
hostSpecs,
|
||||
deployments,
|
||||
}:
|
||||
let
|
||||
deployNodes = lib.filterAttrs (name: _: deployments ? ${name}) hostSpecs;
|
||||
in
|
||||
{
|
||||
meta = {
|
||||
nixpkgs = metaNixpkgs;
|
||||
nodeNixpkgs = lib.mapAttrs (
|
||||
name: spec:
|
||||
import inputs.nixpkgs {
|
||||
system = spec.system;
|
||||
}
|
||||
) deployNodes;
|
||||
nodeSpecialArgs = lib.mapAttrs (_: spec: spec.specialArgs) deployNodes;
|
||||
allowApplyAll = false;
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs (
|
||||
name: spec:
|
||||
{
|
||||
imports = spec.modules or [ spec.configuration ];
|
||||
deployment = deployments.${name};
|
||||
}
|
||||
) deployNodes;
|
||||
in
|
||||
{
|
||||
flake.navi = self.lib.mkNaviHiveConfig {
|
||||
flake.navi = mkNaviHiveConfig {
|
||||
inherit metaNixpkgs hostSpecs deployments;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
{ ... }: {
|
||||
{ lib, ... }: {
|
||||
# Resolve `wisdom*` HM slices for NixOS `extraModules` (`self` is not an HM specialArg).
|
||||
# Slices are gated by `chiasson.home.*.enable`; hosts only flip toggles in `home.nix`.
|
||||
# Lives here (not `modules/lib/`) because `hosts/desktop-home-base.nix` consumes it and
|
||||
# fragment eval order requires the definition to sort before `hosts/`.
|
||||
flake.lib.wisdomCatalogExtraModules =
|
||||
self:
|
||||
let
|
||||
names = lib.sort builtins.lessThan (
|
||||
lib.filter (n: lib.hasPrefix "wisdom" n && n != "wisdom" && n != "wisdomShellBash") (
|
||||
builtins.attrNames self.homeManagerModules
|
||||
)
|
||||
);
|
||||
in
|
||||
map (name: self.homeManagerModules.${name}) names;
|
||||
|
||||
flake.nixosModules.desktopOptions =
|
||||
{ config, options, lib, pkgs, self, inputs, ... }:
|
||||
let
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Build a raw Navi hive attrset from host specs + deployment targets.
|
||||
# Call `inputs.navi.lib.makeHive` on the result to produce `flake.naviHive`.
|
||||
{ lib, inputs, ... }: {
|
||||
flake.lib.mkNaviHiveConfig =
|
||||
{
|
||||
metaNixpkgs,
|
||||
hostSpecs,
|
||||
deployments,
|
||||
}:
|
||||
let
|
||||
deployNodes = lib.filterAttrs (name: _: deployments ? ${name}) hostSpecs;
|
||||
in
|
||||
{
|
||||
meta = {
|
||||
nixpkgs = metaNixpkgs;
|
||||
nodeNixpkgs = lib.mapAttrs (
|
||||
name: spec:
|
||||
import inputs.nixpkgs {
|
||||
system = spec.system;
|
||||
}
|
||||
) deployNodes;
|
||||
nodeSpecialArgs = lib.mapAttrs (_: spec: spec.specialArgs) deployNodes;
|
||||
allowApplyAll = false;
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs (
|
||||
name: spec:
|
||||
{
|
||||
imports = spec.modules or [ spec.configuration ];
|
||||
deployment = deployments.${name};
|
||||
}
|
||||
) deployNodes;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{ ... }: {
|
||||
flake.lib.rbwSshSocket =
|
||||
let
|
||||
# Bash/Fish init snippets — used by wisdom/shells/{bash,fish}.nix.
|
||||
bashSnippet = ''
|
||||
if [ -z "''${SSH_AUTH_SOCK:-}" ]; then
|
||||
if [ -n "''${XDG_RUNTIME_DIR:-}" ]; then
|
||||
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/rbw/ssh-agent-socket"
|
||||
else
|
||||
export SSH_AUTH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket"
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
fishSnippet =
|
||||
pkgs: ''
|
||||
if test -z "$SSH_AUTH_SOCK"
|
||||
if test -n "$XDG_RUNTIME_DIR"
|
||||
set -gx SSH_AUTH_SOCK "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket"
|
||||
else
|
||||
set -l _hm_uid (${pkgs.coreutils}/bin/id -u)
|
||||
set -gx SSH_AUTH_SOCK "/run/user/$_hm_uid/rbw/ssh-agent-socket"
|
||||
end
|
||||
end
|
||||
'';
|
||||
in {
|
||||
inherit bashSnippet fishSnippet;
|
||||
# Used by home-manager modules to expose SSH_AUTH_SOCK to GUI apps
|
||||
# and HM-managed shells declaratively (no activation script needed).
|
||||
sessionVariable = "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket";
|
||||
};
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
# Pure helpers: catalog → NixOS/HM/SSH shapes (`self.lib.usersMerge lib`).
|
||||
{ ... }: {
|
||||
flake.lib.usersMerge =
|
||||
lib:
|
||||
let
|
||||
userHm = user: user.homeManager or { };
|
||||
userSsh = user: user.ssh or { };
|
||||
in
|
||||
rec {
|
||||
resolveHomeManagerModule =
|
||||
moduleSpec:
|
||||
let
|
||||
t = builtins.typeOf moduleSpec;
|
||||
in
|
||||
if t == "path" || t == "string" then import moduleSpec else moduleSpec;
|
||||
|
||||
selectedUsersAttr =
|
||||
{ catalog, enabled, hostOverrides }:
|
||||
lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = lib.recursiveUpdate catalog.${name} (hostOverrides.${name} or { });
|
||||
}) enabled
|
||||
);
|
||||
|
||||
missingEnabledNames = catalog: enabled: builtins.filter (name: !(builtins.hasAttr name catalog)) enabled;
|
||||
|
||||
strayHomeUserKeys = homeUsers: enabled:
|
||||
builtins.filter (k: !(builtins.elem k enabled)) (builtins.attrNames homeUsers);
|
||||
|
||||
mkNixosUser =
|
||||
name: user:
|
||||
{
|
||||
isNormalUser = user.isNormalUser or true;
|
||||
description = user.description or name;
|
||||
extraGroups = user.extraGroups or [ ];
|
||||
}
|
||||
// lib.optionalAttrs (user ? hashedPasswordFile && user.hashedPasswordFile != null) {
|
||||
hashedPasswordFile = user.hashedPasswordFile;
|
||||
};
|
||||
|
||||
hmWiredNames =
|
||||
selectedUsers:
|
||||
lib.attrNames (
|
||||
lib.filterAttrs (_: user:
|
||||
let
|
||||
hm = userHm user;
|
||||
in
|
||||
(hm.enable or false) && (hm.module or null) != null
|
||||
) selectedUsers
|
||||
);
|
||||
|
||||
rbwOutboundSnippet =
|
||||
name: user:
|
||||
let
|
||||
outboundCfg = ((userSsh user).outbound or { }).rbw or { };
|
||||
in
|
||||
lib.mkIf (outboundCfg.enable or false) {
|
||||
chiasson.ssh.outbound.rbw.enable = true;
|
||||
chiasson.ssh.outbound.rbw.user = name;
|
||||
chiasson.ssh.outbound.rbw.hosts =
|
||||
if (outboundCfg.hosts or "all") == "all" then [ "all" ] else outboundCfg.hosts;
|
||||
chiasson.ssh.outbound.rbw.extraIdentities = outboundCfg.extraIdentities or [ ];
|
||||
};
|
||||
|
||||
mkHmUserModule =
|
||||
{ name, user, hostExtraModules }:
|
||||
let
|
||||
hm = userHm user;
|
||||
hmModule = resolveHomeManagerModule hm.module;
|
||||
in
|
||||
lib.mkMerge (
|
||||
[
|
||||
hmModule
|
||||
(rbwOutboundSnippet name user)
|
||||
]
|
||||
++ (hm.extraModules or [ ])
|
||||
++ hostExtraModules
|
||||
);
|
||||
|
||||
inboundAuthorizedOrNull =
|
||||
user:
|
||||
let
|
||||
inboundCfg = (userSsh user).inbound or { };
|
||||
in
|
||||
if !(inboundCfg.enable or false) then
|
||||
null
|
||||
else if (inboundCfg.authorizedHosts or "all") == "all" then
|
||||
"all"
|
||||
else
|
||||
inboundCfg.authorizedHosts;
|
||||
|
||||
inboundHostsAttr =
|
||||
selectedUsers:
|
||||
lib.pipe selectedUsers [
|
||||
(lib.mapAttrs (_: inboundAuthorizedOrNull))
|
||||
(lib.filterAttrs (_: v: v != null))
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# Resolve `wisdom*` HM slices for NixOS `extraModules` (`self` is not an HM specialArg).
|
||||
# Slices are gated by `chiasson.home.*.enable`; hosts only flip toggles in `home.nix`.
|
||||
{ lib, ... }: {
|
||||
flake.lib.wisdomCatalogExtraModules =
|
||||
self:
|
||||
let
|
||||
names = lib.sort builtins.lessThan (
|
||||
lib.filter (
|
||||
n:
|
||||
lib.hasPrefix "wisdom" n
|
||||
&& n != "wisdom"
|
||||
&& n != "wisdomShellBash"
|
||||
) (builtins.attrNames self.homeManagerModules)
|
||||
);
|
||||
in
|
||||
map (name: self.homeManagerModules.${name}) names;
|
||||
}
|
||||
@@ -7,10 +7,7 @@
|
||||
# Resolve which hosts to emit blocks for, and which user identity to
|
||||
# use as the IdentityFile filter against the rbw agent.
|
||||
selectedHostNames =
|
||||
if cfg.hosts == [ "all" ] then
|
||||
builtins.attrNames inventory.activeHosts
|
||||
else
|
||||
cfg.hosts;
|
||||
if cfg.hosts == [ "all" ] then builtins.attrNames inventory.activeHosts else cfg.hosts;
|
||||
missingHosts = builtins.filter (name: !(builtins.hasAttr name inventory.hosts)) selectedHostNames;
|
||||
# Resolve to the actual host attrs for the template. The "all" case
|
||||
# reuses `activeHosts` directly instead of rebuilding the same attrset.
|
||||
@@ -18,20 +15,21 @@
|
||||
if cfg.hosts == [ "all" ] then
|
||||
inventory.activeHosts
|
||||
else
|
||||
builtins.listToAttrs (map (n: { name = n; value = inventory.hosts.${n}; }) selectedHostNames);
|
||||
builtins.listToAttrs (
|
||||
map (n: {
|
||||
name = n;
|
||||
value = inventory.hosts.${n};
|
||||
}) selectedHostNames
|
||||
);
|
||||
# All identities this HM user may authenticate AS: cfg.user ∪
|
||||
# extraIdentities. `cfg.user` can be null (e.g. user not in inventory
|
||||
# yet) — in that case the Host-block IdentityFile line is suppressed
|
||||
# and cross-account SSH is wired from extraIdentities alone.
|
||||
effectiveIdentities = lib.filter (n: n != null) ([ cfg.user ] ++ cfg.extraIdentities);
|
||||
defaultUserHasKey =
|
||||
cfg.user == null
|
||||
|| (inventory.users.${cfg.user}.publicKey or "") != "";
|
||||
defaultUserHasKey = cfg.user == null || (inventory.users.${cfg.user}.publicKey or "") != "";
|
||||
# Each extra identity must have a pubkey in the inventory. Dropping
|
||||
# it silently would produce a dead Match block — fail the build.
|
||||
missingExtraKeys = lib.filter
|
||||
(n: (inventory.users.${n}.publicKey or "") == "")
|
||||
cfg.extraIdentities;
|
||||
missingExtraKeys = lib.filter (n: (inventory.users.${n}.publicKey or "") == "") cfg.extraIdentities;
|
||||
in
|
||||
{
|
||||
options.chiasson.ssh.outbound.rbw = {
|
||||
@@ -64,11 +62,14 @@
|
||||
extraIdentities = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "server" "builder" ];
|
||||
example = [
|
||||
"server"
|
||||
"builder"
|
||||
];
|
||||
description = ''
|
||||
Catalog accounts this HM user may authenticate AS, in addition
|
||||
to `cfg.user`. Each entry must have a publicKey pasted into
|
||||
`modules/lib/ssh-inventory.nix`. Typical for the operator's
|
||||
`modules/ssh/inventory.nix`. Typical for the operator's
|
||||
primary laptop: `[ "server" "builder" ]` so fleet/ops commands
|
||||
like `ssh server@r5500` or `ssh builder@nix-server` work
|
||||
without touching `/etc/passwd` on each host.
|
||||
@@ -76,73 +77,78 @@
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = missingHosts == [ ];
|
||||
message = "ssh.outbound.rbw: unknown host keys: ${builtins.concatStringsSep ", " missingHosts}";
|
||||
}
|
||||
{
|
||||
assertion = defaultUserHasKey;
|
||||
message = "ssh.outbound.rbw: no publicKey for inventory user `${cfg.user}` in `modules/lib/ssh-inventory.nix`.";
|
||||
}
|
||||
{
|
||||
assertion = missingExtraKeys == [ ];
|
||||
message = "ssh.outbound.rbw: extraIdentities has entries with no publicKey in `modules/lib/ssh-inventory.nix`: ${builtins.concatStringsSep ", " missingExtraKeys}";
|
||||
}
|
||||
# `enable=true` with neither a default user nor any extras gives the
|
||||
# agent no IdentityFile at all; with IdentitiesOnly yes the user
|
||||
# would silently be unable to authenticate as any catalog user.
|
||||
{
|
||||
assertion = effectiveIdentities != [ ];
|
||||
message = "ssh.outbound.rbw: enabled but no identities wired. Set `chiasson.ssh.outbound.rbw.user` (defaults to `home.username`) or list catalog accounts under `extraIdentities`.";
|
||||
}
|
||||
];
|
||||
}
|
||||
(lib.mkMerge [
|
||||
config = lib.mkIf cfg.enable (
|
||||
lib.mkMerge [
|
||||
{
|
||||
home.packages = with pkgs; [ rbw pinentry-gtk2 ];
|
||||
home.sessionVariables.SSH_AUTH_SOCK = self.lib.rbwSshSocket.sessionVariable;
|
||||
# Write a `.pub` file per identity this HM user may act as
|
||||
# (cfg.user ∪ extraIdentities). OpenSSH reads each one via
|
||||
# IdentityFile/Match to filter agent keys — they never hold
|
||||
# private key material, so 0644 (HM's default) is fine.
|
||||
# OpenSSH's StrictModes only rejects group/other-writable files.
|
||||
home.file = lib.listToAttrs (map
|
||||
(n: {
|
||||
name = inventory.mkIdentityFileName n;
|
||||
value.text = "${inventory.users.${n}.publicKey}\n";
|
||||
})
|
||||
effectiveIdentities);
|
||||
programs.ssh.enable = lib.mkIf cfg.manageSshConfig false;
|
||||
assertions = [
|
||||
{
|
||||
assertion = missingHosts == [ ];
|
||||
message = "ssh.outbound.rbw: unknown host keys: ${builtins.concatStringsSep ", " missingHosts}";
|
||||
}
|
||||
{
|
||||
assertion = defaultUserHasKey;
|
||||
message = "ssh.outbound.rbw: no publicKey for inventory user `${cfg.user}` in `modules/ssh/inventory.nix`.";
|
||||
}
|
||||
{
|
||||
assertion = missingExtraKeys == [ ];
|
||||
message = "ssh.outbound.rbw: extraIdentities has entries with no publicKey in `modules/ssh/inventory.nix`: ${builtins.concatStringsSep ", " missingExtraKeys}";
|
||||
}
|
||||
# `enable=true` with neither a default user nor any extras gives the
|
||||
# agent no IdentityFile at all; with IdentitiesOnly yes the user
|
||||
# would silently be unable to authenticate as any catalog user.
|
||||
{
|
||||
assertion = effectiveIdentities != [ ];
|
||||
message = "ssh.outbound.rbw: enabled but no identities wired. Set `chiasson.ssh.outbound.rbw.user` (defaults to `home.username`) or list catalog accounts under `extraIdentities`.";
|
||||
}
|
||||
];
|
||||
}
|
||||
(lib.mkIf cfg.manageSshConfig {
|
||||
home.file.".ssh/config".text = inventory.mkSshConfigTemplate {
|
||||
inherit selectedHosts;
|
||||
user = cfg.user;
|
||||
inherit (cfg) extraIdentities;
|
||||
};
|
||||
})
|
||||
{
|
||||
systemd.user.services.rbw-agent-bootstrap = {
|
||||
Unit = {
|
||||
Description = "Bootstrap rbw SSH agent";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
After = [ "graphical-session.target" ];
|
||||
(lib.mkMerge [
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
rbw
|
||||
pinentry-gtk2
|
||||
];
|
||||
home.sessionVariables.SSH_AUTH_SOCK = self.lib.rbwSshSocket.sessionVariable;
|
||||
# Write a `.pub` file per identity this HM user may act as
|
||||
# (cfg.user ∪ extraIdentities). OpenSSH reads each one via
|
||||
# IdentityFile/Match to filter agent keys — they never hold
|
||||
# private key material, so 0644 (HM's default) is fine.
|
||||
# OpenSSH's StrictModes only rejects group/other-writable files.
|
||||
home.file = lib.listToAttrs (
|
||||
map (n: {
|
||||
name = inventory.mkIdentityFileName n;
|
||||
value.text = "${inventory.users.${n}.publicKey}\n";
|
||||
}) effectiveIdentities
|
||||
);
|
||||
programs.ssh.enable = lib.mkIf cfg.manageSshConfig false;
|
||||
}
|
||||
(lib.mkIf cfg.manageSshConfig {
|
||||
home.file.".ssh/config".text = inventory.mkSshConfigTemplate {
|
||||
inherit selectedHosts;
|
||||
user = cfg.user;
|
||||
inherit (cfg) extraIdentities;
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.bash}/bin/bash -lc '${pkgs.rbw}/bin/rbw unlocked >/dev/null 2>&1 || true'";
|
||||
RemainAfterExit = true;
|
||||
})
|
||||
{
|
||||
systemd.user.services.rbw-agent-bootstrap = {
|
||||
Unit = {
|
||||
Description = "Bootstrap rbw SSH agent";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
After = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.bash}/bin/bash -lc '${pkgs.rbw}/bin/rbw unlocked >/dev/null 2>&1 || true'";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
home.activation.rbwPinentryConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
${pkgs.rbw}/bin/rbw config set pinentry "${pkgs.pinentry-gtk2}/bin/pinentry-gtk-2" >/dev/null 2>&1 || true
|
||||
'';
|
||||
}
|
||||
])
|
||||
]);
|
||||
home.activation.rbwPinentryConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
${pkgs.rbw}/bin/rbw config set pinentry "${pkgs.pinentry-gtk2}/bin/pinentry-gtk-2" >/dev/null 2>&1 || true
|
||||
'';
|
||||
}
|
||||
])
|
||||
]
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{
|
||||
assertions = [{
|
||||
assertion = cfg.enable -> missingKeys == [ ];
|
||||
message = "chiasson.ssh.inbound: users enabled but missing public key in `modules/lib/ssh-inventory.nix`: ${lib.concatStringsSep ", " missingKeys}";
|
||||
message = "chiasson.ssh.inbound: users enabled but missing public key in `modules/ssh/inventory.nix`: ${lib.concatStringsSep ", " missingKeys}";
|
||||
}];
|
||||
}
|
||||
(lib.mkIf cfg.enable {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# rbw (Bitwarden) SSH-agent socket wiring, shared across shells and the
|
||||
# outbound-rbw Home Manager module. Exposed as `flake.lib.rbwSshSocket`.
|
||||
{ ... }: {
|
||||
flake.lib.rbwSshSocket =
|
||||
let
|
||||
socketPath = "$XDG_RUNTIME_DIR/rbw/ssh-agent-socket";
|
||||
|
||||
# Bash init snippet — used by wisdom/shells/bash.nix (profile + interactive).
|
||||
bashSnippet = ''
|
||||
if [ -z "''${SSH_AUTH_SOCK:-}" ]; then
|
||||
if [ -n "''${XDG_RUNTIME_DIR:-}" ]; then
|
||||
export SSH_AUTH_SOCK="${socketPath}"
|
||||
else
|
||||
export SSH_AUTH_SOCK="/run/user/$(id -u)/rbw/ssh-agent-socket"
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
# Fish init snippet — used by wisdom/shells/fish.nix.
|
||||
fishSnippet = pkgs: ''
|
||||
if test -z "$SSH_AUTH_SOCK"
|
||||
if test -n "$XDG_RUNTIME_DIR"
|
||||
set -gx SSH_AUTH_SOCK "${socketPath}"
|
||||
else
|
||||
set -l _hm_uid (${pkgs.coreutils}/bin/id -u)
|
||||
set -gx SSH_AUTH_SOCK "/run/user/$_hm_uid/rbw/ssh-agent-socket"
|
||||
end
|
||||
end
|
||||
'';
|
||||
in
|
||||
{
|
||||
inherit bashSnippet fishSnippet;
|
||||
# Used by home-manager modules to expose SSH_AUTH_SOCK to GUI apps
|
||||
# and HM-managed shells declaratively (no activation script needed).
|
||||
sessionVariable = socketPath;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+138
-27
@@ -8,18 +8,115 @@
|
||||
{ config, options, lib, ... }:
|
||||
let
|
||||
cfg = config.chiasson.users;
|
||||
usersLib = self.lib.usersMerge lib;
|
||||
usersLib =
|
||||
let
|
||||
userHm = user: user.homeManager or { };
|
||||
userSsh = user: user.ssh or { };
|
||||
in
|
||||
rec {
|
||||
resolveHomeManagerModule =
|
||||
moduleSpec:
|
||||
let
|
||||
t = builtins.typeOf moduleSpec;
|
||||
in
|
||||
if t == "path" || t == "string" then import moduleSpec else moduleSpec;
|
||||
|
||||
selectedUsersAttr =
|
||||
{ catalog, enabled, hostOverrides }:
|
||||
lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = lib.recursiveUpdate catalog.${name} (hostOverrides.${name} or { });
|
||||
}) enabled
|
||||
);
|
||||
|
||||
missingEnabledNames =
|
||||
catalog: enabled: builtins.filter (name: !(builtins.hasAttr name catalog)) enabled;
|
||||
|
||||
strayHomeUserKeys =
|
||||
homeUsers: enabled: builtins.filter (k: !(builtins.elem k enabled)) (builtins.attrNames homeUsers);
|
||||
|
||||
mkNixosUser =
|
||||
name: user:
|
||||
{
|
||||
isNormalUser = user.isNormalUser or true;
|
||||
description = user.description or name;
|
||||
extraGroups = user.extraGroups or [ ];
|
||||
}
|
||||
// lib.optionalAttrs (user ? hashedPasswordFile && user.hashedPasswordFile != null) {
|
||||
hashedPasswordFile = user.hashedPasswordFile;
|
||||
};
|
||||
|
||||
hmWiredNames =
|
||||
selectedUsers:
|
||||
lib.attrNames (
|
||||
lib.filterAttrs (
|
||||
_: user:
|
||||
let
|
||||
hm = userHm user;
|
||||
in
|
||||
(hm.enable or false) && (hm.module or null) != null
|
||||
) selectedUsers
|
||||
);
|
||||
|
||||
rbwOutboundSnippet =
|
||||
name: user:
|
||||
let
|
||||
outboundCfg = ((userSsh user).outbound or { }).rbw or { };
|
||||
in
|
||||
lib.mkIf (outboundCfg.enable or false) {
|
||||
chiasson.ssh.outbound.rbw.enable = true;
|
||||
chiasson.ssh.outbound.rbw.user = name;
|
||||
chiasson.ssh.outbound.rbw.hosts =
|
||||
if (outboundCfg.hosts or "all") == "all" then [ "all" ] else outboundCfg.hosts;
|
||||
chiasson.ssh.outbound.rbw.extraIdentities = outboundCfg.extraIdentities or [ ];
|
||||
};
|
||||
|
||||
mkHmUserModule =
|
||||
{ name, user, hostExtraModules }:
|
||||
let
|
||||
hm = userHm user;
|
||||
hmModule = resolveHomeManagerModule hm.module;
|
||||
in
|
||||
lib.mkMerge (
|
||||
[
|
||||
hmModule
|
||||
(rbwOutboundSnippet name user)
|
||||
]
|
||||
++ (hm.extraModules or [ ])
|
||||
++ hostExtraModules
|
||||
);
|
||||
|
||||
inboundAuthorizedOrNull =
|
||||
user:
|
||||
let
|
||||
inboundCfg = (userSsh user).inbound or { };
|
||||
in
|
||||
if !(inboundCfg.enable or false) then
|
||||
null
|
||||
else if (inboundCfg.authorizedHosts or "all") == "all" then
|
||||
"all"
|
||||
else
|
||||
inboundCfg.authorizedHosts;
|
||||
|
||||
inboundHostsAttr =
|
||||
selectedUsers:
|
||||
lib.pipe selectedUsers [
|
||||
(lib.mapAttrs (_: inboundAuthorizedOrNull))
|
||||
(lib.filterAttrs (_: v: v != null))
|
||||
];
|
||||
};
|
||||
inventory = self.lib.sshInventory;
|
||||
|
||||
olivierEnabled = lib.elem "olivier" cfg.enabled;
|
||||
|
||||
# Merge catalog + per-host overrides into the final user attrs.
|
||||
selectedUsers = lib.listToAttrs (map
|
||||
(name: {
|
||||
selectedUsers = lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = lib.recursiveUpdate cfg.catalog.${name} (cfg.hostOverrides.${name} or { });
|
||||
})
|
||||
cfg.enabled);
|
||||
}) cfg.enabled
|
||||
);
|
||||
|
||||
names = usersLib.hmWiredNames selectedUsers;
|
||||
missing = usersLib.missingEnabledNames cfg.catalog cfg.enabled;
|
||||
@@ -27,26 +124,27 @@
|
||||
hmAvailable = lib.hasAttrByPath [ "home-manager" "users" ] options;
|
||||
inboundUsersAttr = usersLib.inboundHostsAttr selectedUsers;
|
||||
|
||||
hmUsersAttr = lib.listToAttrs (map
|
||||
(name: {
|
||||
hmUsersAttr = lib.listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = usersLib.mkHmUserModule {
|
||||
inherit name;
|
||||
user = selectedUsers.${name};
|
||||
hostExtraModules = cfg.extraModules.${name} or [ ];
|
||||
};
|
||||
})
|
||||
names);
|
||||
}) names
|
||||
);
|
||||
|
||||
# Fish shell wiring: HM's fish module declares its package, but
|
||||
# /etc/passwd + /etc/shells need updating so login shells work.
|
||||
# Only fires for HM users that actually enable fish.
|
||||
hmFishUsers =
|
||||
if !hmAvailable then { }
|
||||
if !hmAvailable then
|
||||
{ }
|
||||
else
|
||||
lib.filterAttrs
|
||||
(name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names)
|
||||
config.home-manager.users;
|
||||
lib.filterAttrs (
|
||||
name: hmUser: (hmUser.programs.fish.enable or false) && builtins.elem name names
|
||||
) config.home-manager.users;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
@@ -108,12 +206,18 @@
|
||||
isNormalUser = true;
|
||||
description = "Olivier";
|
||||
extraGroups = [
|
||||
"networkmanager" "wheel" "docker" "fuse" "uinput" "kvm"
|
||||
"networkmanager"
|
||||
"wheel"
|
||||
"docker"
|
||||
"fuse"
|
||||
"uinput"
|
||||
"kvm"
|
||||
# `video` lets brightnessctl/light udev rules own /sys/backlight without sudo.
|
||||
# Harmless on headless hosts (no devices).
|
||||
"video"
|
||||
# DRI render + input for gamescope / Steam on Wayland, no sudo.
|
||||
"render" "input"
|
||||
"render"
|
||||
"input"
|
||||
];
|
||||
hashedPasswordFile = lib.mkIf olivierEnabled (
|
||||
config.sops.secrets."users/olivier/hashedPassword".path
|
||||
@@ -147,14 +251,20 @@
|
||||
# `.pub` filter for each of these and emits a `Match user`
|
||||
# block, so `ssh server@r5500` / `ssh builder@nix-server`
|
||||
# route the right private key from the rbw agent.
|
||||
ssh.outbound.rbw.extraIdentities = [ "server" "builder" ];
|
||||
ssh.outbound.rbw.extraIdentities = [
|
||||
"server"
|
||||
"builder"
|
||||
];
|
||||
};
|
||||
|
||||
server = {
|
||||
isNormalUser = true;
|
||||
description = "Server user";
|
||||
extraGroups = [ "wheel" ];
|
||||
homeManager = { enable = false; module = null; };
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
ssh.inbound.enable = true;
|
||||
ssh.inbound.authorizedHosts = "all";
|
||||
ssh.outbound.rbw.enable = false;
|
||||
@@ -166,7 +276,10 @@
|
||||
description = "Navi fleet deploy (push + activate only)";
|
||||
extraGroups = [ ];
|
||||
createHome = false;
|
||||
homeManager = { enable = false; module = null; };
|
||||
homeManager = {
|
||||
enable = false;
|
||||
module = null;
|
||||
};
|
||||
ssh.inbound.enable = true;
|
||||
ssh.inbound.authorizedHosts = "all";
|
||||
};
|
||||
@@ -195,9 +308,7 @@
|
||||
];
|
||||
|
||||
# NixOS user accounts (no HM, raw).
|
||||
users.users = lib.mapAttrs
|
||||
(name: user: usersLib.mkNixosUser name user)
|
||||
selectedUsers;
|
||||
users.users = lib.mapAttrs (name: user: usersLib.mkNixosUser name user) selectedUsers;
|
||||
}
|
||||
(lib.optionalAttrs hmAvailable {
|
||||
"home-manager".useGlobalPkgs = lib.mkIf (cfg.homeManager.autoWire && names != [ ]) true;
|
||||
@@ -216,12 +327,12 @@
|
||||
# /etc/shells with `mkForce`. `mkNixosUser` doesn't emit `shell`, so
|
||||
# the force is uncontested.
|
||||
(lib.mkIf (hmFishUsers != { }) {
|
||||
environment.shells = lib.mkAfter (lib.mapAttrsToList
|
||||
(_: hmUser: lib.getExe hmUser.programs.fish.package)
|
||||
hmFishUsers);
|
||||
users.users = lib.mapAttrs
|
||||
(name: hmUser: { shell = lib.mkForce (lib.getExe hmUser.programs.fish.package); })
|
||||
hmFishUsers;
|
||||
environment.shells = lib.mkAfter (
|
||||
lib.mapAttrsToList (_: hmUser: lib.getExe hmUser.programs.fish.package) hmFishUsers
|
||||
);
|
||||
users.users = lib.mapAttrs (name: hmUser: {
|
||||
shell = lib.mkForce (lib.getExe hmUser.programs.fish.package);
|
||||
}) hmFishUsers;
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user