diff --git a/modules/features/git.nix b/modules/features/git.nix index 31f95e3..f97aac7 100644 --- a/modules/features/git.nix +++ b/modules/features/git.nix @@ -9,6 +9,7 @@ let user = config.meta.user; primaryEmail = builtins.head (lib.filter (email: email.primary) (builtins.attrValues user.emails)); + usesScopedIdentity = user != null && user.sourceControl.profiles != { }; in { programs.git = { @@ -20,6 +21,8 @@ ]; settings = { init.defaultBranch = "main"; + } + // lib.optionalAttrs (!usesScopedIdentity) { user = { name = user.realName; email = primaryEmail.address; diff --git a/modules/features/meta.nix b/modules/features/meta.nix index c6d7228..4488b2b 100644 --- a/modules/features/meta.nix +++ b/modules/features/meta.nix @@ -26,6 +26,69 @@ let } ); + sourceControlHostKeyType = lib.types.submodule ( + { ... }: + { + options = { + publicKey = lib.mkOption { + type = lib.types.str; + }; + + privateKeyPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + }; + }; + } + ); + + sourceControlHostUserType = lib.types.submodule ( + { ... }: + { + options = { + personal = mkNullableOption sourceControlHostKeyType; + work = mkNullableOption sourceControlHostKeyType; + }; + } + ); + + hostSourceControlType = lib.types.submodule ( + { ... }: + { + options.users = lib.mkOption { + type = lib.types.attrsOf sourceControlHostUserType; + default = { }; + }; + } + ); + + sourceControlProfileType = lib.types.submodule ( + { ... }: + { + options = { }; + } + ); + + sourceControlType = lib.types.submodule ( + { ... }: + { + options = { + profiles = lib.mkOption { + type = lib.types.attrsOf sourceControlProfileType; + default = { }; + }; + + projectScope = lib.mkOption { + type = lib.types.enum [ + "personal" + "work" + ]; + default = "personal"; + }; + }; + } + ); + userType = lib.types.submodule ( { config, ... }: { @@ -54,6 +117,11 @@ let emails = lib.mkOption { type = lib.types.attrsOf emailType; }; + + sourceControl = lib.mkOption { + type = sourceControlType; + default = { }; + }; }; } ); @@ -111,20 +179,28 @@ let { ... }: { options = { - accelProfile = mkNullableOption (lib.types.nullOr (lib.types.enum [ - "adaptive" - "flat" - ])); + accelProfile = mkNullableOption ( + lib.types.nullOr ( + lib.types.enum [ + "adaptive" + "flat" + ] + ) + ); accelSpeed = mkNullableOption (lib.types.nullOr lib.types.float); leftHanded = mkNullableOption (lib.types.nullOr lib.types.bool); middleEmulation = mkNullableOption (lib.types.nullOr lib.types.bool); naturalScrolling = mkNullableOption (lib.types.nullOr lib.types.bool); - scrollMethod = mkNullableOption (lib.types.nullOr (lib.types.enum [ - "no-scroll" - "two-finger" - "edge" - "on-button-down" - ])); + scrollMethod = mkNullableOption ( + lib.types.nullOr ( + lib.types.enum [ + "no-scroll" + "two-finger" + "edge" + "on-button-down" + ] + ) + ); }; } ); @@ -133,25 +209,37 @@ let { ... }: { options = { - accelProfile = mkNullableOption (lib.types.nullOr (lib.types.enum [ - "adaptive" - "flat" - ])); + accelProfile = mkNullableOption ( + lib.types.nullOr ( + lib.types.enum [ + "adaptive" + "flat" + ] + ) + ); accelSpeed = mkNullableOption (lib.types.nullOr lib.types.float); - clickMethod = mkNullableOption (lib.types.nullOr (lib.types.enum [ - "button-areas" - "clickfinger" - ])); + clickMethod = mkNullableOption ( + lib.types.nullOr ( + lib.types.enum [ + "button-areas" + "clickfinger" + ] + ) + ); disableWhileTyping = mkNullableOption (lib.types.nullOr lib.types.bool); leftHanded = mkNullableOption (lib.types.nullOr lib.types.bool); middleEmulation = mkNullableOption (lib.types.nullOr lib.types.bool); naturalScrolling = mkNullableOption (lib.types.nullOr lib.types.bool); - scrollMethod = mkNullableOption (lib.types.nullOr (lib.types.enum [ - "no-scroll" - "two-finger" - "edge" - "on-button-down" - ])); + scrollMethod = mkNullableOption ( + lib.types.nullOr ( + lib.types.enum [ + "no-scroll" + "two-finger" + "edge" + "on-button-down" + ] + ) + ); tapping = mkNullableOption (lib.types.nullOr lib.types.bool); }; } @@ -196,6 +284,11 @@ let type = lib.types.attrsOf userType; default = { }; }; + + sourceControl = lib.mkOption { + type = hostSourceControlType; + default = { }; + }; }; } ); diff --git a/modules/features/source-control.nix b/modules/features/source-control.nix new file mode 100644 index 0000000..46c032c --- /dev/null +++ b/modules/features/source-control.nix @@ -0,0 +1,160 @@ +{ config, lib, ... }: +let + homeModules = config.flake.modules.homeManager; + serviceHostNames = { + github = "github.com"; + gitlab = "gitlab.com"; + }; +in +{ + flake.modules.homeManager.source-control = + { + config, + lib, + ... + }: + let + host = config.meta.host; + user = config.meta.user; + sourceControl = user.sourceControl; + hostSourceControlUsers = host.sourceControl.users; + hostUserSourceControl = + if lib.hasAttr user.name hostSourceControlUsers then hostSourceControlUsers.${user.name} else { }; + profileNames = builtins.attrNames sourceControl.profiles; + + parsedProfiles = map ( + name: + let + matches = builtins.match "(github|gitlab)-(personal|work)" name; + in + { + inherit matches name; + isValid = matches != null; + scope = if matches == null then null else builtins.elemAt matches 1; + service = if matches == null then null else builtins.elemAt matches 0; + } + ) profileNames; + + validProfiles = builtins.filter (profile: profile.isValid) parsedProfiles; + invalidProfileNames = map (profile: profile.name) ( + builtins.filter (profile: !profile.isValid) parsedProfiles + ); + + emailNamesForScope = { + personal = [ + "personal" + "main" + ]; + work = [ "work" ]; + }; + + scopeEmails = + scope: + map (name: user.emails.${name}) ( + builtins.filter (name: lib.hasAttr name user.emails) emailNamesForScope.${scope} + ); + + emailForScope = + scope: + let + emails = scopeEmails scope; + in + if builtins.length emails == 1 then (builtins.head emails).address else null; + + scopeConfig = + scope: if lib.hasAttr scope hostUserSourceControl then hostUserSourceControl.${scope} else null; + + privateKeyPathForScope = + scope: + let + keyConfig = scopeConfig scope; + in + if keyConfig == null || keyConfig.privateKeyPath == null then + "~/.ssh/id_${scope}" + else + keyConfig.privateKeyPath; + + scopePublicKey = + scope: + let + keyConfig = scopeConfig scope; + in + if keyConfig == null then null else keyConfig.publicKey; + + scopesInUse = lib.unique ( + [ + "personal" + sourceControl.projectScope + ] + ++ map (profile: profile.scope) validProfiles + ); + + missingKeyScopes = builtins.filter (scope: scopePublicKey scope == null) scopesInUse; + invalidEmailScopes = builtins.filter (scope: emailForScope scope == null) scopesInUse; + allowedSignersLines = map (scope: "${emailForScope scope} ${scopePublicKey scope}") ( + builtins.filter (scope: emailForScope scope != null && scopePublicKey scope != null) scopesInUse + ); + + gitConfigForScope = scope: { + gpg.ssh.allowedSignersFile = "${config.xdg.configHome}/git/allowed_signers"; + user = { + name = user.realName; + email = emailForScope scope; + signingKey = "${privateKeyPathForScope scope}.pub"; + }; + }; + + gitRoots = [ + { + root = user.nixosConfigurationPath; + scope = "personal"; + } + { + root = config.xdg.userDirs.projects; + scope = sourceControl.projectScope; + } + ]; + in + { + imports = [ homeModules.git ]; + + assertions = [ + { + assertion = invalidProfileNames == [ ]; + message = "Invalid source control profiles for `${user.name}`: ${lib.concatStringsSep ", " invalidProfileNames}. Expected `-` using github/gitlab and personal/work."; + } + { + assertion = missingKeyScopes == [ ]; + message = "Missing source control keys for `${user.name}` scopes: ${lib.concatStringsSep ", " missingKeyScopes}."; + } + { + assertion = invalidEmailScopes == [ ]; + message = "Expected exactly one email selected by name for `${user.name}` scopes: ${lib.concatStringsSep ", " invalidEmailScopes}. Personal uses `personal` or `main`; work uses `work`."; + } + ]; + + xdg.configFile."git/allowed_signers".text = lib.concatStringsSep "\n" ( + allowedSignersLines ++ [ "" ] + ); + + programs.git.includes = map (gitRoot: { + condition = "gitdir:${gitRoot.root}/"; + contents = gitConfigForScope gitRoot.scope; + }) gitRoots; + + programs.ssh = { + enable = true; + matchBlocks = lib.listToAttrs ( + map ( + profile: + lib.nameValuePair profile.name { + hostname = serviceHostNames.${profile.service}; + identitiesOnly = true; + identityFile = privateKeyPathForScope profile.scope; + user = "git"; + } + ) validProfiles + ); + }; + }; +} diff --git a/modules/features/terminal.nix b/modules/features/terminal.nix index 62b90a4..f8a4bb9 100644 --- a/modules/features/terminal.nix +++ b/modules/features/terminal.nix @@ -17,8 +17,7 @@ in }; hasTerminalPackage = terminalPackage != null; hasMainProgram = hasTerminalPackage && terminalPackage ? meta.mainProgram; - terminalDesktopId = - if hasMainProgram then "${terminalPackage.meta.mainProgram}.desktop" else null; + terminalDesktopId = if hasMainProgram then "${terminalPackage.meta.mainProgram}.desktop" else null; in { assertions = [ @@ -31,7 +30,8 @@ in message = "Terminal package `${lib.showAttrPath config.meta.user.terminalPackagePath}` must define `meta.mainProgram`."; } { - assertion = hasMainProgram && builtins.pathExists "${terminalPackage}/share/applications/${terminalDesktopId}"; + assertion = + hasMainProgram && builtins.pathExists "${terminalPackage}/share/applications/${terminalDesktopId}"; message = "Terminal package `${lib.showAttrPath config.meta.user.terminalPackagePath}` must provide `${terminalDesktopId}`."; } { diff --git a/modules/features/workstation-base.nix b/modules/features/workstation-base.nix index f80f115..7420d4d 100644 --- a/modules/features/workstation-base.nix +++ b/modules/features/workstation-base.nix @@ -35,7 +35,6 @@ in homeModules.ai homeModules.clipboard homeModules.dev-tools - homeModules.git homeModules.local-apps homeModules.mpv homeModules.neovim @@ -46,6 +45,7 @@ in homeModules.podman homeModules.shell homeModules.sops + homeModules.source-control homeModules.ssh-client homeModules.terminal homeModules.theme diff --git a/modules/hosts/orion/default.nix b/modules/hosts/orion/default.nix index 92fea6e..e8bc5f1 100644 --- a/modules/hosts/orion/default.nix +++ b/modules/hosts/orion/default.nix @@ -42,7 +42,8 @@ in }; environment.systemPackages = [ - ] ++ lib.optional (terminalPackage != null && lib.elem "terminfo" terminalPackage.outputs) ( + ] + ++ lib.optional (terminalPackage != null && lib.elem "terminfo" terminalPackage.outputs) ( lib.getOutput "terminfo" terminalPackage ); }; diff --git a/modules/hosts/polaris/default.nix b/modules/hosts/polaris/default.nix index ed135f8..7b7acd1 100644 --- a/modules/hosts/polaris/default.nix +++ b/modules/hosts/polaris/default.nix @@ -29,6 +29,15 @@ in mouse.accelSpeed = 0.4; }; + sourceControl.users = { + kiri.personal.publicKey = ""; + + ergon = { + personal.publicKey = ""; + work.publicKey = ""; + }; + }; + users = { inherit (metaLib.users) ergon diff --git a/modules/hosts/zenith/default.nix b/modules/hosts/zenith/default.nix index 06e2cd5..2f9b692 100644 --- a/modules/hosts/zenith/default.nix +++ b/modules/hosts/zenith/default.nix @@ -30,6 +30,15 @@ in mouse.accelSpeed = 0.4; }; + sourceControl.users = { + kiri.personal.publicKey = ""; + + ergon = { + personal.publicKey = ""; + work.publicKey = ""; + }; + }; + users = { inherit (metaLib.users) ergon diff --git a/modules/lib.nix b/modules/lib.nix index 21fbb33..9db522d 100644 --- a/modules/lib.nix +++ b/modules/lib.nix @@ -9,6 +9,7 @@ let name, displays ? { }, input ? { }, + sourceControl ? { }, users ? { }, imports ? [ ], stateVersion ? "24.05", @@ -19,6 +20,7 @@ let displays input name + sourceControl users ; }; diff --git a/modules/users.nix b/modules/users.nix index 0f2c495..46741fa 100644 --- a/modules/users.nix +++ b/modules/users.nix @@ -6,7 +6,7 @@ let homeDirectory = "/home/kiri"; terminalPackagePath = [ "kitty" ]; emails = { - main = { + personal = { address = "mail@jelles.net"; primary = true; type = "mxrouting"; @@ -27,6 +27,12 @@ let type = "office365"; }; }; + sourceControl = { + profiles = { + github-personal = { }; + gitlab-personal = { }; + }; + }; }; ergon = { @@ -35,12 +41,26 @@ let homeDirectory = "/home/ergon"; terminalPackagePath = [ "kitty" ]; emails = { + personal = { + address = "mail@jelles.net"; + primary = false; + type = "mxrouting"; + }; work = { address = "jelle.spreeuwenberg@yookr.org"; primary = true; type = "office365"; }; }; + sourceControl = { + profiles = { + github-personal = { }; + github-work = { }; + gitlab-personal = { }; + }; + + projectScope = "work"; + }; }; in {