feat: add source control identity management
This commit is contained in:
@@ -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;
|
||||
|
||||
+103
-10
@@ -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 [
|
||||
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 [
|
||||
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 [
|
||||
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 [
|
||||
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 [
|
||||
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 = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 `<service>-<scope>` 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
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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}`.";
|
||||
}
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,15 @@ in
|
||||
mouse.accelSpeed = 0.4;
|
||||
};
|
||||
|
||||
sourceControl.users = {
|
||||
kiri.personal.publicKey = "";
|
||||
|
||||
ergon = {
|
||||
personal.publicKey = "";
|
||||
work.publicKey = "";
|
||||
};
|
||||
};
|
||||
|
||||
users = {
|
||||
inherit (metaLib.users)
|
||||
ergon
|
||||
|
||||
@@ -30,6 +30,15 @@ in
|
||||
mouse.accelSpeed = 0.4;
|
||||
};
|
||||
|
||||
sourceControl.users = {
|
||||
kiri.personal.publicKey = "";
|
||||
|
||||
ergon = {
|
||||
personal.publicKey = "";
|
||||
work.publicKey = "";
|
||||
};
|
||||
};
|
||||
|
||||
users = {
|
||||
inherit (metaLib.users)
|
||||
ergon
|
||||
|
||||
@@ -9,6 +9,7 @@ let
|
||||
name,
|
||||
displays ? { },
|
||||
input ? { },
|
||||
sourceControl ? { },
|
||||
users ? { },
|
||||
imports ? [ ],
|
||||
stateVersion ? "24.05",
|
||||
@@ -19,6 +20,7 @@ let
|
||||
displays
|
||||
input
|
||||
name
|
||||
sourceControl
|
||||
users
|
||||
;
|
||||
};
|
||||
|
||||
+21
-1
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user