chore: format all files

This commit is contained in:
ibizaman 2025-10-28 20:27:25 +01:00
parent eaad978e37
commit 9fed9a669f
77 changed files with 10265 additions and 8177 deletions

View file

@ -5,8 +5,8 @@ let
targetPort = 2222;
in
{
imports =
[ # Include the results of the hardware scan.
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
@ -26,7 +26,10 @@ in
# Options above are needed to deploy in a VM.
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# We need to create the user we will deploy with.
users.users.${targetUser} = {
@ -41,9 +44,11 @@ in
# The user we're deploying with must be able to run sudo without password.
security.sudo.extraRules = [
{ users = [ targetUser ];
{
users = [ targetUser ];
commands = [
{ command = "ALL";
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];

View file

@ -6,13 +6,20 @@
sops-nix.url = "github:Mic92/sops-nix";
};
outputs = inputs@{ self, selfhostblocks, sops-nix }:
outputs =
inputs@{
self,
selfhostblocks,
sops-nix,
}:
let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
inherit (selfhostblocks.lib.${system}) pkgs;
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
./configuration.nix
selfhostblocks.nixosModules.authelia
@ -67,7 +74,9 @@
];
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.lldap = {
enable = true;
domain = "example.com";
@ -122,7 +131,9 @@
specialArgs = inputs;
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
basic
];
@ -135,7 +146,9 @@
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
imports = [
basic
ldap

View file

@ -3,50 +3,63 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports =
[ (modulesPath + "/profiles/qemu-guest.nix")
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ];
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"virtio_pci"
"floppy"
"sr_mod"
"virtio_blk"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/vda";
fileSystems."/" = {
device = "/dev/vda";
fsType = "ext4";
};
fileSystems."/nix/.ro-store" =
{ device = "nix-store";
fileSystems."/nix/.ro-store" = {
device = "nix-store";
fsType = "9p";
};
fileSystems."/nix/.rw-store" =
{ device = "tmpfs";
fileSystems."/nix/.rw-store" = {
device = "tmpfs";
fsType = "tmpfs";
};
fileSystems."/tmp/shared" =
{ device = "shared";
fileSystems."/tmp/shared" = {
device = "shared";
fsType = "9p";
};
fileSystems."/tmp/xchg" =
{ device = "xchg";
fileSystems."/tmp/xchg" = {
device = "xchg";
fsType = "9p";
};
fileSystems."/nix/store" =
{ device = "overlay";
fileSystems."/nix/store" = {
device = "overlay";
fsType = "overlay";
};
fileSystems."/boot" =
{ device = "/dev/vdb2";
fileSystems."/boot" = {
device = "/dev/vdb2";
fsType = "vfat";
};

View file

@ -5,8 +5,8 @@ let
targetPort = 2222;
in
{
imports =
[ # Include the results of the hardware scan.
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
@ -27,7 +27,10 @@ in
# Options above are needed to deploy in a VM.
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# We need to create the user we will deploy with.
users.users.${targetUser} = {
@ -42,9 +45,11 @@ in
# The user we're deploying with must be able to run sudo without password.
security.sudo.extraRules = [
{ users = [ targetUser ];
{
users = [ targetUser ];
commands = [
{ command = "ALL";
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];

View file

@ -6,13 +6,20 @@
sops-nix.url = "github:Mic92/sops-nix";
};
outputs = inputs@{ self, selfhostblocks, sops-nix }:
outputs =
inputs@{
self,
selfhostblocks,
sops-nix,
}:
let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
inherit (selfhostblocks.lib.${system}) pkgs;
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
./configuration.nix
selfhostblocks.nixosModules.authelia
@ -49,7 +56,9 @@
shb.nginx.debugLog = false;
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.lldap = {
enable = true;
domain = "example.com";
@ -78,7 +87,9 @@
};
};
sso = { config, lib, ... }: {
sso =
{ config, lib, ... }:
{
shb.certs = {
cas.selfsigned.myca = {
name = "My CA";
@ -103,8 +114,7 @@
domain-needed = true;
# no-resolv = true;
bogus-priv = true;
address =
map (hostname: "/${hostname}/127.0.0.1") [
address = map (hostname: "/${hostname}/127.0.0.1") [
"example.com"
"n.example.com"
"ldap.example.com"
@ -136,10 +146,14 @@
request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.key = "lldap/user_password";
};
shb.sops.secret."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request;
shb.sops.secret."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request;
shb.sops.secret."authelia/hmac_secret".request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
shb.sops.secret."authelia/private_key".request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
shb.sops.secret."authelia/session_secret".request =
config.shb.authelia.secrets.sessionSecret.request;
shb.sops.secret."authelia/storage_encryption_key".request =
config.shb.authelia.secrets.storageEncryptionKey.request;
shb.sops.secret."authelia/hmac_secret".request =
config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
shb.sops.secret."authelia/private_key".request =
config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
shb.nextcloud.apps.sso = {
enable = true;
@ -198,7 +212,9 @@
specialArgs = inputs;
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
basic
];
@ -210,7 +226,9 @@
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
imports = [
basic
ldap
@ -223,7 +241,9 @@
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
imports = [
basic
ldap

View file

@ -3,50 +3,63 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports =
[ (modulesPath + "/profiles/qemu-guest.nix")
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ];
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"virtio_pci"
"floppy"
"sr_mod"
"virtio_blk"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/vda";
fileSystems."/" = {
device = "/dev/vda";
fsType = "ext4";
};
fileSystems."/nix/.ro-store" =
{ device = "nix-store";
fileSystems."/nix/.ro-store" = {
device = "nix-store";
fsType = "9p";
};
fileSystems."/nix/.rw-store" =
{ device = "tmpfs";
fileSystems."/nix/.rw-store" = {
device = "tmpfs";
fsType = "tmpfs";
};
fileSystems."/tmp/shared" =
{ device = "shared";
fileSystems."/tmp/shared" = {
device = "shared";
fsType = "9p";
};
fileSystems."/tmp/xchg" =
{ device = "xchg";
fileSystems."/tmp/xchg" = {
device = "xchg";
fsType = "9p";
};
fileSystems."/nix/store" =
{ device = "overlay";
fileSystems."/nix/store" = {
device = "overlay";
fsType = "overlay";
};
fileSystems."/boot" =
{ device = "/dev/vdb2";
fileSystems."/boot" = {
device = "/dev/vdb2";
fsType = "vfat";
};

View file

@ -1,40 +1,49 @@
# Taken nearly verbatim from https://github.com/nix-community/home-manager/pull/4673
# Read these docs online at https://shb.skarabox.com.
{ pkgs
, buildPackages
, lib
, nmdsrc
, stdenv
, documentation-highlighter
, nixos-render-docs
{
pkgs,
buildPackages,
lib,
nmdsrc,
stdenv,
documentation-highlighter,
nixos-render-docs,
, release
, allModules
release,
allModules,
, version ? builtins.readFile ../VERSION
, substituteVersionIn
version ? builtins.readFile ../VERSION,
substituteVersionIn,
, modules
modules,
}:
let
shbPath = toString ./..;
gitHubDeclaration = user: repo: subpath:
let urlRef = "main";
gitHubDeclaration =
user: repo: subpath:
let
urlRef = "main";
end = if subpath == "" then "" else "/" + subpath;
in {
in
{
url = "https://github.com/${user}/${repo}/blob/${urlRef}${end}";
name = "<${repo}${end}>";
};
ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url;
buildOptionsDocs = { modules, filterOptionPath ? null }: args:
buildOptionsDocs =
{
modules,
filterOptionPath ? null,
}:
args:
let
config = {
_module.check = false;
_module.args = {};
_module.args = { };
system.stateVersion = "22.11";
};
@ -52,41 +61,56 @@ let
};
options = lib.setAttrByPath filterOptionPath (lib.getAttrFromPath filterOptionPath eval.options);
in buildPackages.nixosOptionsDoc ({
in
buildPackages.nixosOptionsDoc (
{
inherit options;
transformOptions = opt:
opt // {
transformOptions =
opt:
opt
// {
# Clean up declaration sites to not refer to the Home Manager
# source tree.
declarations = map (decl:
gitHubDeclaration "ibizaman" "selfhostblocks"
(lib.removePrefix "/" (lib.removePrefix shbPath (toString decl)))) opt.declarations;
declarations = map (
decl:
gitHubDeclaration "ibizaman" "selfhostblocks" (
lib.removePrefix "/" (lib.removePrefix shbPath (toString decl))
)
) opt.declarations;
};
} // builtins.removeAttrs args [ "includeModuleSystemOptions" ]);
}
// builtins.removeAttrs args [ "includeModuleSystemOptions" ]
);
scrubbedModule = {
_module.args.pkgs = lib.mkForce (nmd.scrubDerivations "pkgs" pkgs);
_module.check = false;
};
allOptionsDocs = paths: (buildOptionsDocs
allOptionsDocs =
paths:
(buildOptionsDocs
{
modules = paths ++ allModules ++ [ scrubbedModule ];
filterOptionPath = [ "shb" ];
}
{
variablelistId = "selfhostblocks-options";
}).optionsJSON;
}
).optionsJSON;
individualModuleOptionsDocs = filterOptionPath: paths: (buildOptionsDocs
individualModuleOptionsDocs =
filterOptionPath: paths:
(buildOptionsDocs
{
modules = paths ++ [ scrubbedModule ];
inherit filterOptionPath;
}
{
variablelistId = "selfhostblocks-options";
}).optionsJSON;
}
).optionsJSON;
nmd = import nmdsrc {
inherit lib;
@ -94,15 +118,15 @@ let
# `nmd` uses to work around the broken stylesheets in
# `docbook-xsl-ns`, so we restore the patched version here.
pkgs = pkgs // {
docbook-xsl-ns =
pkgs.docbook-xsl-ns.override { withManOptDedupPatch = true; };
docbook-xsl-ns = pkgs.docbook-xsl-ns.override { withManOptDedupPatch = true; };
};
};
outputPath = "share/doc/selfhostblocks";
manpage-urls = pkgs.writeText "manpage-urls.json" ''{}'';
in stdenv.mkDerivation {
in
stdenv.mkDerivation {
name = "self-host-blocks-manual";
nativeBuildInputs = [ nixos-render-docs ];
@ -135,28 +159,41 @@ in stdenv.mkDerivation {
${nmdsrc}/static/highlightjs/highlight.load.js
''
+ lib.concatStringsSep "\n" (map (m: ''
+ lib.concatStringsSep "\n" (
map (m: ''
substituteInPlace ${m} --replace '@VERSION@' ${version}
'') substituteVersionIn)
'') substituteVersionIn
)
+ ''
substituteInPlace ./options.md \
--replace \
'@OPTIONS_JSON@' \
${allOptionsDocs [
${
allOptionsDocs [
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix")
]}/share/doc/nixos/options.json
]
}/share/doc/nixos/options.json
''
+ lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cfg':
+ lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: cfg':
let
cfg = if builtins.isAttrs cfg' then cfg' else { module = cfg'; };
module = if builtins.isList cfg.module then cfg.module else [ cfg.module ];
optionRoot = cfg.optionRoot or [ "shb" (lib.last (lib.splitString "/" name)) ];
in ''
optionRoot =
cfg.optionRoot or [
"shb"
(lib.last (lib.splitString "/" name))
];
in
''
substituteInPlace ./modules/${name}/docs/default.md \
--replace-fail \
'@OPTIONS_JSON@' \
${individualModuleOptionsDocs optionRoot module}/share/doc/nixos/options.json
'') modules)
''
) modules
)
+ ''
find . -name "*.md" -print0 | \
while IFS= read -r -d ''' f; do

153
flake.nix
View file

@ -11,7 +11,17 @@
};
};
outputs = inputs@{ self, nixpkgs, nix-flake-tests, flake-utils, nmdsrc, ... }: flake-utils.lib.eachDefaultSystem (system:
outputs =
inputs@{
self,
nixpkgs,
nix-flake-tests,
flake-utils,
nmdsrc,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
originPkgs = nixpkgs.legacyPackages.${system};
shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [
@ -24,20 +34,24 @@
# hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk=";
# })
];
patchNixpkgs = {
patchNixpkgs =
{
nixpkgs,
patches,
system,
}: nixpkgs.legacyPackages.${system}.applyPatches {
}:
nixpkgs.legacyPackages.${system}.applyPatches {
name = "nixpkgs-patched";
src = nixpkgs;
inherit patches;
};
patchedNixpkgs = (patchNixpkgs {
patchedNixpkgs = (
patchNixpkgs {
nixpkgs = inputs.nixpkgs;
patches = shbPatches;
inherit system;
});
}
);
pkgs = import patchedNixpkgs {
inherit system;
config.allowUnfree = true;
@ -45,11 +59,18 @@
(final: prev: {
lib = prev.lib // {
shb = self.lib.${system};
evalModules = args: ((prev.lib.makeOverridable prev.lib.evalModules) args).override (prevAttrs: {
specialArgs = (prevAttrs.specialArgs or {}) // { inherit (pkgs) lib; };
evalModules =
args:
((prev.lib.makeOverridable prev.lib.evalModules) args).override (prevAttrs: {
specialArgs = (prevAttrs.specialArgs or { }) // {
inherit (pkgs) lib;
};
});
};
nixosSystem = args: ((prev.lib.makeOverridable (import "${patchedNixpkgs}/nixos/lib/eval-config.nix")) args).override (prevAttrs: {
nixosSystem =
args:
((prev.lib.makeOverridable (import "${patchedNixpkgs}/nixos/lib/eval-config.nix")) args).override
(prevAttrs: {
inherit (pkgs) lib;
});
})
@ -67,10 +88,12 @@
packages.manualHtml = pkgs.callPackage ./docs {
inherit nmdsrc;
allModules = self.nixosModules.default.imports
allModules =
self.nixosModules.default.imports
++ [
self.nixosModules.sops
] ++ contractDummyModules;
]
++ contractDummyModules;
release = builtins.readFile ./VERSION;
substituteVersionIn = [
@ -82,7 +105,10 @@
"blocks/lldap" = ./modules/blocks/lldap.nix;
"blocks/ssl" = {
module = ./modules/blocks/ssl.nix;
optionRoot = [ "shb" "certs" ];
optionRoot = [
"shb"
"certs"
];
};
"blocks/mitmdump" = ./modules/blocks/mitmdump.nix;
"blocks/monitoring" = ./modules/blocks/monitoring.nix;
@ -99,26 +125,45 @@
"services/karakeep" = ./modules/services/karakeep.nix;
"services/nextcloud-server" = {
module = ./modules/services/nextcloud-server.nix;
optionRoot = [ "shb" "nextcloud" ];
optionRoot = [
"shb"
"nextcloud"
];
};
"services/open-webui" = ./modules/services/open-webui.nix;
"services/pinchflat" = ./modules/services/pinchflat.nix;
"services/vaultwarden" = ./modules/services/vaultwarden.nix;
"contracts/backup" = {
module = ./modules/contracts/backup/dummyModule.nix;
optionRoot = [ "shb" "contracts" "backup" ];
optionRoot = [
"shb"
"contracts"
"backup"
];
};
"contracts/databasebackup" = {
module = ./modules/contracts/databasebackup/dummyModule.nix;
optionRoot = [ "shb" "contracts" "databasebackup" ];
optionRoot = [
"shb"
"contracts"
"databasebackup"
];
};
"contracts/secret" = {
module = ./modules/contracts/secret/dummyModule.nix;
optionRoot = [ "shb" "contracts" "secret" ];
optionRoot = [
"shb"
"contracts"
"secret"
];
};
"contracts/ssl" = {
module = ./modules/contracts/ssl/dummyModule.nix;
optionRoot = [ "shb" "contracts" "ssl" ];
optionRoot = [
"shb"
"contracts"
"ssl"
];
};
};
};
@ -165,7 +210,8 @@
in
(self.packages.${system}.manualHtml.override {
nixos-render-docs = nixos-render-docs-patched;
}).overrideAttrs (old: {
}).overrideAttrs
(old: {
installPhase = ''
${old.installPhase}
ln -sf share/doc/selfhostblocks/redirects.json $out/redirects.json
@ -173,35 +219,52 @@
});
lib =
(pkgs.callPackage ./lib {})
// (pkgs.callPackage ./test/common.nix {})
(pkgs.callPackage ./lib { })
// (pkgs.callPackage ./test/common.nix { })
// {
contracts = pkgs.callPackage ./modules/contracts {};
contracts = pkgs.callPackage ./modules/contracts { };
patches = shbPatches;
inherit patchNixpkgs patchedNixpkgs pkgs;
};
checks =
let
inherit (pkgs.lib) foldl foldlAttrs removeAttrs mergeAttrs optionalAttrs;
inherit (pkgs.lib)
foldl
foldlAttrs
removeAttrs
mergeAttrs
optionalAttrs
;
importFiles = files:
map (m: pkgs.callPackage m {}) files;
importFiles = files: map (m: pkgs.callPackage m { }) files;
mergeTests = foldl mergeAttrs {};
mergeTests = foldl mergeAttrs { };
flattenAttrs = root: attrset: foldlAttrs (acc: name: value: acc // {
flattenAttrs =
root: attrset:
foldlAttrs (
acc: name: value:
acc
// {
"${root}_${name}" = value;
}) {} attrset;
}
) { } attrset;
vm_test = name: path: flattenAttrs "vm_${name}" (
removeAttrs (pkgs.callPackage path {}) [ "override" "overrideDerivation" ]
vm_test =
name: path:
flattenAttrs "vm_${name}" (
removeAttrs (pkgs.callPackage path { }) [
"override"
"overrideDerivation"
]
);
in (optionalAttrs (system == "x86_64-linux") ({
in
(optionalAttrs (system == "x86_64-linux") (
{
modules = pkgs.lib.shb.check {
inherit pkgs;
tests =
mergeTests (importFiles [
tests = mergeTests (importFiles [
./test/modules/davfs.nix
# TODO: Make this not use IFD
./test/modules/lib.nix
@ -211,7 +274,7 @@
# TODO: Make this not use IFD
lib = nix-flake-tests.lib.check {
inherit pkgs;
tests = pkgs.callPackage ./test/modules/lib.nix {};
tests = pkgs.callPackage ./test/modules/lib.nix { };
};
}
// (vm_test "arr" ./test/services/arr.nix)
@ -245,8 +308,13 @@
# To see the traces, run:
# nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
packages.playwright =
pkgs.callPackage ({ stdenvNoCC, makeWrapper, playwright }: stdenvNoCC.mkDerivation {
packages.playwright = pkgs.callPackage (
{
stdenvNoCC,
makeWrapper,
playwright,
}:
stdenvNoCC.mkDerivation {
name = "playwright";
src = playwright;
@ -261,14 +329,19 @@
--set PLAYWRIGHT_BROWSERS_PATH ${pkgs.playwright-driver.browsers} \
--set PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS true
'';
}) {};
}
) { };
# Run "nix run .#update-redirects" to regenerate docs/redirects.json
apps.update-redirects = {
type = "app";
program = "${pkgs.writeShellApplication {
program = "${
pkgs.writeShellApplication {
name = "update-redirects";
runtimeInputs = [ pkgs.nix pkgs.jq ];
runtimeInputs = [
pkgs.nix
pkgs.jq
];
text = ''
echo "=== SelfHostBlocks Redirects Updater ==="
echo "Generating fresh ./docs/redirects.json..."
@ -283,10 +356,12 @@
echo " Updated docs/redirects.json"
echo "To verify: nix build .#manualHtml"
'';
}}/bin/update-redirects";
}
}/bin/update-redirects";
};
}
) // {
)
// {
herculesCI.ciSystems = [ "x86_64-linux" ];
nixosModules.default = {

View file

@ -9,7 +9,14 @@ rec {
# - resultPath is the location the config file should have on the filesystem.
# - generator is a function taking two arguments name and value and returning path in the nix
# nix store where the
replaceSecrets = { userConfig, resultPath, generator, user ? null, permissions ? "u=r,g=r,o=" }:
replaceSecrets =
{
userConfig,
resultPath,
generator,
user ? null,
permissions ? "u=r,g=r,o=",
}:
let
configWithTemplates = withReplacements userConfig;
@ -24,21 +31,40 @@ rec {
};
replaceSecretsFormatAdapter = format: format.generate;
replaceSecretsGeneratorAdapter = generator: name: value: pkgs.writeText "generator " (generator value);
toEnvVar = replaceSecretsGeneratorAdapter (v: (lib.generators.toINIWithGlobalSection {} { globalSection = v; }));
replaceSecretsGeneratorAdapter =
generator: name: value:
pkgs.writeText "generator " (generator value);
toEnvVar = replaceSecretsGeneratorAdapter (
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
);
template = file: newPath: replacements: replaceSecretsScript {
template =
file: newPath: replacements:
replaceSecretsScript {
inherit file replacements;
resultPath = newPath;
};
genReplacement = secret:
genReplacement =
secret:
let
t = { transform ? null, ... }: if isNull transform then x: x else transform;
t =
{
transform ? null,
...
}:
if isNull transform then x: x else transform;
in
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
replaceSecretsScript = { file, resultPath, replacements, user ? null, permissions ? "u=r,g=r,o=" }:
replaceSecretsScript =
{
file,
resultPath,
replacements,
user ? null,
permissions ? "u=r,g=r,o=",
}:
let
templatePath = resultPath + ".template";
@ -47,13 +73,15 @@ rec {
# step. Otherwise, the $(cat ...) commands inside the sed
# replacements could fail but not fail individually but
# not fail the whole script.
checkPermissions = concatMapStringsSep "\n" (pattern: "cat ${pattern.source} > /dev/null") replacements;
checkPermissions = concatMapStringsSep "\n" (
pattern: "cat ${pattern.source} > /dev/null"
) replacements;
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (map genReplacement replacements);
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (
map genReplacement replacements
);
sedCmd = if replacements == []
then "cat"
else "${pkgs.gnused}/bin/sed ${sedPatterns}";
sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}";
in
''
set -euo pipefail
@ -64,9 +92,11 @@ rec {
ln -fs ${file} ${templatePath}
rm -f ${resultPath}
touch ${resultPath}
'' + (lib.optionalString (user != null) ''
''
+ (lib.optionalString (user != null) ''
chown ${user} ${resultPath}
'') + ''
'')
+ ''
${sedCmd} ${templatePath} > ${resultPath}
chmod ${permissions} ${resultPath}
'';
@ -89,26 +119,25 @@ rec {
};
};
secretName = names:
"%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
secretName =
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
withReplacements = attrs:
withReplacements =
attrs:
let
valueOrReplacement = name: value:
if !(builtins.isAttrs value && value ? "source")
then value
else secretName name;
valueOrReplacement =
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
in
mapAttrsRecursiveCond (v: ! v ? "source") valueOrReplacement attrs;
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
getReplacements = attrs:
getReplacements =
attrs:
let
addNameField = name: value:
if !(builtins.isAttrs value && value ? "source")
then value
else value // { name = name; };
addNameField =
name: value:
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs;
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
in
collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
@ -121,13 +150,16 @@ rec {
# Attribute set or list to recursively map over.
set:
let
recurse = path: val:
if builtins.isAttrs val && cond val
then lib.attrsets.mapAttrs (n: v: recurse (path ++ [n]) v) val
else if builtins.isList val && cond val
then lib.lists.imap0 (i: v: recurse (path ++ [(builtins.toString i)]) v) val
else f path val;
in recurse [] set;
recurse =
path: val:
if builtins.isAttrs val && cond val then
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
else if builtins.isList val && cond val then
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
else
f path val;
in
recurse [ ] set;
# Like lib.attrsets.collect but also recurses on lists.
collect =
@ -142,16 +174,23 @@ rec {
else if builtins.isList attrs then
lib.lists.concatMap (collect pred) attrs
else
[];
[ ];
indent = i: str: lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
indent =
i: str:
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
# Generator for XML
formatXML = {
enclosingRoot ? null
}: {
type = with lib.types; let
valueType = nullOr (oneOf [
formatXML =
{
enclosingRoot ? null,
}:
{
type =
with lib.types;
let
valueType =
nullOr (oneOf [
bool
int
float
@ -159,21 +198,31 @@ rec {
path
(attrsOf valueType)
(listOf valueType)
]) // {
])
// {
description = "XML value";
};
in valueType;
in
valueType;
generate = name: value: pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
value = builtins.toJSON (
if enclosingRoot == null then
value
else
{ ${enclosingRoot} = value; });
generate =
name: value:
pkgs.callPackage (
{ runCommand, python3 }:
runCommand "config"
{
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
passAsFile = [ "value" ];
} (pkgs.writers.writePython3 "dict2xml" {
libraries = with python3.pkgs; [ python dict2xml ];
} ''
}
(
pkgs.writers.writePython3 "dict2xml"
{
libraries = with python3.pkgs; [
python
dict2xml
];
}
''
import os
import json
from dict2xml import dict2xml
@ -185,18 +234,28 @@ rec {
os.exit(2)
with open(os.environ["out"], "w") as out:
out.write(dict2xml(content))
'')) {};
''
)
) { };
};
parseXML = xml:
parseXML =
xml:
let
xmlToJsonFile = pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
xmlToJsonFile = pkgs.callPackage (
{ runCommand, python3 }:
runCommand "config"
{
inherit xml;
passAsFile = [ "xml" ];
} (pkgs.writers.writePython3 "xml2json" {
}
(
pkgs.writers.writePython3 "xml2json"
{
libraries = with python3.pkgs; [ python ];
} ''
}
''
import os
import json
from collections import ChainMap
@ -219,34 +278,54 @@ rec {
with open(os.environ["out"], "w") as out:
out.write(j)
'')) {};
''
)
) { };
in
builtins.fromJSON (builtins.readFile xmlToJsonFile);
renameAttrName = attrset: from: to:
(lib.attrsets.filterAttrs (name: v: name == from) attrset) // {
renameAttrName =
attrset: from: to:
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
// {
${to} = attrset.${from};
};
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
# with a nicer diff display function.
check = { pkgs, tests }:
check =
{ pkgs, tests }:
let
formatValue = val:
if (builtins.isList val || builtins.isAttrs val) then builtins.toJSON val
else builtins.toString val;
formatValue =
val:
if (builtins.isList val || builtins.isAttrs val) then
builtins.toJSON val
else
builtins.toString val;
resultToString = { name, expected, result }:
builtins.readFile (pkgs.runCommand "nix-flake-tests-error" {
resultToString =
{
name,
expected,
result,
}:
builtins.readFile (
pkgs.runCommand "nix-flake-tests-error"
{
expected = formatValue expected;
result = formatValue result;
passAsFile = [ "expected" "result" ];
} ''
passAsFile = [
"expected"
"result"
];
}
''
echo "${name} failed (- expected, + result)" > $out
cp ''${expectedPath} ''${expectedPath}.json
cp ''${resultPath} ''${resultPath}.json
${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
'');
''
);
results = pkgs.lib.runTests tests;
in
@ -255,8 +334,14 @@ rec {
else
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
genConfigOutOfBandSystemd = { config, configLocation, generator, user ? null, permissions ? "u=r,g=r,o=" }:
genConfigOutOfBandSystemd =
{
config,
configLocation,
generator,
user ? null,
permissions ? "u=r,g=r,o=",
}:
{
loadCredentials = getLoadCredentials "source" config;
preStart = lib.mkBefore (replaceSecrets {
@ -267,32 +352,33 @@ rec {
});
};
updateToLoadCredentials = sourceField: rootDir: attrs:
updateToLoadCredentials =
sourceField: rootDir: attrs:
let
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
valueOrLoadCredential = path: value:
if ! (hasPlaceholderField value)
then value
else value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
valueOrLoadCredential =
path: value:
if !(hasPlaceholderField value) then
value
else
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
in
mapAttrsRecursiveCond (v: ! (hasPlaceholderField v)) valueOrLoadCredential attrs;
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
getLoadCredentials = sourceField: attrs:
getLoadCredentials =
sourceField: attrs:
let
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
addPathField = path: value:
if ! (hasPlaceholderField value)
then value
else value // { inherit path; };
addPathField =
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
secretsWithPath = mapAttrsRecursiveCond (v: ! (hasPlaceholderField v)) addPathField attrs;
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
genLoadCredentials = secret:
"${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
in
map genLoadCredentials allSecrets;

View file

@ -1,10 +1,16 @@
{ config, options, pkgs, lib, ... }:
{
config,
options,
pkgs,
lib,
...
}:
let
cfg = config.shb.authelia;
opt = options.shb.authelia;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}";
@ -148,25 +154,25 @@ in
extraOidcClaimsPolicies = lib.mkOption {
description = "Extra OIDC claims policies.";
type = lib.types.attrsOf lib.types.attrs;
default = {};
default = { };
};
extraOidcScopes = lib.mkOption {
description = "Extra OIDC scopes.";
type = lib.types.attrsOf lib.types.attrs;
default = {};
default = { };
};
extraOidcAuthorizationPolicies = lib.mkOption {
description = "Extra OIDC authorization policies.";
type = lib.types.attrsOf lib.types.attrs;
default = {};
default = { };
};
extraDefinitions = lib.mkOption {
description = "Extra definitions.";
type = lib.types.attrsOf lib.types.attrs;
default = {};
default = { };
};
oidcClients = lib.mkOption {
@ -178,10 +184,11 @@ in
client_secret.source = pkgs.writeText "dummy.secret" "dummy_client_secret";
public = false;
authorization_policy = "one_factor";
redirect_uris = [];
redirect_uris = [ ];
}
];
type = lib.types.listOf (lib.types.submodule {
type = lib.types.listOf (
lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.anything;
options = {
@ -222,7 +229,13 @@ in
};
authorization_policy = lib.mkOption {
type = lib.types.enum ([ "one_factor" "two_factor" ] ++ lib.attrNames cfg.extraOidcAuthorizationPolicies);
type = lib.types.enum (
[
"one_factor"
"two_factor"
]
++ lib.attrNames cfg.extraOidcAuthorizationPolicies
);
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -235,8 +248,13 @@ in
scopes = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims";
example = [ "openid" "profile" "email" "groups" ];
default = [];
example = [
"openid"
"profile"
"email"
"groups"
];
default = [ ];
};
claims_policy = lib.mkOption {
@ -250,7 +268,8 @@ in
default = "default";
};
};
});
}
);
};
smtp = lib.mkOption {
@ -263,7 +282,8 @@ in
default = "/tmp/authelia-notifications";
type = lib.types.oneOf [
lib.types.str
(lib.types.nullOr (lib.types.submodule {
(lib.types.nullOr (
lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
@ -299,14 +319,15 @@ in
};
};
};
}))
}
))
];
};
rules = lib.mkOption {
type = lib.types.listOf lib.types.anything;
description = "Rule based clients";
default = [];
default = [ ];
};
mount = lib.mkOption {
@ -324,8 +345,12 @@ in
```
'';
readOnly = true;
default = { path = "/var/lib/authelia-authelia.${cfg.domain}"; };
defaultText = { path = "/var/lib/authelia-authelia.example.com"; };
default = {
path = "/var/lib/authelia-authelia.${cfg.domain}";
};
defaultText = {
path = "/var/lib/authelia-authelia.example.com";
};
};
mountRedis = lib.mkOption {
@ -343,7 +368,9 @@ in
```
'';
readOnly = true;
default = { path = "/var/lib/redis-authelia"; };
default = {
path = "/var/lib/redis-authelia";
};
};
debug = lib.mkOption {
@ -370,7 +397,7 @@ in
# Overriding the user name so we don't allow any weird characters anywhere. For example, postgres users do not accept the '.'.
users = {
groups.${autheliaCfg.user} = {};
groups.${autheliaCfg.user} = { };
users.${autheliaCfg.user} = {
isSystemUser = true;
group = autheliaCfg.user;
@ -395,7 +422,9 @@ in
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.password.result.path);
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (
toString cfg.smtp.password.result.path
);
};
settings = {
server.address = "tcp://127.0.0.1:${toString listenPort}";
@ -428,10 +457,12 @@ in
# Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis
session = {
name = "authelia_session";
cookies = [{
cookies = [
{
domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}";
authelia_url = "https://${cfg.subdomain}.${cfg.domain}";
}];
}
];
same_site = "lax";
expiration = "1h";
inactivity = "5m";
@ -468,7 +499,11 @@ in
networks = [
{
name = "internal";
networks = [ "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/18" ];
networks = [
"10.0.0.0/8"
"172.16.0.0/12"
"192.168.0.0/18"
];
}
];
rules = [
@ -479,7 +514,8 @@ in
"^/api/.*"
];
}
] ++ cfg.rules;
]
++ cfg.rules;
};
telemetry = {
metrics = {
@ -489,17 +525,25 @@ in
};
log.level = if cfg.debug then "debug" else "info";
} // {
}
// {
identity_providers.oidc = {
claims_policies = {
# This default claim should go away at some point.
# https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter
default.id_token = [ "email" "preferred_username" "name" "groups" ];
} // cfg.extraOidcClaimsPolicies;
default.id_token = [
"email"
"preferred_username"
"name"
"groups"
];
}
// cfg.extraOidcClaimsPolicies;
scopes = cfg.extraOidcScopes;
authorization_policies = cfg.extraOidcAuthorizationPolicies;
};
} // lib.optionalAttrs (cfg.extraDefinitions != {}) {
}
// lib.optionalAttrs (cfg.extraDefinitions != { }) {
definitions = cfg.extraDefinitions;
};
@ -508,18 +552,22 @@ in
systemd.services."authelia-${fqdn}".preStart =
let
mkCfg = clients:
mkCfg =
clients:
lib.shb.replaceSecrets {
userConfig = {
identity_providers.oidc.clients = clients;
};
resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML {});
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { });
};
in
lib.mkBefore (mkCfg cfg.oidcClients + ''
lib.mkBefore (
mkCfg cfg.oidcClients
+ ''
${pkgs.bash}/bin/bash -c '(while ! ${pkgs.netcat-openbsd}/bin/nc -z -v -w1 ${cfg.ldapHostname} ${toString cfg.ldapPort}; do echo "Waiting for port ${cfg.ldapHostname}:${toString cfg.ldapPort} to open..."; sleep 2; done); sleep 2'
'');
''
);
services.nginx.virtualHosts.${fqdn} = {
forceSSL = !(isNull cfg.ssl);
@ -579,7 +627,8 @@ in
after = [ "authelia-${fqdn}.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [
"--set" "verbose_pattern=/api"
"--set"
"verbose_pattern=/api"
];
};
@ -600,7 +649,7 @@ in
job_name = "authelia";
static_configs = [
{
targets = ["127.0.0.1:9959"];
targets = [ "127.0.0.1:9959" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
@ -610,13 +659,16 @@ in
}
];
systemd.targets."authelia-${fqdn}" = let
systemd.targets."authelia-${fqdn}" =
let
services = [
"authelia-${fqdn}.service"
] ++ lib.optionals cfg.debug [
]
++ lib.optionals cfg.debug [
config.shb.mitmdump.instances."authelia-${fqdn}".serviceName
];
in {
in
{
after = services;
requires = services;

View file

@ -1,4 +1,10 @@
{ config, pkgs, lib, utils, ... }:
{
config,
pkgs,
lib,
utils,
...
}:
let
cfg = config.shb.borgbackup;
@ -17,7 +23,9 @@ let
type = lib.types.path;
};
encryption_passcommand = "cat /run/secrets/borgmatic/passphrases/${if isNull instance.secretName then name else instance.secretName}";
encryption_passcommand = "cat /run/secrets/borgmatic/passphrases/${
if isNull instance.secretName then name else instance.secretName
}";
borg_keys_directory = "/run/secrets/borgmatic/keys";
sourceDirectories = lib.mkOption {
@ -28,7 +36,7 @@ let
excludePatterns = lib.mkOption {
description = "Exclude patterns.";
type = lib.types.listOf lib.types.str;
default = [];
default = [ ];
};
secretName = lib.mkOption {
@ -39,7 +47,8 @@ let
repositories = lib.mkOption {
description = "Repositories to back this instance to.";
type = lib.types.nonEmptyListOf (lib.types.submodule {
type = lib.types.nonEmptyListOf (
lib.types.submodule {
options = {
path = lib.mkOption {
type = lib.types.str;
@ -60,12 +69,18 @@ let
};
};
};
});
}
);
};
retention = lib.mkOption {
description = "Retention options.";
type = lib.types.attrsOf (lib.types.oneOf [ lib.types.int lib.types.nonEmptyStr ]);
type = lib.types.attrsOf (
lib.types.oneOf [
lib.types.int
lib.types.nonEmptyStr
]
);
default = {
keep_within = "1d";
keep_hourly = 24;
@ -78,7 +93,7 @@ let
consistency = lib.mkOption {
description = "Consistency frequency options.";
type = lib.types.attrsOf lib.types.nonEmptyStr;
default = {};
default = { };
example = {
repository = "2 weeks";
archives = "1 month";
@ -87,19 +102,19 @@ let
hooks = lib.mkOption {
description = "Hooks to run before or after the backup.";
default = {};
default = { };
type = lib.types.submodule {
options = {
beforeBackup = lib.mkOption {
description = "Hooks to run before backup";
type = lib.types.listOf lib.types.str;
default = [];
default = [ ];
};
afterBackup = lib.mkOption {
description = "Hooks to run after backup";
type = lib.types.listOf lib.types.str;
default = [];
default = [ ];
};
};
};
@ -113,7 +128,8 @@ let
};
};
repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (lib.strings.removePrefix "/" name);
repoSlugName =
name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (lib.strings.removePrefix "/" name);
in
{
@ -132,10 +148,12 @@ in
instances = lib.mkOption {
description = "Each instance is a backup setting";
default = {};
type = lib.types.attrsOf (lib.types.submodule {
default = { };
type = lib.types.attrsOf (
lib.types.submodule {
options = instanceOptions;
});
}
);
};
borgServer = lib.mkOption {
@ -148,7 +166,7 @@ in
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
performance = lib.mkOption {
description = "Reduce performance impact of backup jobs.";
default = {};
default = { };
type = lib.types.submodule {
options = {
niceness = lib.mkOption {
@ -157,7 +175,11 @@ in
default = 15;
};
ioSchedulingClass = lib.mkOption {
type = lib.types.enum [ "idle" "best-effort" "realtime" ];
type = lib.types.enum [
"idle"
"best-effort"
"realtime"
];
description = "ionice scheduling class, defaults to best-effort IO.";
default = "best-effort";
};
@ -171,10 +193,11 @@ in
};
};
config = lib.mkIf (cfg.instances != {}) (
config = lib.mkIf (cfg.instances != { }) (
let
enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances;
in lib.mkMerge [
in
lib.mkMerge [
# Secrets configuration
{
users.users = {
@ -195,70 +218,97 @@ in
sops.secrets =
let
mkSopsSecret = name: instance: (
mkSopsSecret =
name: instance:
(
[
{
"${instance.backend}/passphrases/${if isNull instance.secretName then name else instance.secretName}" = {
"${instance.backend}/passphrases/${
if isNull instance.secretName then name else instance.secretName
}" =
{
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
}
] ++ lib.optional ((lib.filter ({path, ...}: lib.strings.hasPrefix "s3" path) instance.repositories) != []) {
"${instance.backend}/environmentfiles/${if isNull instance.secretName then name else instance.secretName}" = {
]
++
lib.optional
((lib.filter ({ path, ... }: lib.strings.hasPrefix "s3" path) instance.repositories) != [ ])
{
"${instance.backend}/environmentfiles/${
if isNull instance.secretName then name else instance.secretName
}" =
{
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
} ++ lib.optionals (instance.backend == "borgmatic") (lib.flatten (map ({path, ...}: {
}
++ lib.optionals (instance.backend == "borgmatic") (
lib.flatten (
map (
{ path, ... }:
{
"${instance.backend}/keys/${repoSlugName path}" = {
key = "${instance.backend}/keys/${if isNull instance.secretName then name else instance.secretName}";
key = "${instance.backend}/keys/${
if isNull instance.secretName then name else instance.secretName
}";
sopsFile = instance.keySopsFile;
mode = "0440";
owner = cfg.user;
group = cfg.group;
};
}) instance.repositories))
}
) instance.repositories
)
)
);
in
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances));
}
# Borgmatic configuration
{
systemd.timers.borgmatic = lib.mkIf (enabledInstances != {}) {
systemd.timers.borgmatic = lib.mkIf (enabledInstances != { }) {
timerConfig = {
OnCalendar = "hourly";
};
};
systemd.services.borgmatic = lib.mkIf (enabledInstances != {}) {
systemd.services.borgmatic = lib.mkIf (enabledInstances != { }) {
serviceConfig = {
User = cfg.user;
Group = cfg.group;
ExecStartPre = [ "" ]; # Do not sleep before starting.
ExecStart = [ "" "${pkgs.borgmatic}/bin/borgmatic --verbosity -1 --syslog-verbosity 1" ];
ExecStart = [
""
"${pkgs.borgmatic}/bin/borgmatic --verbosity -1 --syslog-verbosity 1"
];
# For borgmatic, since we have only one service, we need to merge all environmentFile
# from all instances.
EnvironmentFile = lib.mapAttrsToList (name: value: value.environmentFile) enabledInstances;
};
};
systemd.packages = lib.mkIf (enabledInstances != {}) [ pkgs.borgmatic ];
systemd.packages = lib.mkIf (enabledInstances != { }) [ pkgs.borgmatic ];
environment.systemPackages = (
lib.optionals cfg.borgServer [ pkgs.borgbackup ]
++ lib.optionals (enabledInstances != {}) [ pkgs.borgbackup pkgs.borgmatic ]
++ lib.optionals (enabledInstances != { }) [
pkgs.borgbackup
pkgs.borgmatic
]
);
environment.etc =
let
mkSettings = name: instance: {
"borgmatic.d/${name}.yaml".text = lib.generators.toYAML {} {
location =
{
"borgmatic.d/${name}.yaml".text = lib.generators.toYAML { } {
location = {
source_directories = instance.sourceDirectories;
repositories = map ({path, ...}: path) instance.repositories;
repositories = map ({ path, ... }: path) instance.repositories;
}
// (lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) {
excludePatterns = instance.excludePatterns;
@ -291,5 +341,6 @@ in
in
lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings enabledInstances);
}
]);
]
);
}

View file

@ -7,8 +7,9 @@ in
options.shb.davfs = {
mounts = lib.mkOption {
description = "List of mounts.";
default = [];
type = lib.types.listOf (lib.types.submodule {
default = [ ];
type = lib.types.listOf (
lib.types.submodule {
options = {
remoteUrl = lib.mkOption {
type = lib.types.str;
@ -66,7 +67,8 @@ in
default = true;
};
};
});
}
);
};
};

View file

@ -1,16 +1,26 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.hardcodedsecret;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
inherit (lib) mapAttrs' mkOption nameValuePair;
inherit (lib.types) attrsOf nullOr str submodule;
inherit (lib.types)
attrsOf
nullOr
str
submodule
;
inherit (pkgs) writeText;
in
{
options.shb.hardcodedsecret = mkOption {
default = {};
default = { };
description = ''
Hardcoded secrets. These should only be used in tests.
'';
@ -26,7 +36,10 @@ in
};
}
'';
type = attrsOf (submodule ({ name, ... }: {
type = attrsOf (
submodule (
{ name, ... }:
{
options = contracts.secret.mkProvider {
settings = mkOption {
description = ''
@ -62,15 +75,20 @@ in
path = "/run/hardcodedsecrets/hardcodedsecret_${name}";
};
};
}));
}
)
);
};
config = {
system.activationScripts = mapAttrs' (n: cfg':
system.activationScripts = mapAttrs' (
n: cfg':
let
source = if cfg'.settings.source != null
then cfg'.settings.source
else writeText "hardcodedsecret_${n}_content" cfg'.settings.content;
source =
if cfg'.settings.source != null then
cfg'.settings.source
else
writeText "hardcodedsecret_${n}_content" cfg'.settings.content;
in
nameValuePair "hardcodedsecret_${n}" ''
mkdir -p "$(dirname "${cfg'.result.path}")"

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.lldap;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -145,7 +150,9 @@ in
```
'';
readOnly = true;
default = { path = "/var/lib/lldap"; };
default = {
path = "/var/lib/lldap";
};
};
backup = lib.mkOption {
@ -340,10 +347,16 @@ in
locations."/" = {
extraConfig = ''
proxy_set_header Host $host;
'' + (if isNull cfg.restrictAccessIPRange then "" else ''
''
+ (
if isNull cfg.restrictAccessIPRange then
""
else
''
allow ${cfg.restrictAccessIPRange};
deny all;
'');
''
);
proxyPass = "http://${toString config.services.lldap.settings.http_host}:${toString config.shb.lldap.webUIListenPort}/";
};
};
@ -354,7 +367,7 @@ in
group = "lldap";
isSystemUser = true;
};
users.groups.lldap = {};
users.groups.lldap = { };
services.lldap = {
enable = true;
@ -382,9 +395,13 @@ in
};
inherit (cfg) ensureGroups ensureUserFields ensureGroupFields;
ensureUsers = lib.mapAttrs (n: v: (lib.removeAttrs v [ "password" ]) // {
ensureUsers = lib.mapAttrs (
n: v:
(lib.removeAttrs v [ "password" ])
// {
"password_file" = toString v.password.result.path;
}) cfg.ensureUsers;
}
) cfg.ensureUsers;
};
shb.mitmdump.instances."lldap-web" = lib.mkIf cfg.debug {
@ -393,7 +410,8 @@ in
after = [ "lldap.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [
"--set" "verbose_pattern=/api"
"--set"
"verbose_pattern=/api"
];
};
};

View file

@ -1,15 +1,34 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mapAttrs' mkOption nameValuePair types;
inherit (types) attrsOf listOf port submodule str;
inherit (lib)
mapAttrs'
mkOption
nameValuePair
types
;
inherit (types)
attrsOf
listOf
port
submodule
str
;
cfg = config.shb.mitmdump;
mitmdumpScript = pkgs.writers.writePython3Bin "mitmdump"
mitmdumpScript =
pkgs.writers.writePython3Bin "mitmdump"
{
libraries = let
libraries =
let
p = pkgs.python3Packages;
in [
in
[
p.systemd
p.mitmproxy
];
@ -81,8 +100,8 @@ let
proc.wait()
'';
logger = toString (pkgs.writers.writeText "loggerAddon.py"
''
logger = toString (
pkgs.writers.writeText "loggerAddon.py" ''
import logging
from collections.abc import Sequence
from mitmproxy import ctx, http
@ -131,13 +150,14 @@ let
addons = [RegexLogger()]
'');
''
);
in
{
options.shb.mitmdump = {
addons = mkOption {
type = attrsOf str;
default = [];
default = [ ];
description = ''
Addons available to the be added to the mitmdump instance.
@ -146,11 +166,14 @@ in
};
instances = mkOption {
default = {};
default = { };
description = "Mitmdump instance.";
type = attrsOf (submodule ({ name, ... }: {
type = attrsOf (
submodule (
{ name, ... }:
{
options = {
package = lib.mkPackageOption pkgs "mitmproxy" {};
package = lib.mkPackageOption pkgs "mitmproxy" { };
serviceName = mkOption {
type = str;
@ -201,7 +224,7 @@ in
after = mkOption {
type = listOf str;
default = [];
default = [ ];
description = ''
Systemd services that must be started before this mitmdump proxy instance.
@ -212,7 +235,7 @@ in
enabledAddons = mkOption {
type = listOf str;
default = [];
default = [ ];
description = ''
Addons to enable on this mitmdump instance.
'';
@ -221,7 +244,7 @@ in
extraArgs = mkOption {
type = listOf str;
default = [];
default = [ ];
description = ''
Extra arguments to pass to the mitmdump instance.
@ -230,12 +253,16 @@ in
example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]'';
};
};
}));
}
)
);
};
};
config = {
systemd.services = mapAttrs' (name: cfg': nameValuePair "mitmdump-${name}" {
systemd.services = mapAttrs' (
name: cfg':
nameValuePair "mitmdump-${name}" {
environment = {
"HOME" = "/var/lib/private/mitmdump-${name}";
"MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump";
@ -250,7 +277,8 @@ in
WorkingDirectory = "/var/lib/mitmdump-${name}";
StateDirectory = "mitmdump-${name}";
ExecStart = let
ExecStart =
let
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
in
@ -259,7 +287,8 @@ in
requires = cfg'.after;
after = cfg'.after;
wantedBy = [ "multi-user.target" ];
}) cfg.instances;
}
) cfg.instances;
shb.mitmdump.addons = {
inherit logger;

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.monitoring;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -53,7 +58,10 @@ in
};
lokiMajorVersion = lib.mkOption {
type = lib.types.enum [ 2 3 ];
type = lib.types.enum [
2
3
];
description = ''
Switching from version 2 to 3 requires manual intervention
https://grafana.com/docs/loki/latest/setup/upgrade/#main--unreleased. So this let's the user
@ -84,7 +92,7 @@ in
contactPoints = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of email addresses to send alerts to";
default = [];
default = [ ];
};
adminPassword = lib.mkOption {
@ -114,7 +122,8 @@ in
smtp = lib.mkOption {
description = "SMTP options.";
default = null;
type = lib.types.nullOr (lib.types.submodule {
type = lib.types.nullOr (
lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
@ -144,7 +153,8 @@ in
description = "File containing the password to connect to the SMTP host.";
};
};
});
}
);
};
};
@ -204,12 +214,14 @@ in
services.grafana.provision = {
dashboards.settings = lib.mkIf cfg.provisionDashboards {
apiVersion = 1;
providers = [{
providers = [
{
folder = "Self Host Blocks";
options.path = ./monitoring/dashboards;
allowUiUpdates = true;
disableDeletion = true;
}];
}
];
};
datasources.settings = {
apiVersion = 1;
@ -245,26 +257,35 @@ in
};
alerting.contactPoints.settings = {
apiVersion = 1;
contactPoints = [{
contactPoints = [
{
inherit (cfg) orgId;
name = "grafana-default-email";
receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [{
receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [
{
uid = "sysadmin";
type = "email";
settings.addresses = lib.concatStringsSep ";" cfg.contactPoints;
}];
}];
}
];
}
];
};
alerting.policies.settings = {
apiVersion = 1;
policies = [{
policies = [
{
inherit (cfg) orgId;
receiver = "grafana-default-email";
group_by = [ "grafana_folder" "alertname" ];
group_by = [
"grafana_folder"
"alertname"
];
group_wait = "30s";
group_interval = "5m";
repeat_interval = "4h";
}];
}
];
# resetPolicies seems to happen after setting the above policies, effectively rolling back
# any updates.
};
@ -275,13 +296,15 @@ in
in
{
apiVersion = 1;
groups = [{
groups = [
{
inherit (cfg) orgId;
name = "SysAdmin";
folder = "Self Host Blocks";
interval = "10m";
inherit rules;
}];
}
];
# deleteRules seems to happen after creating the above rules, effectively rolling back
# any updates.
};
@ -295,9 +318,13 @@ in
services.loki = {
enable = true;
dataDir = "/var/lib/loki";
package = if cfg.lokiMajorVersion == 3 then pkgs.grafana-loki else
package =
if cfg.lokiMajorVersion == 3 then
pkgs.grafana-loki
else
# Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4
(pkgs.grafana-loki.overrideAttrs (finalAttrs: previousAttrs: rec {
(pkgs.grafana-loki.overrideAttrs (
finalAttrs: previousAttrs: rec {
version = "2.9.6";
src = pkgs.fetchFromGitHub {
@ -315,7 +342,11 @@ in
# Removes "cmd/lokitool"
];
ldflags = let t = "github.com/grafana/loki/pkg/util/build"; in [
ldflags =
let
t = "github.com/grafana/loki/pkg/util/build";
in
[
"-s"
"-w"
"-X ${t}.Version=${version}"
@ -324,7 +355,8 @@ in
"-X ${t}.Branch=unknown"
"-X ${t}.Revision=unknown"
];
}));
}
));
configuration = {
auth_enabled = false;
@ -448,41 +480,52 @@ in
services.prometheus.scrapeConfigs = [
{
job_name = "node";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.node.port}"];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ];
labels = commonLabels;
}];
}
];
}
{
job_name = "netdata";
metrics_path = "/api/v1/allmetrics";
params.format = [ "prometheus" ];
honor_labels = true;
static_configs = [{
static_configs = [
{
targets = [ "127.0.0.1:19999" ];
labels = commonLabels;
}];
}
];
}
{
job_name = "smartctl";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}"];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}" ];
labels = commonLabels;
}];
}
];
}
{
job_name = "prometheus_internal";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.port}"];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.port}" ];
labels = commonLabels;
}];
}
] ++ (lib.lists.optional config.services.nginx.enable {
];
}
]
++ (lib.lists.optional config.services.nginx.enable {
job_name = "nginx";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}"];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}" ];
labels = commonLabels;
}];
}
];
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) {
# job_name = "redis";
# static_configs = [
@ -497,12 +540,15 @@ in
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"];
# }
# ];
}) ++ (lib.optional config.services.dnsmasq.enable {
})
++ (lib.optional config.services.dnsmasq.enable {
job_name = "dnsmasq";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}"];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}" ];
labels = commonLabels;
}];
}
];
});
services.prometheus.exporters.nginx = lib.mkIf config.services.nginx.enable {
enable = true;
@ -513,7 +559,7 @@ in
services.prometheus.exporters.node = {
enable = true;
# https://github.com/prometheus/node_exporter#collectors
enabledCollectors = ["ethtool"];
enabledCollectors = [ "ethtool" ];
port = 9112;
listenAddress = "127.0.0.1";
};

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.nginx;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = c: "${c.subdomain}.${c.domain}";
@ -42,9 +47,10 @@ let
autheliaRules = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
default = [];
default = [ ];
description = "Authelia rule configuration";
example = lib.literalExpression ''[{
example = lib.literalExpression ''
[{
policy = "two_factor";
subject = ["group:service_user"];
}]'';
@ -77,12 +83,15 @@ in
vhosts = lib.mkOption {
description = "Endpoints to be protected by authelia.";
type = lib.types.listOf vhostConfig;
default = [];
default = [ ];
};
};
config = {
networking.firewall.allowedTCPPorts = [ 80 443 ];
networking.firewall.allowedTCPPorts = [
80
443
];
services.nginx.enable = true;
services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn";

View file

@ -1,9 +1,15 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.postgresql;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
upgrade-script = old: new:
upgrade-script =
old: new:
let
oldStr = builtins.toString old;
newStr = builtins.toString new;
@ -55,7 +61,7 @@ in
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.databasebackup.mkRequester {
user = "postgres";
@ -75,7 +81,8 @@ in
ensures = lib.mkOption {
description = "List of username, database and/or passwords that should be created.";
type = lib.types.listOf (lib.types.submodule {
type = lib.types.listOf (
lib.types.submodule {
options = {
username = lib.mkOption {
type = lib.types.str;
@ -94,8 +101,9 @@ in
example = "/run/secrets/postgresql/password";
};
};
});
default = [];
}
);
default = [ ];
};
};
@ -123,16 +131,19 @@ in
dbConfig = ensureCfgs: {
services.postgresql.enable = lib.mkDefault ((builtins.length ensureCfgs) > 0);
services.postgresql.ensureDatabases = map ({ database, ... }: database) ensureCfgs;
services.postgresql.ensureUsers = map ({ username, database, ... }: {
services.postgresql.ensureUsers = map (
{ username, database, ... }:
{
name = username;
ensureDBOwnership = true;
ensureClauses.login = true;
}) ensureCfgs;
}
) ensureCfgs;
};
pwdConfig = ensureCfgs: {
systemd.services.postgresql-setup.script = lib.mkAfter
(let
systemd.services.postgresql-setup.script = lib.mkAfter (
let
prefix = ''
psql -tA <<'EOF'
DO $$
@ -143,17 +154,24 @@ in
END $$;
EOF
'';
exec = { username, passwordFile, ... }: ''
exec =
{ username, passwordFile, ... }:
''
password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', '''));
EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password);
'';
cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs;
in
if (builtins.length cfgsWithPasswords) == 0 then "" else
prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix);
if (builtins.length cfgsWithPasswords) == 0 then
""
else
prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix
);
};
debugConfig = enableDebug: lib.mkIf enableDebug {
debugConfig =
enableDebug:
lib.mkIf enableDebug {
services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements";
};
in

View file

@ -1,15 +1,55 @@
{ config, pkgs, lib, utils, ... }:
{
config,
pkgs,
lib,
utils,
...
}:
let
cfg = config.shb.restic;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
inherit (lib) concatStringsSep filterAttrs flatten literalExpression optionals listToAttrs mapAttrsToList mkEnableOption mkOption mkMerge;
inherit (lib) hasPrefix mkIf nameValuePair optionalAttrs removePrefix;
inherit (lib.types) attrsOf enum int ints oneOf nonEmptyStr nullOr str submodule;
inherit (lib)
concatStringsSep
filterAttrs
flatten
literalExpression
optionals
listToAttrs
mapAttrsToList
mkEnableOption
mkOption
mkMerge
;
inherit (lib)
hasPrefix
mkIf
nameValuePair
optionalAttrs
removePrefix
;
inherit (lib.types)
attrsOf
enum
int
ints
oneOf
nonEmptyStr
nullOr
str
submodule
;
commonOptions = { name, prefix, config, ... }: {
commonOptions =
{
name,
prefix,
config,
...
}:
{
enable = mkEnableOption ''
this backup intance.
@ -41,7 +81,7 @@ let
secrets = mkOption {
type = attrsOf lib.shb.secretFileType;
default = {};
default = { };
description = ''
Secrets needed to access the repository where the backups will be stored.
@ -76,7 +116,10 @@ let
retention = mkOption {
description = "For how long to keep backup files.";
type = attrsOf (oneOf [ int nonEmptyStr ]);
type = attrsOf (oneOf [
int
nonEmptyStr
]);
default = {
keep_within = "1d";
keep_hourly = 24;
@ -101,15 +144,18 @@ let
};
};
repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (removePrefix "/" name);
repoSlugName = name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (removePrefix "/" name);
fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}";
in
{
options.shb.restic = {
instances = mkOption {
description = "Files to backup following the [backup contract](./contracts-backup.html).";
default = {};
type = attrsOf (submodule ({ name, config, ... }: {
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = contracts.backup.mkProvider {
settings = mkOption {
description = ''
@ -117,7 +163,10 @@ in
'';
type = submodule {
options = commonOptions { inherit name config; prefix = "instances"; };
options = commonOptions {
inherit name config;
prefix = "instances";
};
};
};
@ -129,13 +178,18 @@ in
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}));
}
)
);
};
databases = mkOption {
description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html).";
default = {};
type = attrsOf (submodule ({ name, config, ... }: {
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = contracts.databasebackup.mkProvider {
settings = mkOption {
description = ''
@ -143,7 +197,10 @@ in
'';
type = submodule {
options = commonOptions { inherit name config; prefix = "databases"; };
options = commonOptions {
inherit name config;
prefix = "databases";
};
};
};
@ -155,13 +212,15 @@ in
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}));
}
)
);
};
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
performance = mkOption {
description = "Reduce performance impact of backup jobs.";
default = {};
default = { };
type = submodule {
options = {
niceness = mkOption {
@ -170,7 +229,11 @@ in
default = 15;
};
ioSchedulingClass = mkOption {
type = enum [ "idle" "best-effort" "realtime" ];
type = enum [
"idle"
"best-effort"
"realtime"
];
description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
default = "best-effort";
};
@ -184,19 +247,24 @@ in
};
};
config = mkIf (cfg.instances != {} || cfg.databases != {}) (
config = mkIf (cfg.instances != { } || cfg.databases != { }) (
let
enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances;
enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases;
in mkMerge [
in
mkMerge [
{
environment.systemPackages = optionals (enabledInstances != {} || enabledDatabases != {}) [ pkgs.restic ];
environment.systemPackages = optionals (enabledInstances != { } || enabledDatabases != { }) [
pkgs.restic
];
}
{
# Create repository if it is a local path.
systemd.tmpfiles.rules =
let
mkSettings = name: instance: optionals (hasPrefix "/" instance.settings.repository.path) [
mkSettings =
name: instance:
optionals (hasPrefix "/" instance.settings.repository.path) [
"d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -"
];
in
@ -219,8 +287,8 @@ in
inherit (instance.settings.repository) timerConfig;
pruneOpts = mapAttrsToList (name: value:
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
pruneOpts = mapAttrsToList (
name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}"
) instance.settings.retention;
backupPrepareCommand = concatStringsSep "\n" instance.request.hooks.beforeBackup;
@ -234,7 +302,8 @@ in
++ (optionals (instance.settings.limitDownloadKiBs != null) [
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
]);
} // optionalAttrs (builtins.length instance.request.excludePatterns > 0) {
}
// optionalAttrs (builtins.length instance.request.excludePatterns > 0) {
exclude = instance.request.excludePatterns;
};
};
@ -258,8 +327,8 @@ in
inherit (instance.settings.repository) timerConfig;
pruneOpts = mapAttrsToList (name: value:
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
pruneOpts = mapAttrsToList (
name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}"
) instance.settings.retention;
extraBackupArgs =
@ -269,13 +338,14 @@ in
++ (optionals (instance.settings.limitDownloadKiBs != null) [
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
])
++
(let
++ (
let
cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd;
in
[
"--stdin-filename ${instance.request.backupName} --stdin-from-command -- ${cmd}/bin/dump.sh"
]);
]
);
};
};
in
@ -284,7 +354,8 @@ in
{
systemd.services =
let
mkSettings = name: instance:
mkSettings =
name: instance:
let
serviceName = fullName name instance.settings.repository;
in
@ -298,8 +369,7 @@ in
# BindReadOnlyPaths = instance.sourceDirectories;
};
}
(optionalAttrs (instance.settings.repository.secrets != {})
{
(optionalAttrs (instance.settings.repository.secrets != { }) {
serviceConfig.EnvironmentFile = [
"/run/secrets_restic/${serviceName}"
];
@ -308,8 +378,8 @@ in
})
];
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != {})
(let
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != { }) (
let
script = lib.shb.genConfigOutOfBandSystemd {
config = instance.settings.repository.secrets;
configLocation = "/run/secrets_restic/${serviceName}";
@ -321,19 +391,23 @@ in
script = script.preStart;
serviceConfig.Type = "oneshot";
serviceConfig.LoadCredential = script.loadCredentials;
});
}
);
};
in
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
}
{
systemd.services = let
mkEnv = name: instance:
systemd.services =
let
mkEnv =
name: instance:
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
enable = true;
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = (lib.shb.replaceSecrets {
script = (
lib.shb.replaceSecrets {
userConfig = instance.settings.repository.secrets // {
RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path;
RESTIC_REPOSITORY = instance.settings.repository.path;
@ -341,14 +415,17 @@ in
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
generator = lib.shb.toEnvVar;
user = instance.request.user;
});
}
);
};
in
listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
}
{
environment.systemPackages = let
mkResticBinary = name: instance:
environment.systemPackages =
let
mkResticBinary =
name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
@ -366,8 +443,10 @@ in
flatten (mapAttrsToList mkResticBinary cfg.instances);
}
{
environment.systemPackages = let
mkResticBinary = name: instance:
environment.systemPackages =
let
mkResticBinary =
name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
@ -384,5 +463,6 @@ in
in
flatten (mapAttrsToList mkResticBinary cfg.databases);
}
]);
]
);
}

View file

@ -1,9 +1,14 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mapAttrs mkOption;
inherit (lib.types) attrsOf anything submodule;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
cfg = config.shb.sops;
in
@ -11,8 +16,11 @@ in
options.shb.sops = {
secret = mkOption {
description = "Secret following the [secret contract](./contracts-secret.html).";
default = {};
type = attrsOf (submodule ({ name, options, ... }: {
default = { };
type = attrsOf (
submodule (
{ name, options, ... }:
{
options = contracts.secret.mkProvider {
settings = mkOption {
description = ''
@ -25,7 +33,7 @@ in
'';
type = attrsOf anything;
default = {};
default = { };
};
resultCfg = {
@ -33,13 +41,17 @@ in
pathText = "/run/secrets/<name>";
};
};
}));
}
)
);
};
};
config = {
sops.secrets = let
sops.secrets =
let
mkSecret = n: secretCfg: secretCfg.request // secretCfg.settings;
in mapAttrs mkSecret cfg.secret;
in
mapAttrs mkSecret cfg.secret;
};
}

View file

@ -1,12 +1,23 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.certs;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
inherit (builtins) dirOf;
inherit (lib) flatten mapAttrsToList optionalAttrs optionals unique;
inherit (lib)
flatten
mapAttrsToList
optionalAttrs
optionals
unique
;
in
{
options.shb.certs = {
@ -19,8 +30,11 @@ in
};
cas.selfsigned = lib.mkOption {
description = "Generate a self-signed Certificate Authority.";
default = {};
type = lib.types.attrsOf (lib.types.submodule ({ config, ...}: {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
@ -54,12 +68,17 @@ in
default = "shb-certs-ca-${config._module.args.name}.service";
};
};
}));
}
)
);
};
certs.selfsigned = lib.mkOption {
description = "Generate self-signed certificates signed by a Certificate Authority.";
default = {};
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options = {
ca = lib.mkOption {
type = lib.types.nullOr contracts.ssl.cas;
@ -85,7 +104,7 @@ in
description = ''
Other domains to generate a certificate for.
'';
default = [];
default = [ ];
example = lib.literalExpression ''
[
"sub1.example.com"
@ -131,17 +150,22 @@ in
The list of systemd services to call `systemctl try-reload-or-restart` on.
'';
type = lib.types.listOf lib.types.str;
default = [];
default = [ ];
example = [ "nginx.service" ];
};
};
}));
}
)
);
};
certs.letsencrypt = lib.mkOption {
description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/).";
default = {};
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options = {
domain = lib.mkOption {
type = lib.types.str;
@ -157,7 +181,7 @@ in
description = ''
Other domains to generate a certificate for.
'';
default = [];
default = [ ];
example = lib.literalExpression ''
[
"sub1.example.com"
@ -203,7 +227,7 @@ in
Systemd service(s) that must start successfully before attempting to reach acme.
'';
type = lib.types.listOf lib.types.str;
default = [];
default = [ ];
example = lib.literalExpression ''
[ "dnsmasq.service" ]
'';
@ -214,7 +238,7 @@ in
The list of systemd services to call `systemctl try-reload-or-restart` on.
'';
type = lib.types.listOf lib.types.str;
default = [];
default = [ ];
example = [ "nginx.service" ];
};
@ -258,7 +282,7 @@ in
additionalEnvironment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = {};
default = { };
description = ''
Additional environment variables used to configure the DNS provider.
@ -300,7 +324,9 @@ in
default = false;
};
};
}));
}
)
);
};
};
@ -311,7 +337,8 @@ in
lib.mkMerge [
# Config for self-signed CA.
{
systemd.services = lib.mapAttrs' (_name: caCfg:
systemd.services = lib.mapAttrs' (
_name: caCfg:
lib.nameValuePair (serviceName caCfg.systemdService) {
wantedBy = [ "multi-user.target" ];
wants = [ config.shb.certs.systemdService ];
@ -352,7 +379,8 @@ in
}
# Config for self-signed CA bundle.
{
systemd.services.${serviceName config.shb.certs.systemdService} = (lib.mkIf (cfg.cas.selfsigned != {}) {
systemd.services.${serviceName config.shb.certs.systemdService} = (
lib.mkIf (cfg.cas.selfsigned != { }) {
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = ''
@ -363,16 +391,20 @@ in
cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-bundle.crt
cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-certificates.crt
for file in ${lib.concatStringsSep " " (mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned)}; do
for file in ${
lib.concatStringsSep " " (mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned)
}; do
cat "$file" >> /etc/ssl/certs/ca-bundle.crt
cat "$file" >> /etc/ssl/certs/ca-certificates.crt
done
'';
});
}
);
}
# Config for self-signed cert.
{
systemd.services = lib.mapAttrs' (_name: certCfg:
systemd.services = lib.mapAttrs' (
_name: certCfg:
lib.nameValuePair (serviceName certCfg.systemdService) {
after = [ certCfg.ca.systemdService ];
requires = [ certCfg.ca.systemdService ];
@ -382,8 +414,7 @@ in
script =
let
extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains);
chmod = cert:
''
chmod = cert: ''
chown root:${certCfg.group} ${cert}
chmod 640 ${cert}
'';
@ -421,7 +452,7 @@ in
${chmod certCfg.paths.cert}
'';
postStart = lib.optionalString (certCfg.reloadServices != []) ''
postStart = lib.optionalString (certCfg.reloadServices != [ ]) ''
systemctl --no-block try-reload-or-restart ${lib.escapeShellArgs certCfg.reloadServices}
'';
@ -432,89 +463,140 @@ in
}
# Config for Let's Encrypt cert.
{
users.users = lib.mkMerge (mapAttrsToList (name: certCfg: {
users.users = lib.mkMerge (
mapAttrsToList (name: certCfg: {
${certCfg.makeAvailableToUser}.extraGroups = lib.mkIf (!(isNull certCfg.makeAvailableToUser)) [
config.security.acme.defaults.group
];
}) cfg.certs.letsencrypt);
}) cfg.certs.letsencrypt
);
security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != {}) true;
security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != { }) true;
security.acme.certs = let
extraDomainsCfg = certCfg: map (name: {
security.acme.certs =
let
extraDomainsCfg =
certCfg:
map (name: {
"${name}" = {
email = certCfg.adminEmail;
enableDebugLogs = certCfg.debug;
server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
};
}) certCfg.extraDomains;
in lib.mkMerge (flatten (mapAttrsToList (name: certCfg:
[{
in
lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
[
{
"${name}" = {
extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains;
email = certCfg.adminEmail;
enableDebugLogs = certCfg.debug;
server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
} // lib.optionalAttrs (certCfg.dnsProvider != null) {
}
// lib.optionalAttrs (certCfg.dnsProvider != null) {
inherit (certCfg) dnsProvider dnsResolver;
inherit (certCfg) group reloadServices;
credentialsFile = certCfg.credentialsFile;
};
}]
}
]
++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg)
) cfg.certs.letsencrypt));
) cfg.certs.letsencrypt
)
);
services.nginx = let
extraDomainsCfg = extraDomains: map (name: {
services.nginx =
let
extraDomainsCfg =
extraDomains:
map (name: {
virtualHosts."${name}" = {
# addSSL = true;
enableACME = true;
};
}) extraDomains;
in lib.mkMerge (flatten (mapAttrsToList (name: certCfg:
in
lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
lib.optionals (certCfg.dnsProvider == null) (
[{
[
{
virtualHosts."${name}" = {
# addSSL = true;
enableACME = true;
};
}]
}
]
++ extraDomainsCfg certCfg.extraDomains
)) cfg.certs.letsencrypt));
)
) cfg.certs.letsencrypt
)
);
systemd.services = let
extraDomainsCfg = certCfg: flatten (map (name:
lib.optionals (certCfg.additionalEnvironment != {} && certCfg.dnsProvider == null) [{
systemd.services =
let
extraDomainsCfg =
certCfg:
flatten (
map (
name:
lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [
{
"acme-${name}".environment = certCfg.additionalEnvironment;
}]
++ lib.optionals (certCfg.afterAndWants != [] && certCfg.dnsProvider == null) [{
}
]
++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [
{
"acme-${name}" = {
after = certCfg.afterAndWants;
wants = certCfg.afterAndWants;
};
}]
) certCfg.extraDomains);
in lib.mkMerge (flatten (mapAttrsToList (name: certCfg:
lib.optionals (certCfg.additionalEnvironment != {} && certCfg.dnsProvider == null) [{
}
]
) certCfg.extraDomains
);
in
lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [
{
"acme-${certCfg.domain}".environment = certCfg.additionalEnvironment;
}]
++ lib.optionals (certCfg.afterAndWants != [] && certCfg.dnsProvider == null) [{
}
]
++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [
{
"acme-${certCfg.domain}" = {
after = certCfg.afterAndWants;
wants = certCfg.afterAndWants;
};
}]
}
]
++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg)
) cfg.certs.letsencrypt));
) cfg.certs.letsencrypt
)
);
services.prometheus.exporters.node-cert = optionalAttrs (cfg.certs.letsencrypt != {}) {
services.prometheus.exporters.node-cert = optionalAttrs (cfg.certs.letsencrypt != { }) {
enable = true;
listenAddress = "127.0.0.1";
user = "acme";
paths = let
pathCfg = name: certCfg:
paths =
let
mainDomainPaths = map dirOf [ certCfg.paths.cert certCfg.paths.key ];
pathCfg =
name: certCfg:
let
mainDomainPaths = map dirOf [
certCfg.paths.cert
certCfg.paths.key
];
# Not sure this will work for all cases.
mainPath = dirOf (dirOf certCfg.paths.cert);
extraDomainsPath = map (x: "${mainPath}/${x}") certCfg.extraDomains;
@ -524,19 +606,24 @@ in
unique (flatten (mapAttrsToList pathCfg cfg.certs.letsencrypt));
};
services.prometheus.scrapeConfigs = let
scrapeCfg = name: certCfg: [{
services.prometheus.scrapeConfigs =
let
scrapeCfg = name: certCfg: [
{
job_name = "node-cert-${name}";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.node-cert.port}"];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node-cert.port}" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = certCfg.domain;
};
}];
}];
}
];
}
];
in
optionals (cfg.certs.letsencrypt != {}) (flatten (mapAttrsToList scrapeCfg cfg.certs.letsencrypt));
optionals (cfg.certs.letsencrypt != { }) (flatten (mapAttrsToList scrapeCfg cfg.certs.letsencrypt));
}
];
}

View file

@ -1,28 +1,41 @@
# Inspired from https://github.com/NixOS/nixpkgs/pull/231152 but made it so we can have multiple instances.
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.shb.tinyproxy;
mkValueStringTinyproxy = with lib; v:
if true == v then "yes"
else if false == v then "no"
else generators.mkValueStringDefault {} v;
mkValueStringTinyproxy =
with lib;
v:
if true == v then
"yes"
else if false == v then
"no"
else
generators.mkValueStringDefault { } v;
mkKeyValueTinyproxy = {
mkValueString ? mkValueStringDefault {}
}: sep: k: v:
if null == v then ""
else "${lib.strings.escape [sep] k}${sep}${mkValueString v}";
mkKeyValueTinyproxy =
{
mkValueString ? mkValueStringDefault { },
}:
sep: k: v:
if null == v then "" else "${lib.strings.escape [ sep ] k}${sep}${mkValueString v}";
settingsFormat = (pkgs.formats.keyValue {
settingsFormat = (
pkgs.formats.keyValue {
mkKeyValue = mkKeyValueTinyproxy {
mkValueString = mkValueStringTinyproxy;
} " ";
listsAsDuplicateKeys= true;
});
listsAsDuplicateKeys = true;
}
);
configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings;
in
@ -33,7 +46,7 @@ in
options = {
enable = mkEnableOption "Tinyproxy daemon";
package = mkPackageOption pkgs "tinyproxy" {};
package = mkPackageOption pkgs "tinyproxy" { };
dynamicBindFile = mkOption {
description = ''
@ -47,7 +60,8 @@ in
Configuration for [tinyproxy](https://tinyproxy.github.io/).
'';
default = { };
example = literalExpression ''{
example = literalExpression ''
{
Port 8888;
Listen 127.0.0.1;
Timeout 600;
@ -55,7 +69,9 @@ in
Anonymous = ['"Host"' '"Authorization"'];
ReversePath = '"/example/" "http://www.example.com/"';
}'';
type = types.submodule ({name, ...}: {
type = types.submodule (
{ name, ... }:
{
freeformType = settingsFormat.type;
options = {
Listen = mkOption {
@ -74,7 +90,7 @@ in
};
Anonymous = mkOption {
type = types.listOf types.str;
default = [];
default = [ ];
description = ''
If an `Anonymous` keyword is present, then anonymous proxying is enabled. The
headers listed with `Anonymous` are allowed through, while all others are denied.
@ -91,7 +107,8 @@ in
'';
};
};
});
}
);
};
};
};
@ -99,7 +116,7 @@ in
{
shb.tinyproxy = mkOption {
description = "Tinyproxy instances.";
default = {};
default = { };
type = types.attrsOf instanceOption;
};
};
@ -107,7 +124,9 @@ in
config = {
systemd.services =
let
instanceConfig = name: c: mkIf c.enable {
instanceConfig =
name: c:
mkIf c.enable {
"tinyproxy-${name}" = {
description = "TinyProxy daemon - instance ${name}";
after = [ "network.target" ];
@ -126,12 +145,15 @@ in
RestartMaxDelaySec = "10s";
ConfigurationDirectory = "tinyproxy";
};
preStart = concatStringsSep "\n" ([
preStart = concatStringsSep "\n" (
[
"cat ${configFile name c} > /etc/tinyproxy/${name}.conf"
] ++ optionals (c.dynamicBindFile != "") [
]
++ optionals (c.dynamicBindFile != "") [
"echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf"
"cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf"
]);
]
);
};
};
in
@ -141,7 +163,7 @@ in
group = "tinyproxy";
isSystemUser = true;
};
users.groups.tinyproxy = {};
users.groups.tinyproxy = { };
};
meta.maintainers = with maintainers; [ tcheronneau ];

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.vpn;
@ -6,12 +11,14 @@ let
quoteEach = lib.concatMapStrings (x: ''"${x}"'');
nordvpnConfig =
{ name
, dev
, authFile
, remoteServerIP
, dependentServices ? []
}: ''
{
name,
dev,
authFile,
remoteServerIP,
dependentServices ? [ ],
}:
''
client
dev ${dev}
proto tcp
@ -103,10 +110,16 @@ let
</tls-auth>
'';
routeUp = name: dependentServices: pkgs.writeShellApplication {
routeUp =
name: dependentServices:
pkgs.writeShellApplication {
name = "routeUp.sh";
runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools ];
runtimeInputs = [
pkgs.iproute2
pkgs.systemd
pkgs.nettools
];
text = ''
echo "Running route-up..."
@ -150,10 +163,17 @@ let
'';
};
routeDown = name: dependentServices: pkgs.writeShellApplication {
routeDown =
name: dependentServices:
pkgs.writeShellApplication {
name = "routeDown.sh";
runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools pkgs.coreutils ];
runtimeInputs = [
pkgs.iproute2
pkgs.systemd
pkgs.nettools
pkgs.coreutils
];
text = ''
echo "Running route-down..."
@ -205,7 +225,7 @@ in
options = {
enable = lib.mkEnableOption "OpenVPN config";
package = lib.mkPackageOption pkgs "openvpn" {};
package = lib.mkPackageOption pkgs "openvpn" { };
provider = lib.mkOption {
description = "VPN provider, if given uses ready-made configuration.";
@ -246,7 +266,7 @@ in
{
shb.vpn = lib.mkOption {
description = "OpenVPN instances.";
default = {};
default = { };
type = lib.types.attrsOf instanceOption;
};
};
@ -254,7 +274,9 @@ in
config = {
services.openvpn.servers =
let
instanceConfig = name: c: lib.mkIf c.enable {
instanceConfig =
name: c:
lib.mkIf c.enable {
${name} = {
autoStart = true;
@ -270,17 +292,20 @@ in
in
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
systemd.tmpfiles.rules = map (name:
"d /tmp/openvpn/${name}.status 0700 root root"
) (lib.attrNames cfg);
systemd.tmpfiles.rules = map (name: "d /tmp/openvpn/${name}.status 0700 root root") (
lib.attrNames cfg
);
networking.iproute2.enable = true;
networking.iproute2.rttablesExtraConfig =
lib.concatStringsSep "\n" (lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg);
networking.iproute2.rttablesExtraConfig = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg
);
shb.tinyproxy =
let
instanceConfig = name: c: lib.mkIf (c.enable && c.proxyPort != null) {
instanceConfig =
name: c:
lib.mkIf (c.enable && c.proxyPort != null) {
${name} = {
enable = true;
# package = pkgs.tinyproxy.overrideAttrs (old: {
@ -299,7 +324,10 @@ in
Listen = "127.0.0.1";
Syslog = "On";
LogLevel = "Info";
Allow = [ "127.0.0.1" "::1" ];
Allow = [
"127.0.0.1"
"::1"
];
ViaProxyName = ''"tinyproxy"'';
};
};

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.zfs;
@ -21,11 +26,12 @@ in
This block implements the following contracts:
- mount
'';
default = {};
default = { };
example = lib.literalExpression ''
shb.zfs."safe/postgresql".path = "/var/lib/postgresql";
'';
type = lib.types.attrsOf (lib.types.submodule {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
enable = lib.mkEnableOption "shb.zfs.datasets";
@ -40,19 +46,23 @@ in
description = "Path this dataset should be mounted on.";
};
};
});
}
);
};
};
config = {
assertions = [
{
assertion = lib.any (x: x.poolName == null) (lib.mapAttrsToList (n: v: v) cfg.datasets) -> cfg.defaultPoolName != null;
assertion =
lib.any (x: x.poolName == null) (lib.mapAttrsToList (n: v: v) cfg.datasets)
-> cfg.defaultPoolName != null;
message = "Cannot have both datasets.poolName and defaultPoolName set to null";
}
];
system.activationScripts = lib.mapAttrs' (name: cfg':
system.activationScripts = lib.mapAttrs' (
name: cfg':
let
dataset = (if cfg'.poolName != null then cfg'.poolName else cfg.defaultPoolName) + "/" + name;
in
@ -68,6 +78,7 @@ in
mountpoint=${cfg'.path} \
${dataset}
'';
}) cfg.datasets;
}
) cfg.datasets;
};
}

View file

@ -1,22 +1,35 @@
{ lib, ... }:
let
inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString;
inherit (lib.types) listOf nonEmptyListOf submodule str;
inherit (lib)
concatStringsSep
literalMD
mkOption
optionalAttrs
optionalString
;
inherit (lib.types)
listOf
nonEmptyListOf
submodule
str
;
inherit (lib.shb) anyNotNull;
in
{
mkRequest =
{ user ? "",
{
user ? "",
userText ? null,
sourceDirectories ? [ "/var/lib/example" ],
sourceDirectoriesText ? null,
excludePatterns ? [],
excludePatterns ? [ ],
excludePatternsText ? null,
beforeBackup ? [],
beforeBackup ? [ ],
beforeBackupText ? null,
afterBackup ? [],
afterBackup ? [ ],
afterBackupText ? null,
}: mkOption {
}:
mkOption {
description = ''
Request part of the backup contract.
@ -31,70 +44,100 @@ in
};
};
defaultText = optionalString (anyNotNull [
defaultText =
optionalString
(anyNotNull [
userText
sourceDirectoriesText
excludePatternsText
beforeBackupText
afterBackupText
]) (literalMD ''
])
(literalMD ''
{
user = ${if userText != null then userText else user};
sourceDirectories = ${if sourceDirectoriesText != null then sourceDirectoriesText else "[ " + concatStringsSep " " sourceDirectories + " ]"};
excludePatterns = ${if excludePatternsText != null then excludePatternsText else "[ " + concatStringsSep " " excludePatterns + " ]"};
hooks.beforeBackup = ${if beforeBackupText != null then beforeBackupText else "[ " + concatStringsSep " " beforeBackup + " ]"};
hooks.afterBackup = ${if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]"};
sourceDirectories = ${
if sourceDirectoriesText != null then
sourceDirectoriesText
else
"[ " + concatStringsSep " " sourceDirectories + " ]"
};
excludePatterns = ${
if excludePatternsText != null then
excludePatternsText
else
"[ " + concatStringsSep " " excludePatterns + " ]"
};
hooks.beforeBackup = ${
if beforeBackupText != null then
beforeBackupText
else
"[ " + concatStringsSep " " beforeBackup + " ]"
};
hooks.afterBackup = ${
if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]"
};
};
'');
type = submodule {
options = {
user = mkOption {
user =
mkOption {
description = ''
Unix user doing the backups.
'';
type = str;
example = "vaultwarden";
default = user;
} // optionalAttrs (userText != null) {
}
// optionalAttrs (userText != null) {
defaultText = literalMD userText;
};
sourceDirectories = mkOption {
sourceDirectories =
mkOption {
description = "Directories to backup.";
type = nonEmptyListOf str;
example = "/var/lib/vaultwarden";
default = sourceDirectories;
} // optionalAttrs (sourceDirectoriesText != null) {
}
// optionalAttrs (sourceDirectoriesText != null) {
defaultText = literalMD sourceDirectoriesText;
};
excludePatterns = mkOption {
excludePatterns =
mkOption {
description = "File patterns to exclude.";
type = listOf str;
default = excludePatterns;
} // optionalAttrs (excludePatternsText != null) {
}
// optionalAttrs (excludePatternsText != null) {
defaultText = literalMD excludePatternsText;
};
hooks = mkOption {
description = "Hooks to run around the backup.";
default = {};
default = { };
type = submodule {
options = {
beforeBackup = mkOption {
beforeBackup =
mkOption {
description = "Hooks to run before backup.";
type = listOf str;
default = beforeBackup;
} // optionalAttrs (beforeBackupText != null) {
}
// optionalAttrs (beforeBackupText != null) {
defaultText = literalMD beforeBackupText;
};
afterBackup = mkOption {
afterBackup =
mkOption {
description = "Hooks to run after backup.";
type = listOf str;
default = afterBackup;
} // optionalAttrs (afterBackupText != null) {
}
// optionalAttrs (afterBackupText != null) {
defaultText = literalMD afterBackupText;
};
};
@ -104,12 +147,14 @@ in
};
};
mkResult = {
mkResult =
{
restoreScript ? "restore",
restoreScriptText ? null,
backupService ? "backup.service",
backupServiceText ? null,
}: mkOption {
}:
mkOption {
description = ''
Result part of the backup contract.
@ -119,10 +164,13 @@ in
inherit restoreScript backupService;
};
defaultText = optionalString (anyNotNull [
defaultText =
optionalString
(anyNotNull [
restoreScriptText
backupServiceText
]) (literalMD ''
])
(literalMD ''
{
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
backupService = ${if backupServiceText != null then backupServiceText else backupService};
@ -131,7 +179,8 @@ in
type = submodule {
options = {
restoreScript = mkOption {
restoreScript =
mkOption {
description = ''
Name of script that can restore the database.
One can then list snapshots with:
@ -148,11 +197,13 @@ in
'';
type = str;
default = restoreScript;
} // optionalAttrs (restoreScriptText != null) {
}
// optionalAttrs (restoreScriptText != null) {
defaultText = literalMD restoreScriptText;
};
backupService = mkOption {
backupService =
mkOption {
description = ''
Name of service backing up the database.
@ -164,7 +215,8 @@ in
'';
type = str;
default = backupService;
} // optionalAttrs (backupServiceText != null) {
}
// optionalAttrs (backupServiceText != null) {
defaultText = literalMD backupServiceText;
};
};

View file

@ -1,6 +1,6 @@
{ pkgs, lib, ... }:
let
contracts = pkgs.callPackage ../. {};
contracts = pkgs.callPackage ../. { };
inherit (lib) mkOption;
inherit (lib.types) submodule;

View file

@ -1,10 +1,17 @@
{ pkgs, lib }:
let
inherit (lib) concatMapStringsSep getAttrFromPath mkIf optionalAttrs setAttrByPath;
inherit (lib)
concatMapStringsSep
getAttrFromPath
mkIf
optionalAttrs
setAttrByPath
;
in
{ name,
{
name,
providerRoot,
modules ? [],
modules ? [ ],
username ? "me",
sourceDirectories ? [
"/opt/files/A"
@ -12,10 +19,13 @@ in
],
settings, # { repository, config } -> attrset
extraConfig ? null, # { username, config } -> attrset
}: lib.shb.runNixOSTest {
}:
lib.shb.runNixOSTest {
inherit name;
nodes.machine = { config, ... }: {
nodes.machine =
{ config, ... }:
{
imports = [ lib.shb.baseImports ] ++ modules;
config = lib.mkMerge [
@ -36,16 +46,21 @@ in
group = "root";
};
})
(optionalAttrs (extraConfig != null) (extraConfig { inherit username config; }))
(optionalAttrs (extraConfig != null) (extraConfig {
inherit username config;
}))
];
};
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
testScript = { nodes, ... }: let
testScript =
{ nodes, ... }:
let
provider = (getAttrFromPath providerRoot nodes.machine).result;
in ''
in
''
from dictdiffer import diff
username = "${username}"

View file

@ -1,12 +1,19 @@
{ lib, ... }:
let
inherit (lib) mkOption literalExpression literalMD optionalAttrs optionalString;
inherit (lib)
mkOption
literalExpression
literalMD
optionalAttrs
optionalString
;
inherit (lib.types) submodule str;
inherit (lib.shb) anyNotNull;
in
{
mkRequest =
{ user ? "root",
{
user ? "root",
userText ? null,
backupName ? "dump",
backupNameText ? null,
@ -14,7 +21,8 @@ in
backupCmdText ? null,
restoreCmd ? "",
restoreCmdText ? null,
}: mkOption {
}:
mkOption {
description = ''
Request part of the backup contract.
@ -23,15 +31,23 @@ in
'';
default = {
inherit user backupName backupCmd restoreCmd;
inherit
user
backupName
backupCmd
restoreCmd
;
};
defaultText = optionalString (anyNotNull [
defaultText =
optionalString
(anyNotNull [
userText
backupNameText
backupCmdText
restoreCmdText
]) (literalMD ''
])
(literalMD ''
{
user = ${if userText != null then userText else user};
backupName = ${if backupNameText != null then backupNameText else backupName};
@ -42,7 +58,8 @@ in
type = submodule {
options = {
user = mkOption {
user =
mkOption {
description = ''
Unix user doing the backups.
@ -51,51 +68,59 @@ in
type = str;
example = "postgres";
default = user;
} // optionalAttrs (userText != null) {
}
// optionalAttrs (userText != null) {
defaultText = literalMD userText;
};
backupName = mkOption {
backupName =
mkOption {
description = "Name of the backup in the repository.";
type = str;
example = "postgresql.sql";
default = backupName;
} // optionalAttrs (backupNameText != null) {
}
// optionalAttrs (backupNameText != null) {
defaultText = literalMD backupNameText;
};
backupCmd = mkOption {
backupCmd =
mkOption {
description = "Command that produces the database dump on stdout.";
type = str;
example = literalExpression ''
''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable
'';
default = backupCmd;
} // optionalAttrs (backupCmdText != null) {
}
// optionalAttrs (backupCmdText != null) {
defaultText = literalMD backupCmdText;
};
restoreCmd = mkOption {
restoreCmd =
mkOption {
description = "Command that reads the database dump on stdin and restores the database.";
type = str;
example = literalExpression ''
''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres
'';
default = restoreCmd;
} // optionalAttrs (restoreCmdText != null) {
}
// optionalAttrs (restoreCmdText != null) {
defaultText = literalMD restoreCmdText;
};
};
};
};
mkResult = {
mkResult =
{
restoreScript ? "restore",
restoreScriptText ? null,
backupService ? "backup.service",
backupServiceText ? null,
}: mkOption {
}:
mkOption {
description = ''
Result part of the backup contract.
@ -105,10 +130,13 @@ in
inherit restoreScript backupService;
};
defaultText = optionalString (anyNotNull [
defaultText =
optionalString
(anyNotNull [
restoreScriptText
backupServiceText
]) (literalMD ''
])
(literalMD ''
{
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
backupService = ${if backupServiceText != null then backupServiceText else backupService};
@ -117,7 +145,8 @@ in
type = submodule {
options = {
restoreScript = mkOption {
restoreScript =
mkOption {
description = ''
Name of script that can restore the database.
One can then list snapshots with:
@ -134,11 +163,13 @@ in
'';
type = str;
default = restoreScript;
} // optionalAttrs (restoreScriptText != null) {
}
// optionalAttrs (restoreScriptText != null) {
defaultText = literalMD restoreScriptText;
};
backupService = mkOption {
backupService =
mkOption {
description = ''
Name of service backing up the database.
@ -150,7 +181,8 @@ in
'';
type = str;
default = backupService;
} // optionalAttrs (backupServiceText != null) {
}
// optionalAttrs (backupServiceText != null) {
defaultText = literalMD backupServiceText;
};
};

View file

@ -1,6 +1,6 @@
{ pkgs, lib, ... }:
let
contracts = pkgs.callPackage ../. {};
contracts = pkgs.callPackage ../. { };
inherit (lib) mkOption;
inherit (lib.types) submodule;

View file

@ -1,18 +1,27 @@
{ pkgs, lib }:
let
inherit (lib) getAttrFromPath mkIf optionalAttrs setAttrByPath;
inherit (lib)
getAttrFromPath
mkIf
optionalAttrs
setAttrByPath
;
in
{ name,
{
name,
requesterRoot,
providerRoot,
extraConfig ? null, # { config, database } -> attrset
modules ? [],
modules ? [ ],
database ? "me",
settings, # { repository, config } -> attrset
}: lib.shb.runNixOSTest {
}:
lib.shb.runNixOSTest {
inherit name;
nodes.machine = { config, ... }: {
nodes.machine =
{ config, ... }:
{
imports = [ lib.shb.baseImports ] ++ modules;
config = lib.mkMerge [
(setAttrByPath providerRoot {
@ -29,13 +38,18 @@ in
group = "root";
};
})
(optionalAttrs (extraConfig != null) (extraConfig { inherit config database; }))
(optionalAttrs (extraConfig != null) (extraConfig {
inherit config database;
}))
];
};
testScript = { nodes, ... }: let
testScript =
{ nodes, ... }:
let
provider = getAttrFromPath providerRoot nodes.machine;
in ''
in
''
import csv
start_all()

View file

@ -4,28 +4,33 @@ let
inherit (lib.types) anything;
mkContractFunctions =
{ mkRequest,
{
mkRequest,
mkResult,
}: {
}:
{
mkRequester = requestCfg: {
request = mkRequest requestCfg;
result = mkResult {};
result = mkResult { };
};
mkProvider =
{ resultCfg,
settings ? {},
}: {
request = mkRequest {};
{
resultCfg,
settings ? { },
}:
{
request = mkRequest { };
result = mkResult resultCfg;
} // optionalAttrs (settings != {}) { inherit settings; };
}
// optionalAttrs (settings != { }) { inherit settings; };
contract = {
request = mkRequest {};
request = mkRequest { };
result = mkResult {};
result = mkResult { };
settings = mkOption {
description = ''
@ -36,9 +41,10 @@ let
};
};
importContract = module:
importContract =
module:
let
importedModule = pkgs.callPackage module {};
importedModule = pkgs.callPackage module { };
in
mkContractFunctions {
inherit (importedModule) mkRequest mkResult;
@ -47,12 +53,12 @@ in
{
databasebackup = importContract ./databasebackup.nix;
backup = importContract ./backup.nix;
mount = pkgs.callPackage ./mount.nix {};
mount = pkgs.callPackage ./mount.nix { };
secret = importContract ./secret.nix;
ssl = pkgs.callPackage ./ssl.nix {};
ssl = pkgs.callPackage ./ssl.nix { };
test = {
secret = pkgs.callPackage ./secret/test.nix {};
databasebackup = pkgs.callPackage ./databasebackup/test.nix {};
backup = pkgs.callPackage ./backup/test.nix {};
secret = pkgs.callPackage ./secret/test.nix { };
databasebackup = pkgs.callPackage ./databasebackup/test.nix { };
backup = pkgs.callPackage ./backup/test.nix { };
};
}

View file

@ -1,20 +1,28 @@
{ lib, ... }:
let
inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString;
inherit (lib)
concatStringsSep
literalMD
mkOption
optionalAttrs
optionalString
;
inherit (lib.types) listOf submodule str;
inherit (lib.shb) anyNotNull;
in
{
mkRequest =
{ mode ? "0400",
{
mode ? "0400",
modeText ? null,
owner ? "root",
ownerText ? null,
group ? "root",
groupText ? null,
restartUnits ? [],
restartUnits ? [ ],
restartUnitsText ? null,
}: mkOption {
}:
mkOption {
description = ''
Request part of the secret contract.
@ -23,64 +31,87 @@ in
'';
default = {
inherit mode owner group restartUnits;
inherit
mode
owner
group
restartUnits
;
};
defaultText = optionalString (anyNotNull [
defaultText =
optionalString
(anyNotNull [
modeText
ownerText
groupText
restartUnitsText
]) (literalMD ''
])
(literalMD ''
{
mode = ${if modeText != null then modeText else mode};
owner = ${if ownerText != null then ownerText else owner};
group = ${if groupText != null then groupText else group};
restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + concatStringsSep " " restartUnits + " ]"};
restartUnits = ${
if restartUnitsText != null then
restartUnitsText
else
"[ " + concatStringsSep " " restartUnits + " ]"
};
}
'');
type = submodule {
options = {
mode = mkOption {
mode =
mkOption {
description = ''
Mode of the secret file.
'';
type = str;
default = mode;
} // optionalAttrs (modeText != null) {
}
// optionalAttrs (modeText != null) {
defaultText = literalMD modeText;
};
owner = mkOption ({
owner = mkOption (
{
description = ''
Linux user owning the secret file.
'';
type = str;
default = owner;
} // optionalAttrs (ownerText != null) {
}
// optionalAttrs (ownerText != null) {
defaultText = literalMD ownerText;
});
}
);
group = mkOption {
group =
mkOption {
description = ''
Linux group owning the secret file.
'';
type = str;
default = group;
} // optionalAttrs (groupText != null) {
}
// optionalAttrs (groupText != null) {
defaultText = literalMD groupText;
};
restartUnits = mkOption ({
restartUnits = mkOption (
{
description = ''
Systemd units to restart after the secret is updated.
'';
type = listOf str;
default = restartUnits;
} // optionalAttrs (restartUnitsText != null) {
}
// optionalAttrs (restartUnitsText != null) {
defaultText = literalMD restartUnitsText;
});
}
);
};
};
};
@ -90,7 +121,8 @@ in
path ? "/run/secrets/secret",
pathText ? null,
}:
mkOption ({
mkOption (
{
description = ''
Result part of the secret contract.
@ -101,7 +133,8 @@ in
};
type = submodule {
options = {
path = mkOption {
path =
mkOption {
type = lib.types.path;
description = ''
Path to the file containing the secret generated out of band.
@ -110,14 +143,17 @@ in
it is not available through the nix store.
'';
default = path;
} // optionalAttrs (pathText != null) {
}
// optionalAttrs (pathText != null) {
defaultText = pathText;
};
};
};
} // optionalAttrs (pathText != null) {
}
// optionalAttrs (pathText != null) {
defaultText = {
path = pathText;
};
});
}
);
}

View file

@ -1,6 +1,6 @@
{ pkgs, lib, ... }:
let
contracts = pkgs.callPackage ../. {};
contracts = pkgs.callPackage ../. { };
inherit (lib) mkOption;
inherit (lib.types) submodule;

View file

@ -3,24 +3,33 @@ let
inherit (lib) getAttrFromPath setAttrByPath;
inherit (lib) mkIf;
in
{ name,
{
name,
configRoot,
settingsCfg, # str -> attrset
modules ? [],
modules ? [ ],
owner ? "root",
group ? "root",
mode ? "0400",
restartUnits ? [ "myunit.service" ],
}: lib.shb.runNixOSTest {
}:
lib.shb.runNixOSTest {
name = "secret_${name}_${owner}_${group}_${mode}";
nodes.machine = { config, ... }: {
nodes.machine =
{ config, ... }:
{
imports = [ lib.shb.baseImports ] ++ modules;
config = lib.mkMerge [
(setAttrByPath configRoot {
A = {
request = {
inherit owner group mode restartUnits;
inherit
owner
group
mode
restartUnits
;
};
settings = settingsCfg "secretA";
};
@ -29,12 +38,13 @@ in
users.users.${owner}.isNormalUser = true;
})
(mkIf (group != "root") {
users.groups.${group} = {};
users.groups.${group} = { };
})
];
};
testScript = { nodes, ... }:
testScript =
{ nodes, ... }:
let
result = (getAttrFromPath configRoot nodes.machine)."A".result;
in

View file

@ -1,6 +1,6 @@
{ pkgs, lib, ... }:
let
contracts = pkgs.callPackage ../. {};
contracts = pkgs.callPackage ../. { };
in
{
options.shb.contracts.ssl = lib.mkOption {

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.arr;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
apps = {
radarr = {
@ -11,7 +16,7 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for radarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.radarr.settingsFormat.type;
options = {
@ -20,7 +25,10 @@ let
description = "Path to api key secret file.";
};
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -69,7 +77,7 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for sonarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.sonarr.settingsFormat.type;
options = {
@ -78,7 +86,10 @@ let
description = "Path to api key secret file.";
};
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -122,12 +133,15 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for bazarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.bazarr.settingsFormat.type;
options = {
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -147,12 +161,15 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for readarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.readarr.settingsFormat.type;
options = {
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -171,12 +188,15 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for lidarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.lidarr.settingsFormat.type;
options = {
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -191,11 +211,11 @@ let
};
};
jackett = {
settingsFormat = pkgs.formats.json {};
settingsFormat = pkgs.formats.json { };
moreOptions = {
settings = lib.mkOption {
description = "Specific options for jackett.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.jackett.settingsFormat.type;
options = {
@ -214,7 +234,12 @@ let
default = null;
};
ProxyType = lib.mkOption {
type = lib.types.enum [ "-1" "0" "1" "2" ];
type = lib.types.enum [
"-1"
"0"
"1"
"2"
];
default = "-1";
description = ''
-1 = disabled
@ -256,8 +281,17 @@ let
};
};
vhosts = { extraBypassResources ? [] }: c: {
inherit (c) subdomain domain authEndpoint ssl;
vhosts =
{
extraBypassResources ? [ ],
}:
c: {
inherit (c)
subdomain
domain
authEndpoint
ssl
;
upstream = "http://127.0.0.1:${toString c.settings.Port}";
autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [
@ -272,14 +306,17 @@ let
{
domain = "${c.subdomain}.${c.domain}";
policy = "two_factor";
subject = ["group:arr_user"];
subject = [ "group:arr_user" ];
}
];
};
appOption = name: c: lib.nameValuePair name (lib.mkOption {
appOption =
name: c:
lib.nameValuePair name (
lib.mkOption {
description = "Configuration for ${name}";
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption name;
@ -319,20 +356,26 @@ let
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = name;
sourceDirectories = [
cfg.${name}.dataDir
];
excludePatterns = [".db-shm" ".db-wal" ".mono"];
excludePatterns = [
".db-shm"
".db-wal"
".mono"
];
};
};
};
} // (c.moreOptions or {});
}
// (c.moreOptions or { });
};
});
}
);
in
{
imports = [
@ -356,7 +399,8 @@ in
};
systemd.services.radarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings
userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
@ -365,8 +409,9 @@ in
generator = lib.shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.sonarr.enable (
let
@ -385,7 +430,8 @@ in
};
systemd.services.sonarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings
userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
@ -394,8 +440,9 @@ in
generator = apps.sonarr.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.bazarr.enable (
let
@ -411,7 +458,8 @@ in
extraGroups = [ "media" ];
};
systemd.services.bazarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings
userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
@ -420,8 +468,9 @@ in
generator = apps.bazarr.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.readarr.enable (
let
@ -441,8 +490,9 @@ in
generator = apps.readarr.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.lidarr.enable (
let
@ -458,7 +508,8 @@ in
extraGroups = [ "media" ];
};
systemd.services.lidarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings
userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
@ -467,8 +518,9 @@ in
generator = apps.lidarr.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.jackett.enable (
let
@ -489,9 +541,12 @@ in
generator = apps.jackett.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {
shb.nginx.vhosts = [
(vhosts {
extraBypassResources = [ "^/dl.*" ];
} cfg') ];
}))
} cfg')
];
}
))
];
}

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.audiobookshelf;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -40,7 +45,7 @@ in
extraServiceConfig = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra configuration given to the systemd service file.";
default = {};
default = { };
example = lib.literalExpression ''
{
MemoryHigh = "512M";
@ -51,7 +56,7 @@ in
sso = lib.mkOption {
description = "SSO configuration.";
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO";
@ -87,7 +92,10 @@ in
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -133,14 +141,24 @@ in
};
logLevel = lib.mkOption {
type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
type = lib.types.nullOr (
lib.types.enum [
"critical"
"error"
"warning"
"info"
"debug"
]
);
description = "Enable logging.";
default = false;
example = true;
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [{
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.audiobookshelf = {
enable = true;
@ -175,7 +193,6 @@ in
'';
};
shb.authelia.extraDefinitions = {
user_attributes.${roleClaim}.expression =
''"${cfg.sso.adminUserGroup}" in groups ? ["admin"] : ("${cfg.sso.userGroup}" in groups ? ["user"] : [""])'';
@ -183,7 +200,7 @@ in
shb.authelia.extraOidcClaimsPolicies.${roleClaim} = {
custom_claims = {
"${roleClaim}" = {};
"${roleClaim}" = { };
};
};
@ -203,14 +220,23 @@ in
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback"
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect"
];
scopes = [ "openid" "profile" "email" "groups" "${roleClaim}" ];
scopes = [
"openid"
"profile"
"email"
"groups"
"${roleClaim}"
];
require_pkce = true;
pkce_challenge_method = "S256";
userinfo_signed_response_alg = "none";
token_endpoint_auth_method = "client_secret_basic";
}
];
} {
}
{
systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig;
}]);
}
]
);
}

View file

@ -1,15 +1,26 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.deluge;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
authGenerator = users:
authGenerator =
users:
let
genLine = name: { password, priority ? 10 }:
genLine =
name:
{
password,
priority ? 10,
}:
"${name}:${password}:${toString priority}";
lines = lib.mapAttrsToList genLine users;
@ -57,7 +68,10 @@ in
daemonListenPorts = lib.mkOption {
type = lib.types.listOf lib.types.int;
description = "Deluge daemon listen ports";
default = [ 6881 6889 ];
default = [
6881
6889
];
};
webPort = lib.mkOption {
@ -158,7 +172,7 @@ in
extraServiceConfig = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra configuration given to the systemd service file.";
default = {};
default = { };
example = lib.literalExpression ''
{
MemoryHigh = "512M";
@ -176,14 +190,16 @@ in
extraUsers = lib.mkOption {
description = "Users having access to this deluge instance. Attrset of username to user options.";
type = lib.types.attrsOf (lib.types.submodule {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
password = lib.mkOption {
type = lib.shb.secretFileType;
description = "File containing the user password.";
};
};
});
}
);
};
localclientPassword = lib.mkOption {
@ -198,12 +214,17 @@ in
prometheusScraperPassword = lib.mkOption {
description = "Password for prometheus scraper. Setting this option will activate the prometheus deluge exporter.";
type = lib.types.nullOr (lib.types.submodule {
type = lib.types.nullOr (
lib.types.submodule {
options = contracts.secret.mkRequester {
owner = "deluge";
restartUnits = [ "deluged.service" "prometheus.service" ];
restartUnits = [
"deluged.service"
"prometheus.service"
];
};
});
}
);
default = null;
};
@ -214,14 +235,14 @@ in
Label is automatically enabled if any of the `shb.arr.*` service is enabled.
'';
example = ["Label"];
default = [];
example = [ "Label" ];
default = [ ];
};
additionalPlugins = lib.mkOption {
type = lib.types.listOf lib.types.path;
description = "Location of additional plugins. Each item in the list must be the path to the directory containing the plugin .egg file.";
default = [];
default = [ ];
example = lib.literalExpression ''
additionalPlugins = [
(pkgs.callPackage ({ python3, fetchFromGitHub }: python3.pkgs.buildPythonPackage {
@ -250,7 +271,7 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "deluge";
@ -262,14 +283,24 @@ in
};
logLevel = lib.mkOption {
type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
type = lib.types.nullOr (
lib.types.enum [
"critical"
"error"
"warning"
"info"
"debug"
]
);
description = "Enable logging.";
default = null;
example = "info";
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [{
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.deluge = {
enable = true;
declarative = true;
@ -292,7 +323,8 @@ in
};
outgoing_interface = cfg.outgoingInterface;
enabled_plugins = cfg.enabledPlugins
enabled_plugins =
cfg.enabledPlugins
++ lib.optional (lib.any (x: x.enable) [
config.services.radarr
config.services.sonarr
@ -316,7 +348,8 @@ in
max_upload_speed
max_upload_speed_per_torrent
dont_count_slow_torrents;
dont_count_slow_torrents
;
new_release_check = false;
};
@ -327,22 +360,31 @@ in
web.port = cfg.webPort;
};
systemd.services.deluged.preStart = lib.mkBefore (lib.shb.replaceSecrets {
userConfig = cfg.extraUsers // {
systemd.services.deluged.preStart = lib.mkBefore (
lib.shb.replaceSecrets {
userConfig =
cfg.extraUsers
// {
localclient.password.source = config.shb.deluge.localclientPassword.result.path;
} // (lib.optionalAttrs (config.shb.deluge.prometheusScraperPassword != null) {
}
// (lib.optionalAttrs (config.shb.deluge.prometheusScraperPassword != null) {
prometheus_scraper.password.source = config.shb.deluge.prometheusScraperPassword.result.path;
});
resultPath = "${cfg.dataDir}/.config/deluge/authTemplate";
generator = name: value: pkgs.writeText "delugeAuth" (authGenerator value);
});
}
);
systemd.services.deluged.serviceConfig.ExecStart = lib.mkForce (lib.concatStringsSep " \\\n " ([
systemd.services.deluged.serviceConfig.ExecStart = lib.mkForce (
lib.concatStringsSep " \\\n " (
[
"${config.services.deluge.package}/bin/deluged"
"--do-not-daemonize"
"--config ${cfg.dataDir}/.config/deluge"
] ++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}")
));
]
++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}")
)
);
systemd.tmpfiles.rules =
let
@ -356,7 +398,8 @@ in
];
shb.nginx.vhosts = [
({
(
{
inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
@ -370,16 +413,20 @@ in
{
domain = fqdn;
policy = "two_factor";
subject = ["group:deluge_user"];
subject = [ "group:deluge_user" ];
}
];
} // (lib.optionalAttrs (cfg.authEndpoint != null) {
}
// (lib.optionalAttrs (cfg.authEndpoint != null) {
inherit (cfg) authEndpoint;
}))
})
)
];
} {
}
{
systemd.services.deluged.serviceConfig = cfg.extraServiceConfig;
} (lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) {
}
(lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) {
services.prometheus.exporters.deluge = {
enable = true;
@ -393,15 +440,18 @@ in
services.prometheus.scrapeConfigs = [
{
job_name = "deluge";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.deluge.port}"];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.deluge.port}" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
}];
}
];
}
];
})
]);
]
);
}

View file

@ -1,18 +1,53 @@
{ config, options, pkgs, lib, ... }:
{
config,
options,
pkgs,
lib,
...
}:
let
cfg = config.shb.forgejo;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
inherit (lib) all attrNames concatMapStringsSep getExe lists literalExpression mapAttrsToList mkBefore mkEnableOption mkForce mkIf mkMerge mkOption mkOverride nameValuePair optionalString optionals;
inherit (lib.types) attrsOf bool enum listOf nullOr package port submodule str;
inherit (lib)
all
attrNames
concatMapStringsSep
getExe
lists
literalExpression
mapAttrsToList
mkBefore
mkEnableOption
mkForce
mkIf
mkMerge
mkOption
mkOverride
nameValuePair
optionalString
optionals
;
inherit (lib.types)
attrsOf
bool
enum
listOf
nullOr
package
port
submodule
str
;
in
{
imports = [
../blocks/nginx.nix
(lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''Instead, define an admin user in shb.forgejo.users and give it the same password, like so:
(lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''
Instead, define an admin user in shb.forgejo.users and give it the same password, like so:
shb.forgejo.users = {
"forgejoadmin" = {
isAdmin = true;
@ -60,7 +95,7 @@ in
description = ''
LDAP Integration.
'';
default = {};
default = { };
type = nullOr (submodule {
options = {
enable = mkEnableOption "LDAP integration.";
@ -125,7 +160,7 @@ in
waitForSystemdServices = mkOption {
type = listOf str;
default = [];
default = [ ];
description = ''
List of systemd services to wait on before starting.
This is needed because forgejo will try a lookup on the LDAP instance
@ -140,7 +175,7 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = submodule {
options = {
enable = mkEnableOption "SSO integration.";
@ -164,7 +199,10 @@ in
};
authorization_policy = mkOption {
type = enum [ "one_factor" "two_factor" ];
type = enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -205,7 +243,8 @@ in
};
email = mkOption {
description = ''Email of user.
description = ''
Email of user.
This is only set when the user is created, changing this later on will have no effect.
'';
@ -254,7 +293,6 @@ in
'';
};
hostPackages = mkOption {
type = listOf package;
default = with pkgs; [
@ -289,13 +327,14 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = options.services.forgejo.user.value;
sourceDirectories = [
options.services.forgejo.dump.backupDir.value
] ++ optionals (cfg.repositoryRoot != null) [
]
++ optionals (cfg.repositoryRoot != null) [
cfg.repositoryRoot
];
};
@ -317,7 +356,9 @@ in
```
'';
readOnly = true;
default = { path = config.services.forgejo.stateDir; };
default = {
path = config.services.forgejo.stateDir;
};
};
smtp = mkOption {
@ -390,10 +431,12 @@ in
# https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113
systemd.services.forgejo.serviceConfig.Type = mkForce "exec";
shb.nginx.vhosts = [{
shb.nginx.vhosts = [
{
inherit (cfg) domain subdomain ssl;
upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}";
}];
}
];
})
(mkIf cfg.enable {
@ -420,9 +463,11 @@ in
systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices;
systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices;
# The delimiter in the `cut` command is a TAB!
systemd.services.forgejo.preStart = let
systemd.services.forgejo.preStart =
let
provider = "SHB-${cfg.ldap.provider}";
in ''
in
''
auth="${getExe config.services.forgejo.package} admin auth"
echo "Trying to find existing ldap configuration for ${provider}"...
@ -497,9 +542,11 @@ in
};
# The delimiter in the `cut` command is a TAB!
systemd.services.forgejo.preStart = let
systemd.services.forgejo.preStart =
let
provider = "SHB-${cfg.sso.provider}";
in ''
in
''
auth="${getExe config.services.forgejo.package} admin auth"
echo "Trying to find existing sso configuration for ${provider}"...
@ -529,16 +576,19 @@ in
'';
shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [
(let
(
let
provider = "SHB-${cfg.sso.provider}";
in {
in
{
client_id = cfg.sso.clientID;
client_name = "Forgejo";
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
public = false;
authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ];
})
}
)
];
})
@ -552,13 +602,14 @@ in
systemd.services.forgejo.preStart = ''
admin="${getExe config.services.forgejo.package} admin user"
'' + concatMapStringsSep "\n" (u: ''
''
+ concatMapStringsSep "\n" (u: ''
if ! $admin list | grep "${u.name}"; then
$admin create ${optionalString u.value.isAdmin "--admin"} --email "${u.value.email}" --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})"
else
$admin change-password --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})"
fi
'') (mapAttrsToList nameValuePair cfg.users);
'') (mapAttrsToList nameValuePair cfg.users);
})
(mkIf (cfg.enable && cfg.smtp != null) {
@ -584,9 +635,11 @@ in
instances.local = {
enable = true;
name = "local";
url = let
url =
let
protocol = if cfg.ssl != null then "https" else "http";
in "${protocol}://${cfg.subdomain}.${cfg.domain}";
in
"${protocol}://${cfg.subdomain}.${cfg.domain}";
tokenFile = ""; # Empty variable to satisfy an assertion.
labels = [
# "ubuntu-latest:docker://node:16-bullseye"

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.grocy;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
in
@ -37,7 +42,24 @@ in
};
culture = lib.mkOption {
type = lib.types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ];
type = lib.types.enum [
"de"
"en"
"da"
"en_GB"
"es"
"fr"
"hu"
"it"
"nl"
"no"
"pl"
"pt_BR"
"ru"
"sk_SK"
"sv_SE"
"tr"
];
default = "en";
description = ''
Display language of the frontend.
@ -53,7 +75,7 @@ in
extraServiceConfig = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra configuration given to the systemd service file.";
default = {};
default = { };
example = lib.literalExpression ''
{
MemoryHigh = "512M";
@ -78,14 +100,24 @@ in
};
logLevel = lib.mkOption {
type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
type = lib.types.nullOr (
lib.types.enum [
"critical"
"error"
"warning"
"info"
"debug"
]
);
description = "Enable logging.";
default = false;
example = true;
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [{
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.grocy = {
enable = true;
hostName = fqdn;
@ -97,7 +129,7 @@ in
services.phpfpm.pools.grocy.group = lib.mkForce "grocy";
users.groups.grocy = {};
users.groups.grocy = { };
users.users.grocy.group = lib.mkForce "grocy";
services.nginx.virtualHosts."${fqdn}" = {
@ -105,7 +137,10 @@ in
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
};
} {
}
{
systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig;
}]);
}
]
);
}

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.hledger;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
in
@ -63,7 +68,7 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "hledger";
@ -76,7 +81,7 @@ in
extraArguments = lib.mkOption {
description = "Extra arguments append to the hledger command.";
default = ["--forecast"];
default = [ "--forecast" ];
type = lib.types.listOf lib.types.str;
};
};
@ -88,7 +93,7 @@ in
baseUrl = "";
stateDir = cfg.dataDir;
journalFiles = ["hledger.journal"];
journalFiles = [ "hledger.journal" ];
host = "127.0.0.1";
port = cfg.port;
@ -108,13 +113,20 @@ in
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain authEndpoint ssl;
inherit (cfg)
subdomain
domain
authEndpoint
ssl
;
upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}";
autheliaRules = [{
autheliaRules = [
{
domain = fqdn;
policy = "two_factor";
subject = ["group:hledger_user"];
}];
subject = [ "group:hledger_user" ];
}
];
}
];
};

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.home-assistant;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -24,9 +29,7 @@ let
nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config);
configWithSecretsIncludes =
nonSecrets
// (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
configWithSecretsIncludes = nonSecrets // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
in
{
options.shb.home-assistant = {
@ -56,28 +59,49 @@ in
freeformType = lib.types.attrsOf lib.types.str;
options = {
name = lib.mkOption {
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
type = lib.types.oneOf [
lib.types.str
lib.shb.secretFileType
];
description = "Name of the Home Assistant instance.";
};
country = lib.mkOption {
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
type = lib.types.oneOf [
lib.types.str
lib.shb.secretFileType
];
description = "Two letter country code where this instance is located.";
};
latitude = lib.mkOption {
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
type = lib.types.oneOf [
lib.types.str
lib.shb.secretFileType
];
description = "Latitude where this instance is located.";
};
longitude = lib.mkOption {
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
type = lib.types.oneOf [
lib.types.str
lib.shb.secretFileType
];
description = "Longitude where this instance is located.";
};
time_zone = lib.mkOption {
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
type = lib.types.oneOf [
lib.types.str
lib.shb.secretFileType
];
description = "Timezone of this instance.";
example = "America/Los_Angeles";
};
unit_system = lib.mkOption {
type = lib.types.oneOf [ lib.types.str (lib.types.enum [ "metric" "us_customary" ]) ];
type = lib.types.oneOf [
lib.types.str
(lib.types.enum [
"metric"
"us_customary"
])
];
description = "Unit system of this instance.";
example = "metric";
};
@ -95,7 +119,7 @@ in
Also, enabling LDAP will skip onboarding
otherwise Home Assistant gets into a cyclic lock.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "LDAP app.";
@ -140,7 +164,7 @@ in
voice = lib.mkOption {
description = "Options related to voice service.";
default = {};
default = { };
type = lib.types.submodule {
options = {
speech-to-text = lib.mkOption {
@ -150,7 +174,7 @@ in
https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.piper.servers
'';
type = lib.types.attrsOf lib.types.anything;
default = {};
default = { };
};
text-to-speech = lib.mkOption {
description = ''
@ -159,7 +183,7 @@ in
https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.faster-whisper.servers
'';
type = lib.types.attrsOf lib.types.anything;
default = {};
default = { };
};
wakeword = lib.mkOption {
description = ''
@ -168,7 +192,9 @@ in
https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.openwakeword
'';
type = lib.types.anything;
default = { enable = false; };
default = {
enable = false;
};
};
};
};
@ -178,7 +204,7 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "hass";
@ -219,7 +245,7 @@ in
config = {
# Includes dependencies for a basic setup
# https://www.home-assistant.io/integrations/default_config/
default_config = {};
default_config = { };
http = {
use_x_forwarded_for = true;
server_host = "127.0.0.1";
@ -240,7 +266,10 @@ in
{
type = "command_line";
command = ldap_auth_script + "/bin/ldap_auth.sh";
args = [ "http://${cfg.ldap.host}:${toString cfg.ldap.port}" cfg.ldap.userGroup ];
args = [
"http://${cfg.ldap.host}:${toString cfg.ldap.port}"
cfg.ldap.userGroup
];
meta = true;
}
]);
@ -261,11 +290,11 @@ in
action = [
{
service = "shell_command.delete_backups";
data = {};
data = { };
}
{
service = "backup.create";
data = {};
data = { };
}
];
mode = "single";
@ -286,7 +315,11 @@ in
{
name = "random_joke";
platform = "rest";
json_attributes = ["joke" "id" "status"];
json_attributes = [
"joke"
"id"
"status"
];
value_template = "{{ value_json.joke }}";
resource = "https://icanhazdadjoke.com/";
scan_interval = "3600";
@ -324,7 +357,8 @@ in
};
systemd.services.home-assistant.preStart =
(let
(
let
# TODO: this probably does not work anymore
onboarding = pkgs.writeText "onboarding" ''
{
@ -342,15 +376,17 @@ in
'';
storage = "${config.services.home-assistant.configDir}";
file = "${storage}/.storage/onboarding";
in ''
in
''
if [ ! -f ${file} ]; then
mkdir -p ''$(dirname ${file}) && cp ${onboarding} ${file}
fi
'')
''
)
+ (lib.shb.replaceSecrets {
userConfig = cfg.config;
resultPath = "${config.services.home-assistant.configDir}/secrets.yaml";
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML {});
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { });
});
systemd.tmpfiles.rules = [

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.immich;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
protocol = if !(isNull cfg.ssl) then "https" else "http";
@ -12,21 +17,29 @@ let
# TODO: Quota management, see https://github.com/ibizaman/selfhostblocks/pull/523#discussion_r2309421694
#quotaClaim = "immich_quota";
scopes = [ "openid" "email" "profile" "groups" "immich_scope"];
scopes = [
"openid"
"email"
"profile"
"groups"
"immich_scope"
];
dataFolder = cfg.mediaLocation;
ssoFqdnWithPort = if isNull cfg.sso.port
then cfg.sso.endpoint
else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
ssoFqdnWithPort =
if isNull cfg.sso.port then cfg.sso.endpoint else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
# Generate Immich configuration file only for SHB-managed settings
shbManagedSettings = lib.optionalAttrs (cfg.settings != {}) cfg.settings
shbManagedSettings =
lib.optionalAttrs (cfg.settings != { }) cfg.settings
// lib.optionalAttrs (cfg.sso.enable) {
oauth = {
enabled = true;
issuerUrl = "${ssoFqdnWithPort}";
clientId = cfg.sso.clientID;
roleClaim = roleClaim;
clientSecret = { source = cfg.sso.sharedSecret.result.path; };
clientSecret = {
source = cfg.sso.sharedSecret.result.path;
};
scope = builtins.concatStringsSep " " scopes;
storageLabelClaim = cfg.sso.storageLabelClaim;
#storageQuotaClaim = quotaClaim; # TODO (commented out, otherwise defaults to 0 bytes!)
@ -49,7 +62,9 @@ let
host = cfg.smtp.host;
port = cfg.smtp.port;
username = cfg.smtp.username;
password = { source = cfg.smtp.password.result.path; };
password = {
source = cfg.smtp.password.result.path;
};
ignoreTLS = cfg.smtp.ignoreTLS;
secure = cfg.smtp.secure;
};
@ -64,13 +79,30 @@ let
lib.shb.replaceSecrets {
userConfig = shbManagedSettings;
resultPath = configFile;
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json {});
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json { });
user = "immich";
permissions = "u=r,g=,o=";
}
);
inherit (lib) mkEnableOption mkIf lists mkOption optionals;
inherit (lib.types) attrs attrsOf bool enum listOf nullOr port submodule str path;
inherit (lib)
mkEnableOption
mkIf
lists
mkOption
optionals
;
inherit (lib.types)
attrs
attrsOf
bool
enum
listOf
nullOr
port
submodule
str
path
;
in
{
imports = [
@ -154,14 +186,16 @@ in
```
'';
readOnly = true;
default = { path = dataFolder; };
default = {
path = dataFolder;
};
};
backup = mkOption {
description = ''
Backup configuration for Immich media files and database.
'';
default = {};
default = { };
type = submodule {
options = contracts.backup.mkRequester {
user = "immich";
@ -190,7 +224,7 @@ in
machineLearning = mkOption {
description = "Machine learning configuration.";
default = {};
default = { };
type = submodule {
options = {
enable = mkOption {
@ -202,7 +236,7 @@ in
environment = mkOption {
description = "Extra environment variables for machine learning service.";
type = attrsOf str;
default = {};
default = { };
example = {
MACHINE_LEARNING_WORKERS = "2";
MACHINE_LEARNING_WORKER_TIMEOUT = "180";
@ -216,13 +250,17 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = submodule {
options = {
enable = mkEnableOption "SSO integration.";
provider = mkOption {
type = enum [ "Authelia" "Keycloak" "Generic" ];
type = enum [
"Authelia"
"Keycloak"
"Generic"
];
description = "OIDC provider name, used for display.";
default = "Authelia";
};
@ -311,7 +349,10 @@ in
};
authorization_policy = mkOption {
type = enum [ "one_factor" "two_factor" ];
type = enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -328,7 +369,7 @@ in
See https://immich.app/docs/install/config-file/ for available options.
'';
default = {};
default = { };
example = {
ffmpeg.crf = 23;
job.backgroundTask.concurrency = 5;
@ -452,9 +493,11 @@ in
REDIS_HOSTNAME = "127.0.0.1";
REDIS_PORT = "6379";
REDIS_DBINDEX = "0";
} // lib.optionalAttrs (cfg.jwtSecretFile != null) {
}
// lib.optionalAttrs (cfg.jwtSecretFile != null) {
JWT_SECRET_FILE = cfg.jwtSecretFile.result.path;
} // lib.optionalAttrs (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) {
}
// lib.optionalAttrs (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) {
IMMICH_CONFIG_FILE = configFile;
};
};
@ -465,7 +508,9 @@ in
];
# Configuration setup service - generates config only for SHB-managed settings
systemd.services.immich-setup-config = mkIf (cfg.enable && (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null)) {
systemd.services.immich-setup-config =
mkIf (cfg.enable && (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null))
{
description = "Setup Immich configuration for SHB-managed settings";
wantedBy = [ "multi-user.target" ];
before = [ "immich-server.service" ];
@ -484,7 +529,10 @@ in
};
# Add immich user to video and render groups for hardware acceleration
users.users.immich.extraGroups = optionals (cfg.accelerationDevices != []) [ "video" "render" ];
users.users.immich.extraGroups = optionals (cfg.accelerationDevices != [ ]) [
"video"
"render"
];
# PostgreSQL extensions are automatically handled by the Immich service
@ -499,7 +547,10 @@ in
{
domain = fqdn;
policy = cfg.sso.authorization_policy;
subject = ["group:immich_user" "group:immich_admin"];
subject = [
"group:immich_user"
"group:immich_admin"
];
}
];
authEndpoint = lib.mkIf (cfg.sso.enable) cfg.sso.endpoint;
@ -515,10 +566,20 @@ in
# Ensure services start in correct order
systemd.services.immich-server = {
after = [ "postgresql.service" "redis-immich.service" ]
++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ];
requires = [ "postgresql.service" "redis-immich.service" ]
++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ];
after = [
"postgresql.service"
"redis-immich.service"
]
++ optionals (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) [
"immich-setup-config.service"
];
requires = [
"postgresql.service"
"redis-immich.service"
]
++ optionals (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) [
"immich-setup-config.service"
];
};
systemd.services.immich-machine-learning = mkIf cfg.machineLearning.enable {
@ -530,18 +591,18 @@ in
# Immich expects all users that get a token to be granted access. So users can either be part of the
# "admin" group or the "user" group. Users that are not part of either should be blocked by
# the ID provider (Authelia).
user_attributes.${roleClaim}.expression = ''"${cfg.sso.adminUserGroup}" in groups ? "admin" : "user"'';
user_attributes.${roleClaim}.expression =
''"${cfg.sso.adminUserGroup}" in groups ? "admin" : "user"'';
};
shb.authelia.extraOidcClaimsPolicies.immich_policy = {
custom_claims = {
${roleClaim} = {};
${roleClaim} = { };
};
};
shb.authelia.extraOidcScopes.immich_scope = {
claims = [ roleClaim ];
};
shb.authelia.oidcClients = lists.optionals (cfg.sso.enable && cfg.sso.provider == "Authelia") [
{
client_id = cfg.sso.clientID;

View file

@ -1,11 +1,16 @@
{ config, lib, pkgs, ...}:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
cfg = config.shb.jellyfin;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -91,7 +96,8 @@ in
admin = lib.mkOption {
description = "Default admin user info. Only needed if LDAP or SSO is not configured.";
default = null;
type = types.nullOr (types.submodule {
type = types.nullOr (
types.submodule {
options = {
username = lib.mkOption {
description = "Username of the default admin user.";
@ -110,12 +116,13 @@ in
};
};
};
});
}
);
};
ldap = lib.mkOption {
description = "LDAP configuration.";
default = {};
default = { };
type = types.submodule {
options = {
enable = lib.mkEnableOption "LDAP";
@ -167,7 +174,7 @@ in
sso = lib.mkOption {
description = "SSO configuration.";
default = {};
default = { };
type = types.submodule {
options = {
enable = lib.mkEnableOption "SSO";
@ -203,7 +210,10 @@ in
};
authorization_policy = lib.mkOption {
type = types.enum [ "one_factor" "two_factor" ];
type = types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -238,14 +248,15 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = types.submodule {
options = contracts.backup.mkRequester {
user = "jellyfin";
sourceDirectories = [
config.services.jellyfin.dataDir
];
sourceDirectoriesText = ''[
sourceDirectoriesText = ''
[
"services.jellyfin.dataDir"
]'';
};
@ -254,7 +265,10 @@ in
};
imports = [
(lib.mkRenamedOptionModule [ "shb" "jellyfin" "adminPassword" ] [ "shb" "jellyfin" "admin" "password" ])
(lib.mkRenamedOptionModule
[ "shb" "jellyfin" "adminPassword" ]
[ "shb" "jellyfin" "admin" "password" ]
)
];
config = lib.mkIf cfg.enable {
@ -269,7 +283,10 @@ in
networking.firewall = {
# from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery
allowedUDPPorts = [ 1900 7359 ];
allowedUDPPorts = [
1900
7359
];
};
services.nginx.enable = true;
@ -383,18 +400,20 @@ in
'';
};
services.prometheus.scrapeConfigs = [{
services.prometheus.scrapeConfigs = [
{
job_name = "jellyfin";
static_configs = [
{
targets = ["127.0.0.1:${toString cfg.port}"];
targets = [ "127.0.0.1:${toString cfg.port}" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
}
];
}];
}
];
# LDAP config but you need to install the plugin by hand
@ -560,8 +579,7 @@ in
</NetworkConfiguration>
'';
in
lib.strings.optionalString cfg.debug
''
lib.strings.optionalString cfg.debug ''
if [ -f "${config.services.jellyfin.configDir}/logging.json" ] && [ ! -L "${config.services.jellyfin.configDir}/logging.json" ]; then
echo "A ${config.services.jellyfin.configDir}/logging.json file exists already, this indicates probably an existing installation. Please remove it before continuing."
exit 1
@ -576,7 +594,8 @@ in
replacements = [
];
})
+ lib.strings.optionalString cfg.ldap.enable (lib.shb.replaceSecretsScript {
+ lib.strings.optionalString cfg.ldap.enable (
lib.shb.replaceSecretsScript {
file = ldapConfig;
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml";
replacements = [
@ -585,8 +604,10 @@ in
source = cfg.ldap.adminPassword.result.path;
}
];
})
+ lib.strings.optionalString cfg.sso.enable (lib.shb.replaceSecretsScript {
}
)
+ lib.strings.optionalString cfg.sso.enable (
lib.shb.replaceSecretsScript {
file = ssoConfig;
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/SSO-Auth.xml";
replacements = [
@ -595,15 +616,19 @@ in
source = cfg.sso.sharedSecret.result.path;
}
];
})
+ lib.strings.optionalString cfg.sso.enable (lib.shb.replaceSecretsScript {
}
)
+ lib.strings.optionalString cfg.sso.enable (
lib.shb.replaceSecretsScript {
file = brandingConfig;
resultPath = "${config.services.jellyfin.dataDir}/config/branding.xml";
replacements = [
];
});
}
);
systemd.services.jellyfin.serviceConfig.ExecStartPost = let
systemd.services.jellyfin.serviceConfig.ExecStartPost =
let
# We must always wait for the service to be fully initialized,
# even if we're planning on changing the config and restarting.
waitForCurl = pkgs.writeShellApplication {
@ -646,7 +671,7 @@ in
#
# If the file does not exist, write the config, create the file then restart.
# If the file exists, do nothing and remove the file, resetting the state for the next time.
restartedFile="${config.services.jellyfin.dataDir}/.jellyfin-restarted";
restartedFile = "${config.services.jellyfin.dataDir}/.jellyfin-restarted";
writeConfig = pkgs.writeShellApplication {
name = "writeConfig";

View file

@ -1,8 +1,13 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.karakeep;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
in
{
imports = [
@ -37,7 +42,7 @@ in
};
environment = lib.mkOption {
default = {};
default = { };
type = lib.types.attrsOf lib.types.str;
description = "Extra environment variables. See https://docs.karakeep.app/configuration/";
example = ''
@ -56,7 +61,7 @@ in
description = ''
Setup LDAP integration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
userGroup = lib.mkOption {
@ -72,7 +77,7 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO integration.";
@ -91,7 +96,10 @@ in
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -102,7 +110,11 @@ in
options = contracts.secret.mkRequester {
owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets.
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
restartUnits = [
"karakeep-init.service"
"karakeep-workers.service"
"karakeep-workers.service"
];
};
};
};
@ -125,7 +137,7 @@ in
description = ''
Backup state directory.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "karakeep";
@ -142,7 +154,11 @@ in
options = contracts.secret.mkRequester {
owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets.
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
restartUnits = [
"karakeep-init.service"
"karakeep-workers.service"
"karakeep-workers.service"
];
};
};
};
@ -153,13 +169,18 @@ in
options = contracts.secret.mkRequester {
owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets.
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
restartUnits = [
"karakeep-init.service"
"karakeep-workers.service"
"karakeep-workers.service"
];
};
};
};
};
config = (lib.mkMerge [
config = (
lib.mkMerge [
(lib.mkIf cfg.enable {
services.karakeep = {
enable = true;
@ -168,7 +189,8 @@ in
extraEnvironment = {
PORT = toString cfg.port;
DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS
} // cfg.environment;
}
// cfg.environment;
};
shb.nginx.vhosts = [
@ -182,19 +204,23 @@ in
# This is needed otherwise the MEILI_MASTER_KEY is generated randomly on first start
# instead of using the value from the cfg.meilisearchMasterKey option.
systemd.services.karakeep-init = {
script = lib.mkForce ((lib.shb.replaceSecrets {
script = lib.mkForce (
(lib.shb.replaceSecrets {
userConfig = {
MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path;
NEXTAUTH_SECRET.source = cfg.nextauthSecret.result.path;
} // lib.optionalAttrs cfg.sso.enable {
}
// lib.optionalAttrs cfg.sso.enable {
OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path;
};
resultPath = "/var/lib/karakeep/settings.env";
generator = lib.shb.toEnvVar;
}) + ''
})
+ ''
export DATA_DIR="$STATE_DIRECTORY"
exec ${config.services.karakeep.package}/lib/karakeep/migrate
'');
''
);
};
})
(lib.mkIf cfg.enable {
@ -206,7 +232,7 @@ in
})
(lib.mkIf (cfg.enable && cfg.sso.enable) {
shb.lldap.ensureGroups = {
${cfg.ldap.userGroup} = {};
${cfg.ldap.userGroup} = { };
};
shb.authelia.extraOidcAuthorizationPolicies.karakeep = {
@ -222,7 +248,11 @@ in
{
client_id = cfg.sso.clientID;
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
scopes = [ "openid" "email" "profile" ];
scopes = [
"openid"
"email"
"profile"
];
authorization_policy = "karakeep";
redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/api/auth/callback/custom"
@ -241,5 +271,6 @@ in
};
};
})
]);
]
);
}

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.nextcloud;
@ -7,12 +12,17 @@ let
fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}";
protocol = if !(isNull cfg.ssl) then "https" else "http";
ssoFqdnWithPort = if isNull cfg.apps.sso.port then cfg.apps.sso.endpoint else "${cfg.apps.sso.endpoint}:${toString cfg.apps.sso.port}";
ssoFqdnWithPort =
if isNull cfg.apps.sso.port then
cfg.apps.sso.endpoint
else
"${cfg.apps.sso.endpoint}:${toString cfg.apps.sso.port}";
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
nextcloudPkg = builtins.getAttr ("nextcloud" + builtins.toString cfg.version) pkgs;
nextcloudApps = (builtins.getAttr ("nextcloud" + builtins.toString cfg.version + "Packages") pkgs).apps;
nextcloudApps =
(builtins.getAttr ("nextcloud" + builtins.toString cfg.version + "Packages") pkgs).apps;
occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ";
in
@ -71,7 +81,10 @@ in
version = lib.mkOption {
description = "Nextcloud version to choose from.";
type = lib.types.enum [ 31 32 ];
type = lib.types.enum [
31
32
];
default = 31;
};
@ -84,7 +97,7 @@ in
mountPointServices = lib.mkOption {
description = "If given, all the systemd services and timers will depend on the specified mount point systemd services.";
type = lib.types.listOf lib.types.str;
default = [];
default = [ ];
example = lib.literalExpression ''["var.mount"]'';
};
@ -105,7 +118,6 @@ in
};
};
maxUploadSize = lib.mkOption {
default = "4G";
type = lib.types.str;
@ -188,7 +200,7 @@ in
phpFpmPrometheusExporter = lib.mkOption {
description = "Settings for exporting";
default = {};
default = { };
type = lib.types.submodule {
options = {
@ -216,7 +228,7 @@ in
through the UI. You can still make changes but they will be overridden on next deploy. You
can still install and configure other apps through the UI.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
onlyoffice = lib.mkOption {
@ -226,7 +238,7 @@ in
Enabling this app will also start an OnlyOffice instance accessible at the given
subdomain from the given network range.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Nextcloud OnlyOffice App";
@ -275,7 +287,7 @@ in
nextcloud-occ -vvv preview:generate-all
```
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Nextcloud Preview Generator App";
@ -330,14 +342,15 @@ in
other side, a spinning hard drive can store more data which is well suited for storing
user data.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Nextcloud External Storage App";
userLocalMount = lib.mkOption {
default = null;
description = "If set, adds a local mount as external storage.";
type = lib.types.nullOr (lib.types.submodule {
type = lib.types.nullOr (
lib.types.submodule {
options = {
directory = lib.mkOption {
type = lib.types.str;
@ -354,10 +367,14 @@ in
Path of the mount in Nextcloud. Use `/` to mount as the root.
'';
default = "";
example = [ "home" "/" ];
example = [
"home"
"/"
];
};
};
});
}
);
};
};
};
@ -370,8 +387,9 @@ in
Enabling this app will create a new LDAP configuration or update one that exists with
the given host.
'';
default = {};
type = lib.types.nullOr (lib.types.submodule {
default = { };
type = lib.types.nullOr (
lib.types.submodule {
options = {
enable = lib.mkEnableOption "LDAP app.";
@ -429,7 +447,8 @@ in
default = 50;
};
};
});
}
);
};
sso = lib.mkOption {
@ -439,7 +458,7 @@ in
Enabling this app will create a new LDAP configuration or update one that exists with
the given host.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO app.";
@ -469,7 +488,10 @@ in
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -491,7 +513,6 @@ in
};
};
secretForAuthelia = lib.mkOption {
description = "OIDC shared secret. Content must be the same as `secretFile` option.";
type = lib.types.submodule {
@ -502,7 +523,6 @@ in
};
};
fallbackDefaultAuth = lib.mkOption {
type = lib.types.bool;
description = ''
@ -528,7 +548,7 @@ in
nextcloud-occ memories:index
```
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Memories app.";
@ -561,7 +581,7 @@ in
Enabling this app will set up the Recognize app and configure all its dependencies.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "Recognize app.";
@ -599,19 +619,18 @@ in
'';
};
backup = lib.mkOption {
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "nextcloud";
sourceDirectories = [
cfg.dataDir
];
excludePatterns = [".rnd"];
excludePatterns = [ ".rnd" ];
};
};
};
@ -722,13 +741,15 @@ in
# Very important for a bunch of scripts to load correctly. Otherwise you get Content-Security-Policy errors. See https://docs.nextcloud.com/server/13/admin_manual/configuration_server/harden_server.html#enable-http-strict-transport-security
https = !(isNull cfg.ssl);
extraApps = if isNull cfg.extraApps then {} else cfg.extraApps nextcloudApps;
extraApps = if isNull cfg.extraApps then { } else cfg.extraApps nextcloudApps;
extraAppsEnable = true;
appstoreEnable = true;
settings = let
settings =
let
protocol = if !(isNull cfg.ssl) then "https" else "http";
in {
in
{
"default_phone_region" = cfg.defaultPhoneRegion;
"overwrite.cli.url" = "${protocol}://${fqdn}";
@ -773,7 +794,8 @@ in
"redis.session.locking_enabled" = "1";
"redis.session.lock_retries" = "-1";
"redis.session.lock_wait_time" = "10000";
} // lib.optionalAttrs (! (isNull cfg.tracing)) {
}
// lib.optionalAttrs (!(isNull cfg.tracing)) {
# "xdebug.remote_enable" = "on";
# "xdebug.remote_host" = "127.0.0.1";
# "xdebug.remote_port" = "9000";
@ -785,7 +807,7 @@ in
"xdebug.start_with_request" = "trigger";
};
poolSettings = lib.mkIf (! (isNull cfg.phpFpmPoolSettings)) cfg.phpFpmPoolSettings;
poolSettings = lib.mkIf (!(isNull cfg.phpFpmPoolSettings)) cfg.phpFpmPoolSettings;
phpExtraExtensions = all: [ all.xdebug ];
};
@ -810,7 +832,7 @@ in
pkgs.ffmpeg-headless
];
services.postgresql.settings = lib.mkIf (! (isNull cfg.postgresSettings)) cfg.postgresSettings;
services.postgresql.settings = lib.mkIf (!(isNull cfg.postgresSettings)) cfg.postgresSettings;
systemd.services.phpfpm-nextcloud.preStart = ''
mkdir -p /var/log/xdebug; chown -R nextcloud: /var/log/xdebug
@ -837,7 +859,9 @@ in
port = cfg.phpFpmPrometheusExporter.port;
listenAddress = "127.0.0.1";
extraFlags = [
"--phpfpm.scrape-uri=tcp://127.0.0.1:${toString (cfg.phpFpmPrometheusExporter.port -1)}/status?full"
"--phpfpm.scrape-uri=tcp://127.0.0.1:${
toString (cfg.phpFpmPrometheusExporter.port - 1)
}/status?full"
];
};
@ -849,20 +873,22 @@ in
#
# I also tried to server the status page at /status.php
# but fcgi doesn't like the returned headers.
"pm.status_listen" = "127.0.0.1:${toString (cfg.phpFpmPrometheusExporter.port -1)}";
"pm.status_listen" = "127.0.0.1:${toString (cfg.phpFpmPrometheusExporter.port - 1)}";
};
};
services.prometheus.scrapeConfigs = [
{
job_name = "phpfpm-nextcloud";
static_configs = [{
targets = ["127.0.0.1:${toString cfg.phpFpmPrometheusExporter.port}"];
static_configs = [
{
targets = [ "127.0.0.1:${toString cfg.phpFpmPrometheusExporter.port}" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
}];
}
];
}
];
})
@ -964,7 +990,8 @@ in
systemd.services.nextcloud-setup.script = ''
${occ} app:install files_external || :
${occ} app:enable files_external
'' + lib.optionalString (cfg.apps.externalStorage.userLocalMount != null) (
''
+ lib.optionalString (cfg.apps.externalStorage.userLocalMount != null) (
let
cfg' = cfg.apps.externalStorage.userLocalMount;
jq = "${pkgs.jq}/bin/jq";
@ -979,7 +1006,8 @@ in
null::null \
--config datadir='${cfg'.directory}'
fi
'');
''
);
})
(lib.mkIf (cfg.enable && cfg.apps.ldap.enable) {
@ -988,7 +1016,8 @@ in
let
cfg' = cfg.apps.ldap;
cID = "s" + toString cfg'.configID;
in ''
in
''
${occ} app:install user_ldap || :
${occ} app:enable user_ldap
@ -1055,7 +1084,8 @@ in
'';
})
(let
(
let
scopes = [
"openid"
"profile"
@ -1063,7 +1093,8 @@ in
"groups"
"nextcloud_userinfo"
];
in lib.mkIf (cfg.enable && cfg.apps.sso.enable) {
in
lib.mkIf (cfg.enable && cfg.apps.sso.enable) {
assertions = [
{
assertion = cfg.ssl != null;
@ -1080,8 +1111,7 @@ in
before = [ "nextcloud-setup.service" ];
serviceConfig.Type = "oneshot";
serviceConfig.User = "nextcloud";
script =
''
script = ''
mkdir -p ${cfg.dataDir}/config
cat <<EOF > "${cfg.dataDir}/config/secretFile"
{
@ -1157,11 +1187,12 @@ in
};
shb.authelia.extraDefinitions = {
user_attributes."is_nextcloud_admin".expression = ''type(groups) == list && "${cfg.apps.sso.adminGroup}" in groups'';
user_attributes."is_nextcloud_admin".expression =
''type(groups) == list && "${cfg.apps.sso.adminGroup}" in groups'';
};
shb.authelia.extraOidcClaimsPolicies."nextcloud_userinfo" = {
custom_claims = {
is_nextcloud_admin = {};
is_nextcloud_admin = { };
};
};
shb.authelia.extraOidcScopes."nextcloud_userinfo" = {
@ -1187,11 +1218,11 @@ in
token_endpoint_auth_method = "client_secret_basic";
}
];
})
}
)
(lib.mkIf (cfg.enable && cfg.autoDisableMaintenanceModeOnStart) {
systemd.services.nextcloud-setup.preStart =
lib.mkBefore ''
systemd.services.nextcloud-setup.preStart = lib.mkBefore ''
if [[ -e /var/lib/nextcloud/config/config.php ]]; then
${occ} maintenance:mode --no-interaction --quiet --off
fi
@ -1199,8 +1230,7 @@ in
})
(lib.mkIf (cfg.enable && cfg.alwaysApplyExpensiveMigrations) {
systemd.services.nextcloud-setup.script =
''
systemd.services.nextcloud-setup.script = ''
if [[ -e /var/lib/nextcloud/config/config.php ]]; then
${occ} maintenance:repair --include-expensive
fi
@ -1209,17 +1239,19 @@ in
# Great source of inspiration:
# https://github.com/Shawn8901/nix-configuration/blob/538c18d9ecbf7c7e649b1540c0d40881bada6690/modules/nixos/private/nextcloud/memories.nix#L226
(lib.mkIf cfg.apps.memories.enable
(let
(lib.mkIf cfg.apps.memories.enable (
let
cfg' = cfg.apps.memories;
exiftool = pkgs.exiftool.overrideAttrs (f: p: {
exiftool = pkgs.exiftool.overrideAttrs (
f: p: {
version = "12.70";
src = pkgs.fetchurl {
url = "https://exiftool.org/Image-ExifTool-12.70.tar.gz";
hash = "sha256-TLJSJEXMPj870TkExq6uraX8Wl4kmNerrSlX3LQsr/4=";
};
});
}
);
in
{
assertions = [
@ -1265,10 +1297,11 @@ in
DeviceAllow = [ "/dev/dri/renderD128 rwm" ];
PrivateDevices = lib.mkForce false;
};
}))
}
))
(lib.mkIf cfg.apps.recognize.enable
(let
(lib.mkIf cfg.apps.recognize.enable (
let
cfg' = cfg.apps.recognize;
in
{
@ -1276,8 +1309,7 @@ in
inherit (nextcloudApps) recognize;
};
systemd.services.nextcloud-setup.script =
''
systemd.services.nextcloud-setup.script = ''
${occ} config:app:set recognize nice_binary --value ${pkgs.coreutils}/bin/nice
${occ} config:app:set recognize node_binary --value ${pkgs.nodejs}/bin/node
${occ} config:app:set recognize faces.enabled --value true
@ -1292,6 +1324,7 @@ in
${occ} config:app:set recognize musicnn.enabled --value true
${occ} config:app:set recognize musicnn.batchSize --value 100
'';
}))
}
))
];
}

View file

@ -1,11 +1,22 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.open-webui;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
roleClaim = "openwebui_groups";
oauthScopes = [ "openid" "email" "profile" "groups" "${roleClaim}" ];
oauthScopes = [
"openid"
"email"
"profile"
"groups"
"${roleClaim}"
];
in
{
imports = [
@ -42,7 +53,7 @@ in
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra environment variables. See https://docs.openwebui.com/getting-started/env-configuration";
default = {};
default = { };
example = ''
{
WEBUI_NAME = "SelfHostBlocks";
@ -62,7 +73,7 @@ in
description = ''
Setup LDAP integration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
userGroup = lib.mkOption {
@ -84,7 +95,7 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO integration.";
@ -103,7 +114,10 @@ in
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -136,7 +150,7 @@ in
description = ''
Backup state directory.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "open-webui";
@ -149,13 +163,14 @@ in
};
};
config = (lib.mkMerge [
config = (
lib.mkMerge [
(lib.mkIf cfg.enable {
users.users.open-webui = {
isSystemUser = true;
group = "open-webui";
};
users.groups.open-webui = {};
users.groups.open-webui = { };
services.open-webui = {
enable = true;
@ -173,7 +188,8 @@ in
SCARF_NO_ANALYTICS = "True";
ENABLE_VERSION_UPDATE_CHECK = "False";
} // cfg.environment;
}
// cfg.environment;
};
systemd.services.open-webui.path = [
@ -193,8 +209,8 @@ in
})
(lib.mkIf (cfg.enable && cfg.sso.enable) {
shb.lldap.ensureGroups = {
${cfg.ldap.userGroup} = {};
${cfg.ldap.adminGroup} = {};
${cfg.ldap.userGroup} = { };
${cfg.ldap.adminGroup} = { };
};
services.open-webui = {
@ -228,7 +244,7 @@ in
};
shb.authelia.extraOidcClaimsPolicies.${roleClaim} = {
custom_claims = {
"${roleClaim}" = {};
"${roleClaim}" = { };
};
};
shb.authelia.extraOidcScopes."${roleClaim}" = {
@ -268,5 +284,6 @@ in
requiredBy = [ "open-webui.service" ];
};
})
]);
]
);
}

View file

@ -1,10 +1,15 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.pinchflat;
inherit (lib) types;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
in
{
imports = [
@ -57,7 +62,10 @@ in
};
timeZone = lib.mkOption {
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
type = lib.types.oneOf [
lib.types.str
lib.shb.secretFileType
];
description = "Timezone of this instance.";
example = "America/Los_Angeles";
};
@ -66,7 +74,7 @@ in
description = ''
Setup LDAP integration.
'';
default = {};
default = { };
type = types.submodule {
options = {
enable = lib.mkEnableOption "LDAP integration." // {
@ -86,7 +94,7 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = types.submodule {
options = {
enable = lib.mkEnableOption "SSO integration.";
@ -99,7 +107,10 @@ in
};
authorization_policy = lib.mkOption {
type = types.enum [ "one_factor" "two_factor" ];
type = types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -111,7 +122,7 @@ in
description = ''
Backup media directory `shb.mediaDir`.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "pinchflat";
@ -142,7 +153,9 @@ in
# This should be using a contract instead of setting the option directly.
shb.lldap = lib.mkIf config.shb.lldap.enable {
ensureGroups = { ${cfg.ldap.userGroup} = {}; };
ensureGroups = {
${cfg.ldap.userGroup} = { };
};
};
systemd.services.pinchflat-pre = {
@ -181,7 +194,7 @@ in
job_name = "pinchflat";
static_configs = [
{
targets = ["127.0.0.1:${toString cfg.port}"];
targets = [ "127.0.0.1:${toString cfg.port}" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;

View file

@ -1,13 +1,22 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.vaultwarden;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
dataFolder = if lib.versionOlder (config.system.stateVersion or "24.11") "24.11" then "/var/lib/bitwarden_rs" else "/var/lib/vaultwarden";
dataFolder =
if lib.versionOlder (config.system.stateVersion or "24.11") "24.11" then
"/var/lib/bitwarden_rs"
else
"/var/lib/vaultwarden";
in
{
imports = [
@ -55,7 +64,10 @@ in
mode = "0440";
owner = "vaultwarden";
group = "postgres";
restartUnits = [ "vaultwarden.service" "postgresql.service" ];
restartUnits = [
"vaultwarden.service"
"postgresql.service"
];
};
};
};
@ -63,7 +75,8 @@ in
smtp = lib.mkOption {
description = "SMTP options.";
default = null;
type = lib.types.nullOr (lib.types.submodule {
type = lib.types.nullOr (
lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
@ -80,7 +93,11 @@ in
description = "SMTP host to send the emails to.";
};
security = lib.mkOption {
type = lib.types.enum [ "starttls" "force_tls" "off" ];
type = lib.types.enum [
"starttls"
"force_tls"
"off"
];
description = "Security expected by SMTP host.";
default = "starttls";
};
@ -109,7 +126,8 @@ in
};
};
};
});
}
);
};
mount = lib.mkOption {
@ -127,14 +145,16 @@ in
```
'';
readOnly = true;
default = { path = dataFolder; };
default = {
path = dataFolder;
};
};
backup = lib.mkOption {
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "vaultwarden";
@ -170,7 +190,8 @@ in
ROCKET_LOG = if cfg.debug then "trace" else "info";
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = cfg.port;
} // lib.optionalAttrs (cfg.smtp != null) {
}
// lib.optionalAttrs (cfg.smtp != null) {
SMTP_FROM = cfg.smtp.from_address;
SMTP_FROM_NAME = cfg.smtp.from_name;
SMTP_HOST = cfg.smtp.host;
@ -189,12 +210,12 @@ in
];
# Needed to be able to write template config.
systemd.services.vaultwarden.serviceConfig.ProtectHome = lib.mkForce false;
systemd.services.vaultwarden.preStart =
lib.shb.replaceSecrets {
systemd.services.vaultwarden.preStart = lib.shb.replaceSecrets {
userConfig = {
DATABASE_URL.source = cfg.databasePassword.result.path;
DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden";
} // lib.optionalAttrs (cfg.smtp != null) {
}
// lib.optionalAttrs (cfg.smtp != null) {
SMTP_PASSWORD.source = cfg.smtp.password.result.path;
};
resultPath = "${dataFolder}/vaultwarden.env";
@ -203,13 +224,18 @@ in
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain authEndpoint ssl;
inherit (cfg)
subdomain
domain
authEndpoint
ssl
;
upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}";
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
{
domain = "${fqdn}";
policy = "two_factor";
subject = ["group:vaultwarden_admin"];
subject = [ "group:vaultwarden_admin" ];
resources = [
"^/admin"
];

View file

@ -8,7 +8,9 @@ in
basic = lib.shb.runNixOSTest {
name = "authelia-basic";
nodes.machine = { config, pkgs, ... }: {
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -56,8 +58,10 @@ in
ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
identityProvidersOIDCHMACSecret.result =
config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result =
config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
};
oidcClients = [
@ -102,10 +106,12 @@ in
};
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = {
request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
settings.source = (pkgs.runCommand "gen-private-key" {} ''
settings.source =
(pkgs.runCommand "gen-private-key" { } ''
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem";
'')
+ "/private.pem";
};
specialisation = {
@ -115,7 +121,8 @@ in
};
};
testScript = { nodes, ... }:
testScript =
{ nodes, ... }:
let
specializations = "${nodes.machine.system.build.toplevel}/specialisation";
in

View file

@ -24,7 +24,9 @@ in
configWithTemplates = lib.shb.withReplacements userConfig;
nonSecretConfigFile = pkgs.writeText "config.yaml.template" (lib.generators.toJSON {} configWithTemplates);
nonSecretConfigFile = pkgs.writeText "config.yaml.template" (
lib.generators.toJSON { } configWithTemplates
);
replacements = lib.shb.getReplacements userConfig;
@ -37,24 +39,25 @@ in
replaceInTemplateJSON = lib.shb.replaceSecrets {
inherit userConfig;
resultPath = "/var/lib/config.json";
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json {});
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json { });
};
replaceInTemplateJSONGen = lib.shb.replaceSecrets {
inherit userConfig;
resultPath = "/var/lib/config_gen.json";
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toJSON {});
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toJSON { });
};
replaceInTemplateXML = lib.shb.replaceSecrets {
inherit userConfig;
resultPath = "/var/lib/config.xml";
generator = lib.shb.replaceSecretsFormatAdapter (lib.shb.formatXML {enclosingRoot = "Root";});
generator = lib.shb.replaceSecretsFormatAdapter (lib.shb.formatXML { enclosingRoot = "Root"; });
};
in
lib.shb.runNixOSTest {
name = "lib-template";
nodes.machine = { config, pkgs, ... }:
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
@ -62,7 +65,12 @@ in
{
options = {
libtest.config = lib.mkOption {
type = lib.types.attrsOf (lib.types.oneOf [ lib.types.str lib.secretFileType ]);
type = lib.types.attrsOf (
lib.types.oneOf [
lib.types.str
lib.secretFileType
]
);
};
};
}
@ -76,7 +84,9 @@ in
};
};
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
import json
from collections import ChainMap
from xml.etree import ElementTree
@ -95,7 +105,7 @@ in
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
return {root.tag: dict(merged_dict)}
wantedConfig = json.loads('${lib.generators.toJSON {} wantedConfig}')
wantedConfig = json.loads('${lib.generators.toJSON { } wantedConfig}')
with subtest("config"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}"))

View file

@ -9,7 +9,9 @@ in
auth = lib.shb.runNixOSTest {
name = "ldap-auth";
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -38,7 +40,7 @@ in
};
ensureGroups = {
"family" = {};
"family" = { };
};
};
shb.hardcodedsecret.ldapUserPassword = {
@ -65,10 +67,11 @@ in
};
};
nodes.client = {};
nodes.client = { };
# Inspired from https://github.com/lldap/lldap/blob/33f50d13a2e2d24a3e6bb05a148246bc98090df0/example_configs/lldap-ha-auth.sh
testScript = { nodes, ... }:
testScript =
{ nodes, ... }:
let
specializations = "${nodes.server.system.build.toplevel}/specialisation";
in

View file

@ -1,12 +1,17 @@
{ pkgs, lib, ... }:
let
serve = port: text: lib.getExe (pkgs.writers.writePython3Bin "serve"
serve =
port: text:
lib.getExe (
pkgs.writers.writePython3Bin "serve"
{
libraries = [ pkgs.python3Packages.systemd ];
}
(let
(
let
content = pkgs.writeText "content" text;
in ''
in
''
from http.server import BaseHTTPRequestHandler, HTTPServer
from systemd.daemon import notify
@ -35,14 +40,17 @@ let
print("Serving hardcoded page on http://127.0.0.1:${toString port}")
notify('READY=1')
httpd.serve_forever()
'')
''
)
);
in
{
default = lib.shb.runNixOSTest {
name = "mitmdump-default";
nodes.machine = { config, pkgs, ... }: {
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
../../modules/blocks/mitmdump.nix
];
@ -79,12 +87,15 @@ in
after = [ "test2.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [
"--set" "verbose_pattern=/verbose"
"--set"
"verbose_pattern=/verbose"
];
};
};
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("test1.service")

View file

@ -6,7 +6,9 @@ in
peerWithoutUser = lib.shb.runNixOSTest {
name = "postgresql-peerWithoutUser";
nodes.machine = { config, pkgs, ... }: {
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -21,7 +23,9 @@ in
];
};
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
@ -43,7 +47,9 @@ in
peerAuth = lib.shb.runNixOSTest {
name = "postgresql-peerAuth";
nodes.machine = { config, pkgs, ... }: {
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -55,7 +61,7 @@ in
group = "me";
extraGroups = [ "sudoers" ];
};
users.groups.me = {};
users.groups.me = { };
shb.postgresql.ensures = [
{
@ -65,7 +71,9 @@ in
];
};
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
@ -93,7 +101,9 @@ in
tcpIPWithoutPasswordAuth = lib.shb.runNixOSTest {
name = "postgresql-tcpIpWithoutPasswordAuth";
nodes.machine = { config, pkgs, ... }: {
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -109,7 +119,9 @@ in
];
};
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
@ -131,7 +143,9 @@ in
tcpIPPasswordAuth = lib.shb.runNixOSTest {
name = "postgresql-tcpIPPasswordAuth";
nodes.machine = { config, pkgs, ... }: {
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -143,7 +157,7 @@ in
group = "me";
extraGroups = [ "sudoers" ];
};
users.groups.me = {};
users.groups.me = { };
system.activationScripts.secret = ''
echo secretpw > /run/dbsecret
@ -158,7 +172,9 @@ in
];
};
testScript = { nodes, ... }: ''
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)

View file

@ -1,11 +1,15 @@
{ pkgs, lib, ... }:
let
testLib = pkgs.callPackage ../common.nix {};
testLib = pkgs.callPackage ../common.nix { };
commonTest = user: lib.shb.runNixOSTest {
commonTest =
user:
lib.shb.runNixOSTest {
name = "restic_backupAndRestore_${user}";
nodes.machine = { config, ... }: {
nodes.machine =
{ config, ... }:
{
imports = [
testLib.baseImports
@ -64,7 +68,8 @@ let
"/opt/files/B"
];
hooks.beforeBackup = [''
hooks.beforeBackup = [
''
echo $RUNTIME_DIRECTORY
if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then
if ! [ -f /run/secrets_restic/restic-backups-testinstance_opt_repos_A ]; then
@ -79,7 +84,8 @@ let
exit 12
fi
fi
''];
''
];
};
};
};
@ -87,11 +93,14 @@ let
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
testScript = { nodes, ... }: let
testScript =
{ nodes, ... }:
let
provider = nodes.machine.shb.restic.instances."testinstance";
backupService = provider.result.backupService;
restoreScript = provider.result.restoreScript;
in ''
in
''
from dictdiffer import diff
def list_files(dir):

View file

@ -6,7 +6,9 @@ in
test = lib.shb.runNixOSTest {
name = "ssl-test";
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -24,8 +26,8 @@ in
};
};
users.groups = {
group1 = {};
group2 = {};
group1 = { };
group2 = { };
};
shb.certs = {
@ -54,7 +56,10 @@ in
ca = config.shb.certs.cas.selfsigned.myca;
domain = "multi1.example.com";
extraDomains = [ "multi2.example.com" "multi3.example.com" ];
extraDomains = [
"multi2.example.com"
"multi3.example.com"
];
group = "nginx";
};
@ -121,7 +126,8 @@ in
};
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
testScript = { nodes, ... }:
testScript =
{ nodes, ... }:
let
myca = nodes.server.shb.certs.cas.selfsigned.myca;
myotherca = nodes.server.shb.certs.cas.selfsigned.myotherca;

View file

@ -1,7 +1,14 @@
{ pkgs, lib }:
let
inherit (lib) hasAttr mkOption optionalString;
inherit (lib.types) bool enum listOf nullOr submodule str;
inherit (lib.types)
bool
enum
listOf
nullOr
submodule
str
;
baseImports = {
imports = [
@ -10,15 +17,17 @@ let
];
};
accessScript = lib.makeOverridable ({
hasSSL
, waitForServices ? s: []
, waitForPorts ? p: []
, waitForUnixSocket ? u: []
, waitForUrls ? u: []
, extraScript ? {...}: ""
, redirectSSO ? false
}: { nodes, ... }:
accessScript = lib.makeOverridable (
{
hasSSL,
waitForServices ? s: [ ],
waitForPorts ? p: [ ],
waitForUnixSocket ? u: [ ],
waitForUrls ? u: [ ],
extraScript ? { ... }: "",
redirectSSO ? false,
}:
{ nodes, ... }:
let
cfg = nodes.server.test;
@ -72,9 +81,13 @@ let
+ lib.strings.concatMapStrings (p: ''server.wait_for_open_port(${toString p})'' + "\n") (
waitForPorts args
# TODO: when the SSO block exists, replace this hardcoded port.
++ (lib.optionals autheliaEnabled [ 9091 /* nodes.server.services.authelia.instances."auth.${domain}".settings.server.port */ ])
++ (lib.optionals autheliaEnabled [
9091 # nodes.server.services.authelia.instances."auth.${domain}".settings.server.port
])
)
+ lib.strings.concatMapStrings (u: ''server.wait_for_open_unix_socket("${u}")'' + "\n") (
waitForUnixSocket args
)
+ lib.strings.concatMapStrings (u: ''server.wait_for_open_unix_socket("${u}")'' + "\n") (waitForUnixSocket args)
+ ''
if ${if hasSSL args then "True" else "False"}:
server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
@ -84,7 +97,9 @@ let
''
# Making a curl request to an URL needs to happen after we copied the certificates over,
# otherwise curl will not be able to verify the "legitimacy of the server".
+ lib.strings.concatMapStrings (u: ''
+ lib.strings.concatMapStrings (
u:
''
import time
done = False
@ -97,16 +112,20 @@ let
done = response.get('code') == 200
if not done:
raise Exception(f"Response was never 200, got last: {response}")
'' + "\n") (
waitForUrls args
)
+ (if (! redirectSSO) then ''
''
+ "\n"
) (waitForUrls args)
+ (
if (!redirectSSO) then
''
with subtest("access"):
response = curl(client, """{"code":%{response_code}}""", "${proto_fqdn}")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
'' else ''
''
else
''
with subtest("unauthenticated access is not granted"):
response = curl(client, """{"code":%{response_code},"auth_host":"%{urle.host}","auth_query":"%{urle.query}","all":%{json}}""", "${proto_fqdn}")
@ -116,11 +135,14 @@ let
raise Exception(f"auth host should be auth.${cfg.domain} but is {response['auth_host']}")
if response['auth_query'] != "rd=${proto_fqdn}/":
raise Exception(f"auth query should be rd=${proto_fqdn}/ but is {response['auth_query']}")
'')
+ (let
''
)
+ (
let
script = extraScript args;
in
lib.optionalString (script != "") script)
lib.optionalString (script != "") script
)
+ (optionalString (hasAttr "test" nodes.server && hasAttr "login" nodes.server.test) ''
with subtest("Login from server"):
code, logs = server.execute("login_playwright")
@ -145,8 +167,12 @@ let
'')
);
backupScript = args: (accessScript args).override {
extraScript = { proto_fqdn, ... }: ''
backupScript =
args:
(accessScript args).override {
extraScript =
{ proto_fqdn, ... }:
''
with subtest("backup"):
server.succeed("systemctl start restic-backups-testinstance_opt_repos_A")
'';
@ -155,17 +181,23 @@ in
{
inherit baseImports accessScript;
runNixOSTest = args: pkgs.testers.runNixOSTest ({
interactive.sshBackdoor.enable = true;
} // args);
mkScripts = args:
runNixOSTest =
args:
pkgs.testers.runNixOSTest (
{
interactive.sshBackdoor.enable = true;
}
// args
);
mkScripts = args: {
access = accessScript args;
backup = backupScript args;
};
baseModule = { config, ... }: {
baseModule =
{ config, ... }:
{
options.test = {
domain = mkOption {
type = str;
@ -204,21 +236,34 @@ in
];
config = {
# HTTP(s) server port.
networking.firewall.allowedTCPPorts = [ 80 443 ];
networking.firewall.allowedTCPPorts = [
80
443
];
shb.nginx.accessLog = true;
networking.hosts = {
"192.168.1.2" = [ config.test.fqdn "auth.${config.test.domain}" ];
"192.168.1.2" = [
config.test.fqdn
"auth.${config.test.domain}"
];
};
};
};
clientLoginModule = { config, pkgs, ... }: let
clientLoginModule =
{ config, pkgs, ... }:
let
cfg = config.test.login;
in {
in
{
options.test.login = {
browser = mkOption {
type = enum [ "firefox" "chromium" "webkit" ];
type = enum [
"firefox"
"chromium"
"webkit"
];
default = "firefox";
};
usernameFieldLabelRegex = mkOption {
@ -269,7 +314,10 @@ in
};
config = {
networking.hosts = {
"192.168.1.2" = [ config.test.fqdn "auth.${config.test.domain}" ];
"192.168.1.2" = [
config.test.fqdn
"auth.${config.test.domain}"
];
};
environment.variables = {
@ -280,11 +328,16 @@ in
(pkgs.writers.writePython3Bin "login_playwright"
{
libraries = [ pkgs.python3Packages.playwright ];
flakeIgnore = [ "F401" "E501" ];
flakeIgnore = [
"F401"
"E501"
];
}
(let
(
let
testCfg = pkgs.writeText "users.json" (builtins.toJSON cfg);
in ''
in
''
import json
import re
import sys
@ -343,13 +396,17 @@ in
context.tracing.stop(path=f"trace/{i}.zip")
browser.close()
'')
''
)
)
];
};
};
backup = backupOption: { config, ... }: {
backup =
backupOption:
{ config, ... }:
{
imports = [
../modules/blocks/restic.nix
];
@ -373,7 +430,9 @@ in
};
};
certs = { config, ... }: {
certs =
{ config, ... }:
{
imports = [
../modules/blocks/ssl.nix
];
@ -395,7 +454,9 @@ in
systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
};
ldap = { config, pkgs, ... }: {
ldap =
{ config, pkgs, ... }:
{
imports = [
../modules/blocks/lldap.nix
];
@ -432,7 +493,10 @@ in
};
bob = {
email = "bob@example.com";
groups = [ "user_group" "admin_group" ];
groups = [
"user_group"
"admin_group"
];
password.result.path = pkgs.writeText "bobPassword" "BobPassword";
};
charlie = {
@ -443,14 +507,17 @@ in
};
ensureGroups = {
user_group = {};
admin_group = {};
other_group = {};
user_group = { };
admin_group = { };
other_group = { };
};
};
};
sso = ssl: { config, pkgs, ... }: {
sso =
ssl:
{ config, pkgs, ... }:
{
imports = [
../modules/blocks/authelia.nix
];
@ -475,8 +542,10 @@ in
ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
identityProvidersOIDCHMACSecret.result =
config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result =
config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
};
};
@ -502,10 +571,12 @@ in
};
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = {
request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
settings.source = (pkgs.runCommand "gen-private-key" {} ''
settings.source =
(pkgs.runCommand "gen-private-key" { } ''
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem";
'')
+ "/private.pem";
};
};

View file

@ -1,17 +1,24 @@
{ pkgs, ... }:
let
contracts = pkgs.callPackage ../../modules/contracts {};
contracts = pkgs.callPackage ../../modules/contracts { };
in
{
restic_root = contracts.test.backup {
name = "restic_root";
username = "root";
providerRoot = [ "shb" "restic" "instances" "mytest" ];
providerRoot = [
"shb"
"restic"
"instances"
"mytest"
];
modules = [
../../modules/blocks/restic.nix
../../modules/blocks/hardcodedsecret.nix
];
settings = { repository, config, ... }: {
settings =
{ repository, config, ... }:
{
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
@ -21,7 +28,9 @@ in
};
};
};
extraConfig = { username, config, ... }: {
extraConfig =
{ username, config, ... }:
{
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."mytest".settings.passphrase.request;
settings.content = "passphrase";
@ -32,12 +41,19 @@ in
restic_nonroot = contracts.test.backup {
name = "restic_nonroot";
username = "me";
providerRoot = [ "shb" "restic" "instances" "mytest" ];
providerRoot = [
"shb"
"restic"
"instances"
"mytest"
];
modules = [
../../modules/blocks/restic.nix
../../modules/blocks/hardcodedsecret.nix
];
settings = { repository, config, ... }: {
settings =
{ repository, config, ... }:
{
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
@ -47,7 +63,9 @@ in
};
};
};
extraConfig = { username, config, ... }: {
extraConfig =
{ username, config, ... }:
{
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."mytest".settings.passphrase.request;
settings.content = "passphrase";

View file

@ -1,18 +1,29 @@
{ pkgs, ... }:
let
contracts = pkgs.callPackage ../../modules/contracts {};
contracts = pkgs.callPackage ../../modules/contracts { };
in
{
restic_postgres = contracts.test.databasebackup {
name = "restic_postgres";
requesterRoot = [ "shb" "postgresql" "databasebackup" ];
providerRoot = [ "shb" "restic" "databases" "postgresql" ];
requesterRoot = [
"shb"
"postgresql"
"databasebackup"
];
providerRoot = [
"shb"
"restic"
"databases"
"postgresql"
];
modules = [
../../modules/blocks/postgresql.nix
../../modules/blocks/restic.nix
../../modules/blocks/hardcodedsecret.nix
];
settings = { repository, config, ... }: {
settings =
{ repository, config, ... }:
{
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
@ -22,7 +33,9 @@ in
};
};
};
extraConfig = { config, database, ... }: {
extraConfig =
{ config, database, ... }:
{
shb.postgresql.ensures = [
{
inherit database;

View file

@ -1,12 +1,15 @@
{ pkgs, ... }:
let
contracts = pkgs.callPackage ../../modules/contracts {};
contracts = pkgs.callPackage ../../modules/contracts { };
in
{
hardcoded_root_root = contracts.test.secret {
name = "hardcoded";
modules = [ ../../modules/blocks/hardcodedsecret.nix ];
configRoot = [ "shb" "hardcodedsecret" ];
configRoot = [
"shb"
"hardcodedsecret"
];
settingsCfg = secret: {
content = secret;
};
@ -15,7 +18,10 @@ in
hardcoded_user_group = contracts.test.secret {
name = "hardcoded";
modules = [ ../../modules/blocks/hardcodedsecret.nix ];
configRoot = [ "shb" "hardcodedsecret" ];
configRoot = [
"shb"
"hardcodedsecret"
];
settingsCfg = secret: {
content = secret;
};

View file

@ -1,26 +1,31 @@
{ pkgs, lib, ... }:
let
anyOpt = default: lib.mkOption {
anyOpt =
default:
lib.mkOption {
type = lib.types.anything;
inherit default;
};
testConfig = m:
testConfig =
m:
let
cfg = (lib.evalModules {
cfg =
(lib.evalModules {
specialArgs = { inherit pkgs; };
modules = [
{
options = {
systemd = anyOpt {};
services = anyOpt {};
systemd = anyOpt { };
services = anyOpt { };
};
}
../../modules/blocks/davfs.nix
m
];
}).config;
in {
in
{
inherit (cfg) systemd services;
};
in
@ -28,8 +33,8 @@ in
testDavfsNoOptions = {
expected = {
services.davfs2.enable = false;
systemd.mounts = [];
systemd.mounts = [ ];
};
expr = testConfig {};
expr = testConfig { };
};
}

View file

@ -17,7 +17,8 @@ in
c = "%SECRET_${root}C%";
};
in
(item "") // {
(item "")
// {
nestedAttr = item "NESTEDATTR_";
nestedList = [ (item "NESTEDLIST_0_") ];
doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ];
@ -34,7 +35,8 @@ in
};
in
lib.shb.withReplacements (
item // {
item
// {
nestedAttr = item;
nestedList = [ item ];
doubleNestedList = [ { n = item; } ];
@ -84,10 +86,10 @@ in
(nameValuePair "%SECRET_${root}C%" "prefix-$(cat /path/C)-suffix")
];
in
(secrets "") ++
(secrets "DOUBLENESTEDLIST_0_N_") ++
(secrets "NESTEDATTR_") ++
(secrets "NESTEDLIST_0_");
(secrets "")
++ (secrets "DOUBLENESTEDLIST_0_N_")
++ (secrets "NESTEDATTR_")
++ (secrets "NESTEDLIST_0_");
expr =
let
item = {
@ -99,13 +101,16 @@ in
c.other = "other";
};
in
map lib.shb.genReplacement (lib.shb.getReplacements (
item // {
map lib.shb.genReplacement (
lib.shb.getReplacements (
item
// {
nestedAttr = item;
nestedList = [ item ];
doubleNestedList = [ { n = item; } ];
}
));
)
);
};
testParseXML = {

View file

@ -4,20 +4,34 @@ let
loginUrl = "/UI/Login";
# TODO: Test login
commonTestScript = appname: cfgPathFn: lib.shb.mkScripts {
commonTestScript =
appname: cfgPathFn:
lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.arr.${appname}.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"${appname}.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
node.config.shb.arr.${appname}.settings.Port
];
extraScript = { node, fqdn, proto_fqdn, ... }: let
extraScript =
{
node,
fqdn,
proto_fqdn,
...
}:
let
shbapp = node.config.shb.arr.${appname};
cfgPath = cfgPathFn shbapp;
apiKey = if (shbapp.settings ? ApiKey) then "01234567890123456789" else null;
in ''
in
''
# These curl requests still return a 200 even with sso redirect.
with subtest("health"):
response = curl(client, """{"code":%{response_code}}""", "${fqdn}${healthUrl}")
@ -31,7 +45,8 @@ let
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
'' + lib.optionalString (apiKey != null) ''
''
+ lib.optionalString (apiKey != null) ''
with subtest("apikey"):
config = server.succeed("cat ${cfgPath}")
@ -40,7 +55,10 @@ let
'';
};
basic = appname: { config, ... }: {
basic =
appname:
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/arr.nix
@ -58,7 +76,10 @@ let
};
};
clientLogin = appname: { config, ... }: {
clientLogin =
appname:
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -74,14 +95,18 @@ let
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{ nextPageExpect = [
{
nextPageExpect = [
"expect(page).to_have_title(re.compile('${appname}', re.IGNORECASE))"
]; }
];
}
];
};
};
basicTest = appname: cfgPathFn: lib.shb.runNixOSTest {
basicTest =
appname: cfgPathFn:
lib.shb.runNixOSTest {
name = "arr_${appname}_basic";
nodes.client = {
@ -98,31 +123,42 @@ let
testScript = (commonTestScript appname cfgPathFn).access;
};
backupTest = appname: cfgPathFn: lib.shb.runNixOSTest {
backupTest =
appname: cfgPathFn:
lib.shb.runNixOSTest {
name = "arr_${appname}_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
(basic appname)
(lib.shb.backup config.shb.arr.${appname}.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = (commonTestScript appname cfgPathFn).backup;
};
https = appname: { config, ...}: {
https =
appname:
{ config, ... }:
{
shb.arr.${appname} = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
httpsTest = appname: cfgPathFn: lib.shb.runNixOSTest {
httpsTest =
appname: cfgPathFn:
lib.shb.runNixOSTest {
name = "arr_${appname}_https";
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
(basic appname)
lib.shb.certs
@ -130,21 +166,28 @@ let
];
};
nodes.client = {};
nodes.client = { };
testScript = (commonTestScript appname cfgPathFn).access;
};
sso = appname: { config, ...}: {
sso =
appname:
{ config, ... }:
{
shb.arr.${appname} = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
};
ssoTest = appname: cfgPathFn: lib.shb.runNixOSTest {
ssoTest =
appname: cfgPathFn:
lib.shb.runNixOSTest {
name = "arr_${appname}_sso";
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
(basic appname)
lib.shb.certs
@ -155,7 +198,7 @@ let
];
};
nodes.client = {};
nodes.client = { };
testScript = (commonTestScript appname cfgPathFn).access.override {
redirectSSO = true;

View file

@ -2,11 +2,15 @@
let
commonTestScript = lib.shb.accessScript {
hasSSL = { node, ... }: !(isNull node.config.shb.audiobookshelf.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"audiobookshelf.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
node.config.shb.audiobookshelf.webPort
];
# TODO: Test login
@ -14,7 +18,9 @@ let
# '';
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/audiobookshelf.nix
@ -29,7 +35,9 @@ let
};
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -47,9 +55,13 @@ let
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
# Failure is after so we're not throttled too much.
{ username = "root"; password = "rootpw"; nextPageExpect = [
{
username = "root";
password = "rootpw";
nextPageExpect = [
"expect(page.get_by_text('Wrong username or password')).to_be_visible()"
]; }
];
}
# { username = adminUser; password = adminPass; nextPageExpect = [
# "expect(page.get_by_text('Wrong username or password')).not_to_be_visible()"
# "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
@ -59,19 +71,24 @@ let
};
};
https = { config, ... }: {
https =
{ config, ... }:
{
shb.audiobookshelf = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.audiobookshelf = {
sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result = config.shb.hardcodedsecret.audiobookshelfSSOPassword.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia.result;
sharedSecretForAuthelia.result =
config.shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia.result;
};
};
@ -116,7 +133,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};
@ -124,7 +141,9 @@ in
sso = lib.shb.runNixOSTest {
name = "audiobookshelf-sso";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
lib.shb.certs
@ -135,7 +154,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};

View file

@ -2,16 +2,22 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.deluge.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"nginx.service"
"deluged.service"
"delugeweb.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
node.config.shb.deluge.daemonPort
node.config.shb.deluge.webPort
];
extraScript = { node, proto_fqdn, ... }: ''
extraScript =
{ node, proto_fqdn, ... }:
''
print(${node.name}.succeed('journalctl -n100 -u deluged'))
print(${node.name}.succeed('systemctl status deluged'))
print(${node.name}.succeed('systemctl status delugeweb'))
@ -54,7 +60,8 @@ let
'';
};
prometheusTestScript = { nodes, ... }:
prometheusTestScript =
{ nodes, ... }:
''
server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.deluge.port})
with subtest("prometheus"):
@ -65,7 +72,9 @@ let
print(response)
'';
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
@ -96,7 +105,9 @@ let
};
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -109,19 +120,27 @@ let
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "Login";
testLoginWith = [
{ password = "deluge"; nextPageExpect = [
{
password = "deluge";
nextPageExpect = [
"expect(page.get_by_role('button', name='Login')).not_to_be_visible()"
"expect(page.get_by_text('Login Failed')).not_to_be_visible()"
]; }
{ password = "other"; nextPageExpect = [
];
}
{
password = "other";
nextPageExpect = [
"expect(page.get_by_role('button', name='Login')).to_be_visible()"
"expect(page.get_by_text('Login Failed')).to_be_visible()"
]; }
];
}
];
};
};
prometheus = { config, ... }: {
prometheus =
{ config, ... }:
{
shb.deluge = {
prometheusScraperPassword.result = config.shb.hardcodedsecret."scraper".result;
};
@ -131,13 +150,17 @@ let
};
};
https = { config, ...}: {
https =
{ config, ... }:
{
shb.deluge = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.deluge = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
@ -164,14 +187,16 @@ in
backup = lib.shb.runNixOSTest {
name = "deluge_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.deluge.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -187,7 +212,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -195,7 +220,9 @@ in
sso = lib.shb.runNixOSTest {
name = "deluge_sso";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
lib.shb.certs
@ -206,7 +233,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access.override {
redirectSSO = true;
@ -225,10 +252,8 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = inputs:
(commonTestScript.access inputs)
+ (prometheusTestScript inputs);
testScript = inputs: (commonTestScript.access inputs) + (prometheusTestScript inputs);
};
}

View file

@ -4,20 +4,28 @@ let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.forgejo.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"forgejo.service"
"nginx.service"
];
waitForUnixSocket = { node, ... }: [
waitForUnixSocket =
{ node, ... }:
[
node.config.services.forgejo.settings.server.HTTP_ADDR
];
extraScript = { node, ... }: ''
extraScript =
{ node, ... }:
''
server.wait_for_unit("gitea-runner-local.service", timeout=10)
server.succeed("journalctl -o cat -u gitea-runner-local.service | grep -q 'Runner registered successfully'")
'';
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
@ -67,7 +75,9 @@ let
};
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -82,33 +92,53 @@ let
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "theadmin"; password = adminPassword + "oops"; nextPageExpect = [
{
username = "theadmin";
password = adminPassword + "oops";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "theadmin"; password = adminPassword; nextPageExpect = [
];
}
{
username = "theadmin";
password = adminPassword;
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "theuser"; password = "userPasswordOops"; nextPageExpect = [
];
}
{
username = "theuser";
password = "userPasswordOops";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "theuser"; password = "userPassword"; nextPageExpect = [
];
}
{
username = "theuser";
password = "userPassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
];
}
];
};
};
https = { config, ... }: {
https =
{ config, ... }:
{
shb.forgejo = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.forgejo = {
ldap = {
enable = true;
@ -128,7 +158,9 @@ let
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.forgejo = {
sso = {
enable = true;
@ -170,14 +202,16 @@ in
backup = lib.shb.runNixOSTest {
name = "forgejo_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.forgejo.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -193,7 +227,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -211,7 +245,9 @@ in
nodes.client = {
imports = [
({ config, ... }: {
(
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -227,31 +263,56 @@ in
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
];
}
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
];
}
];
};
})
}
)
];
};
@ -261,7 +322,9 @@ in
sso = lib.shb.runNixOSTest {
name = "forgejo_sso";
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs
@ -272,7 +335,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};

View file

@ -2,16 +2,22 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.grocy.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"phpfpm-grocy.service"
"nginx.service"
];
waitForUnixSocket = { node, ... }: [
waitForUnixSocket =
{ node, ... }:
[
node.config.services.phpfpm.pools.grocy.socket
];
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/grocy.nix
@ -27,7 +33,9 @@ let
};
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -44,19 +52,29 @@ let
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "OK";
testLoginWith = [
{ username = "admin"; password = "admin oops"; nextPageExpect = [
{
username = "admin";
password = "admin oops";
nextPageExpect = [
"expect(page.get_by_text('Invalid credentials, please try again')).to_be_visible()"
]; }
{ username = "admin"; password = "admin"; nextPageExpect = [
];
}
{
username = "admin";
password = "admin";
nextPageExpect = [
"expect(page.get_by_text('Invalid credentials, please try again')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('OK'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Grocy'))"
]; }
];
}
];
};
};
https = { config, ...}: {
https =
{ config, ... }:
{
shb.grocy = {
ssl = config.shb.certs.certs.selfsigned.n;
};
@ -91,7 +109,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};

View file

@ -2,13 +2,17 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.hledger.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"hledger-web.service"
"nginx.service"
];
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/hledger.nix
@ -24,7 +28,9 @@ let
};
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -37,20 +43,26 @@ let
test.login = {
startUrl = "http://${config.test.fqdn}";
testLoginWith = [
{ nextPageExpect = [
{
nextPageExpect = [
"expect(page).to_have_title('journal - hledger-web')"
]; }
];
}
];
};
};
https = { config, ... }: {
https =
{ config, ... }:
{
shb.hledger = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.hledger = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
@ -78,14 +90,16 @@ in
backup = lib.shb.runNixOSTest {
name = "hledger_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.hledger.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -101,7 +115,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -109,7 +123,9 @@ in
sso = lib.shb.runNixOSTest {
name = "hledger_sso";
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs
@ -120,7 +136,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access.override {
redirectSSO = true;

View file

@ -2,16 +2,22 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.home-assistant.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"home-assistant.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
8123
];
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/home-assistant.nix
@ -36,7 +42,9 @@ let
};
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -50,7 +58,8 @@ let
test.login = {
startUrl = "http://${config.test.fqdn}";
testLoginWith = [
{ nextPageExpect = [
{
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Create my smart home')).click()"
"expect(page.get_by_text('Create user')).to_be_visible()"
@ -64,18 +73,23 @@ let
"page.get_by_role('button', name=re.compile('Finish')).click()"
"expect(page).to_have_title(re.compile('Overview'), timeout=15000)"
]; }
];
}
];
};
};
https = { config, ...}: {
https =
{ config, ... }:
{
shb.home-assistant = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.home-assistant = {
ldap = {
enable = true;
@ -95,7 +109,9 @@ let
# };
# };
voice = { config, ... }: {
voice =
{ config, ... }:
{
# For now, verifying the packages can build is good enough.
environment.systemPackages = [
config.services.wyoming.piper.package
@ -169,14 +185,16 @@ in
backup = lib.shb.runNixOSTest {
name = "homeassistant_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.home-assistant.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -192,7 +210,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -208,7 +226,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -243,7 +261,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};

View file

@ -5,12 +5,25 @@ let
commonTestScript = lib.shb.accessScript {
hasSSL = { node, ... }: !(isNull node.config.shb.immich.ssl);
waitForServices = { ... }: [ "immich-server.service" "postgresql.service" "nginx.service" ];
waitForPorts = { ... }: [ 2283 80 ];
waitForServices =
{ ... }:
[
"immich-server.service"
"postgresql.service"
"nginx.service"
];
waitForPorts =
{ ... }:
[
2283
80
];
waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}" ];
};
base = { config, ... }: {
base =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/immich.nix
@ -34,13 +47,17 @@ let
environment.systemPackages = [ pkgs.curl ];
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [ base ];
test.hasSSL = false;
};
https = { config, ... }: {
https =
{ config, ... }:
{
imports = [
base
lib.shb.certs
@ -50,14 +67,18 @@ let
shb.immich.ssl = config.shb.certs.certs.selfsigned.n;
};
backup = { config, ... }: {
backup =
{ config, ... }:
{
imports = [
https
(lib.shb.backup config.shb.immich.backup)
];
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
imports = [
https
lib.shb.ldap
@ -85,7 +106,7 @@ let
};
# Configure LDAP groups for group-based access control
shb.lldap.ensureGroups.immich_user = {};
shb.lldap.ensureGroups.immich_user = { };
shb.lldap.ensureUsers.immich_test_user = {
email = "immich_user@example.com";
@ -115,7 +136,7 @@ in
name = "immich-basic";
nodes.server = basic;
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};
@ -124,7 +145,7 @@ in
name = "immich-https";
nodes.server = https;
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};
@ -133,12 +154,20 @@ in
name = "immich-backup";
nodes.server = backup;
nodes.client = {};
nodes.client = { };
testScript = (lib.shb.mkScripts {
testScript =
(lib.shb.mkScripts {
hasSSL = args: !(isNull args.node.config.shb.immich.ssl);
waitForServices = args: [ "immich-server.service" "postgresql.service" "nginx.service" ];
waitForPorts = args: [ 2283 80 ];
waitForServices = args: [
"immich-server.service"
"postgresql.service"
"nginx.service"
];
waitForPorts = args: [
2283
80
];
waitForUrls = args: [ "${args.proto_fqdn}" ];
}).backup;
};

View file

@ -4,17 +4,25 @@ let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"jellyfin.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
port
];
waitForUrls = { proto_fqdn, ... }: [
waitForUrls =
{ proto_fqdn, ... }:
[
"${proto_fqdn}/System/Info/Public"
];
extraScript = { node, ... }: ''
extraScript =
{ node, ... }:
''
headers = unline_with(" ", """
-H 'Content-Type: application/json'
-H 'Authorization: MediaBrowser Client="Android TV", Device="Nvidia Shield", DeviceId="ZQ9YQHHrUzk24vV", Version="0.15.3"'
@ -35,7 +43,9 @@ let
'';
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/jellyfin.nix
@ -65,7 +75,9 @@ let
];
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.clientLoginModule
];
@ -103,7 +115,9 @@ let
};
};
https = { config, ... }: {
https =
{ config, ... }:
{
shb.jellyfin = {
ssl = config.shb.certs.certs.selfsigned.n;
};
@ -112,7 +126,9 @@ let
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.jellyfin = {
ldap = {
enable = true;
@ -129,7 +145,9 @@ let
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.jellyfin = {
sso = {
enable = true;
@ -150,7 +168,10 @@ let
};
};
jellyfinTest = name: { nodes, testScript }: lib.shb.runNixOSTest {
jellyfinTest =
name:
{ nodes, testScript }:
lib.shb.runNixOSTest {
name = "jellyfin_${name}";
interactive.nodes.server = {
@ -174,20 +195,22 @@ in
# Client login does not work without SSL.
# At least, I couldn't make it work.
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
backup = jellyfinTest "backup" {
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.jellyfin.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -201,7 +224,9 @@ in
];
};
nodes.client = { config, lib, ... }: {
nodes.client =
{ config, lib, ... }:
{
imports = [
lib.shb.baseModule
clientLogin
@ -220,13 +245,15 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
sso = jellyfinTest "sso" {
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs
@ -237,7 +264,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};

View file

@ -3,23 +3,29 @@ let
nextauthSecret = "nextauthSecret";
oidcSecret = "oidcSecret";
testLib = pkgs.callPackage ../common.nix {};
testLib = pkgs.callPackage ../common.nix { };
commonTestScript = testLib.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.karakeep.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"karakeep-init.service"
"karakeep-browser.service"
"karakeep-web.service"
"karakeep-workers.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
node.config.shb.karakeep.port
];
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
testLib.baseModule
../../modules/blocks/hardcodedsecret.nix
@ -53,13 +59,17 @@ let
};
};
https = { config, ... }: {
https =
{ config, ... }:
{
shb.karakeep = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.karakeep = {
ldap = {
userGroup = "user_group";
@ -67,7 +77,9 @@ let
};
};
clientLoginSso = { config, ... }: {
clientLoginSso =
{ config, ... }:
{
imports = [
testLib.baseModule
testLib.clientLoginModule
@ -85,36 +97,62 @@ let
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
];
}
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('new item')).to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('new item')).to_be_visible()"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
# "page.get_by_role('button', name=re.compile('Accept')).click()" # I don't understand why this is not needed. Maybe it keeps somewhere the previous token?
"expect(page.get_by_text(re.compile('login failed'))).to_be_visible(timeout=10000)"
]; }
];
}
];
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.karakeep = {
sso = {
enable = true;
@ -140,7 +178,7 @@ in
basic = pkgs.testers.runNixOSTest {
name = "karakeep_basic";
nodes.client = {};
nodes.client = { };
nodes.server = {
imports = [
basic
@ -153,14 +191,16 @@ in
backup = pkgs.testers.runNixOSTest {
name = "karakeep_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(testLib.backup config.shb.karakeep.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -168,7 +208,7 @@ in
https = pkgs.testers.runNixOSTest {
name = "karakeep_https";
nodes.client = {};
nodes.client = { };
nodes.server = {
imports = [
basic
@ -191,7 +231,9 @@ in
virtualisation.memorySize = 4096;
};
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
testLib.certs

View file

@ -4,15 +4,21 @@ let
commonTestScript = lib.shb.accessScript {
hasSSL = { node, ... }: !(isNull node.config.shb.monitoring.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"grafana.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
node.config.shb.monitoring.grafanaPort
];
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
test = {
subdomain = "g";
};
@ -36,7 +42,9 @@ let
};
};
https = { config, ...}: {
https =
{ config, ... }:
{
shb.monitoring = {
ssl = config.shb.certs.certs.selfsigned.n;
};
@ -54,7 +62,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};
@ -72,7 +80,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};

View file

@ -6,14 +6,25 @@ let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.nextcloud.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"phpfpm-nextcloud.service"
"nginx.service"
];
waitForUnixSocket = { node, ... }: [
waitForUnixSocket =
{ node, ... }:
[
node.config.services.phpfpm.pools.nextcloud.socket
];
extraScript = { node, fqdn, proto_fqdn, ... }: ''
extraScript =
{
node,
fqdn,
proto_fqdn,
...
}:
''
with subtest("fails with incorrect authentication"):
client.fail(
"curl -f -s --location -X PROPFIND"
@ -87,7 +98,9 @@ let
'';
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/nextcloud-server.nix
@ -119,7 +132,9 @@ let
};
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -136,20 +151,30 @@ let
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{ username = adminUser; password = adminPass; nextPageExpect = [
{
username = adminUser;
password = adminPass;
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
];
}
# Failure is after so we're not throttled too much.
{ username = adminUser; password = adminPass + "oops"; nextPageExpect = [
{
username = adminUser;
password = adminPass + "oops";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
]; }
];
}
];
};
};
clientLdapLogin = { config, ... }: {
clientLdapLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -166,27 +191,45 @@ let
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
];
}
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
]; }
];
}
];
};
};
clientSsoLogin = { config, ... }: {
clientSsoLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -211,36 +254,62 @@ let
passwordFieldSelector = "get_by_label(\"Password *\")";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page).to_have_title(re.compile('Dashboard'))"
"page.goto('https://${config.test.fqdn}/settings/admin')"
"expect(page.get_by_text('Access forbidden')).to_be_visible()"
]; }
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
];
}
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page).to_have_title(re.compile('Dashboard'))"
"page.goto('https://${config.test.fqdn}/settings/admin')"
"expect(page.get_by_text('Access forbidden')).not_to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text('not member of the allowed groups')).to_be_visible()"
]; }
];
}
];
};
};
https = { config, ...}: {
https =
{ config, ... }:
{
shb.nextcloud = {
ssl = config.shb.certs.certs.selfsigned.n;
@ -248,7 +317,9 @@ let
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.nextcloud = {
apps.ldap = {
enable = true;
@ -266,7 +337,8 @@ let
};
};
sso = { config, ... }:
sso =
{ config, ... }:
{
shb.nextcloud = {
apps.ldap = {
@ -299,7 +371,9 @@ let
};
};
previewgenerator = { config, ...}: {
previewgenerator =
{ config, ... }:
{
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
@ -323,7 +397,9 @@ let
};
};
memories = { config, ...}: {
memories =
{ config, ... }:
{
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
@ -334,7 +410,9 @@ let
};
};
recognize = { config, ...}: {
recognize =
{ config, ... }:
{
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
@ -344,13 +422,16 @@ let
};
};
prometheus = { config, ... }: {
prometheus =
{ config, ... }:
{
shb.nextcloud = {
phpFpmPrometheusExporter.enable = true;
};
};
prometheusTestScript = { nodes, ... }:
prometheusTestScript =
{ nodes, ... }:
''
server.wait_for_open_unix_socket("${nodes.server.services.phpfpm.pools.nextcloud.socket}")
server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.php-fpm.port})
@ -389,10 +470,17 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access.override {
extraScript = { node, fqdn, proto_fqdn, ... }: ''
extraScript =
{
node,
fqdn,
proto_fqdn,
...
}:
''
import time
def find_in_logs(unit, text):
@ -419,14 +507,16 @@ in
backup = lib.shb.runNixOSTest {
name = "nextcloud_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.nextcloud.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -442,7 +532,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
# TODO: Test login
testScript = commonTestScript.access;
@ -460,7 +550,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -477,7 +567,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -514,7 +604,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -522,7 +612,9 @@ in
ldap = lib.shb.runNixOSTest {
name = "nextcloud_ldap";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
lib.shb.certs
@ -544,7 +636,9 @@ in
sso = lib.shb.runNixOSTest {
name = "nextcloud_sso";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
lib.shb.certs
@ -552,22 +646,28 @@ in
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
({ config, ... }: {
(
{ config, ... }:
{
networking.hosts = {
"127.0.0.1" = [ config.test.fqdn ];
};
})
}
)
];
};
nodes.client = {
imports = [
clientSsoLogin
({ config, ... }: {
(
{ config, ... }:
{
networking.hosts = {
"192.168.1.2" = [ config.test.fqdn ];
};
})
}
)
];
};
@ -577,14 +677,16 @@ in
prometheus = lib.shb.runNixOSTest {
name = "nextcloud_prometheus";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
prometheus
];
};
nodes.client = {};
nodes.client = { };
testScript = prometheusTestScript;
};

View file

@ -4,16 +4,22 @@ let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.open-webui.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"open-webui.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
node.config.shb.open-webui.port
];
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
@ -34,7 +40,9 @@ let
};
};
https = { config, ... }: {
https =
{ config, ... }:
{
shb.open-webui = {
ssl = config.shb.certs.certs.selfsigned.n;
};
@ -45,7 +53,9 @@ let
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.open-webui = {
ldap = {
userGroup = "user_group";
@ -54,7 +64,9 @@ let
};
};
clientLoginSso = { config, ... }: {
clientLoginSso =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -73,36 +85,62 @@ let
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
];
}
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('logged in')).to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('logged in')).to_be_visible()"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text('unauthorized')).to_be_visible()"
]; }
];
}
];
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.open-webui = {
sso = {
enable = true;
@ -128,7 +166,7 @@ in
basic = lib.shb.runNixOSTest {
name = "open-webui_basic";
nodes.client = {};
nodes.client = { };
nodes.server = {
imports = [
basic
@ -141,14 +179,16 @@ in
backup = lib.shb.runNixOSTest {
name = "open-webui_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.open-webui.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -156,7 +196,7 @@ in
https = lib.shb.runNixOSTest {
name = "open-webui_https";
nodes.client = {};
nodes.client = { };
nodes.server = {
imports = [
basic
@ -176,7 +216,9 @@ in
clientLoginSso
];
};
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs

View file

@ -2,16 +2,22 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.pinchflat.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"pinchflat.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
node.config.shb.pinchflat.port
];
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
@ -45,7 +51,9 @@ let
};
};
clientLogin = { config, ... }: {
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -58,20 +66,28 @@ let
startUrl = "http://${config.test.fqdn}";
# There is no login without SSO integration.
testLoginWith = [
{ username = null; password = null; nextPageExpect = [
{
username = null;
password = null;
nextPageExpect = [
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
];
}
];
};
};
https = { config, ... }: {
https =
{ config, ... }:
{
shb.pinchflat = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
ldap = { config, ... }: {
ldap =
{ config, ... }:
{
shb.pinchflat = {
ldap = {
enable = true;
@ -81,7 +97,9 @@ let
};
};
clientLoginSso = { config, ... }: {
clientLoginSso =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
@ -96,33 +114,59 @@ let
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
];
}
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
"expect(page).to_have_url(re.compile('.*/authenticated'))"
]; }
];
}
];
};
};
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.pinchflat = {
sso = {
enable = true;
@ -152,14 +196,16 @@ in
backup = lib.shb.runNixOSTest {
name = "pinchflat_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.pinchflat.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -191,7 +237,9 @@ in
clientLoginSso
];
};
nodes.server = { config, pkgs, ... }: {
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs

View file

@ -2,11 +2,15 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.vaultwarden.ssl);
waitForServices = { ... }: [
waitForServices =
{ ... }:
[
"vaultwarden.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
8222
5432
];
@ -17,7 +21,9 @@ let
# 4. go to the Vaultwarden /admin endpoint
# 5. create a Vaultwarden user
# 6. now login with that new user to Vaultwarden
extraScript = { node, proto_fqdn, ... }: ''
extraScript =
{ node, proto_fqdn, ... }:
''
with subtest("prelogin"):
response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """
{"email": "me@example.com"}
@ -42,7 +48,9 @@ let
'';
};
basic = { config, ... }: {
basic =
{ config, ... }:
{
test = {
subdomain = "v";
};
@ -64,7 +72,9 @@ let
# };
};
https = { config, ... }: {
https =
{ config, ... }:
{
shb.vaultwarden = {
ssl = config.shb.certs.certs.selfsigned.n;
};
@ -78,7 +88,9 @@ let
# # };
# };
sso = { config, ... }: {
sso =
{ config, ... }:
{
shb.vaultwarden = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
@ -97,7 +109,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -116,7 +128,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -142,7 +154,9 @@ in
sso = lib.shb.runNixOSTest {
name = "vaultwarden_sso";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
@ -156,15 +170,19 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access.override {
waitForPorts = { node, ... }: [
waitForPorts =
{ node, ... }:
[
8222
5432
9091
];
extraScript = { node, proto_fqdn, ... }: ''
extraScript =
{ node, proto_fqdn, ... }:
''
with subtest("unauthenticated access is not granted to /admin"):
response = curl(client, """{"code":%{response_code},"auth_host":"%{urle.host}","auth_query":"%{urle.query}","all":%{json}}""", "${proto_fqdn}/admin")
@ -181,7 +199,9 @@ in
backup = lib.shb.runNixOSTest {
name = "vaultwarden_backup";
nodes.server = { config, ... }: {
nodes.server =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
@ -191,7 +211,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};