chore: format all files

This commit is contained in:
ibizaman 2025-10-28 20:27:25 +01:00 committed by Pierre Penninckx
parent 16fcb94897
commit 48802553de
77 changed files with 10265 additions and 8177 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -1,4 +1,10 @@
{ config, options, pkgs, lib, ... }: {
config,
options,
pkgs,
lib,
...
}:
let let
cfg = config.shb.authelia; cfg = config.shb.authelia;
@ -181,7 +187,8 @@ in
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; freeformType = lib.types.attrsOf lib.types.anything;
options = { options = {
@ -222,7 +229,13 @@ in
}; };
authorization_policy = lib.mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -235,7 +248,12 @@ in
scopes = lib.mkOption { scopes = lib.mkOption {
type = lib.types.listOf lib.types.str; 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"; description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims";
example = [ "openid" "profile" "email" "groups" ]; example = [
"openid"
"profile"
"email"
"groups"
];
default = [ ]; default = [ ];
}; };
@ -250,7 +268,8 @@ in
default = "default"; default = "default";
}; };
}; };
}); }
);
}; };
smtp = lib.mkOption { smtp = lib.mkOption {
@ -263,7 +282,8 @@ in
default = "/tmp/authelia-notifications"; default = "/tmp/authelia-notifications";
type = lib.types.oneOf [ type = lib.types.oneOf [
lib.types.str lib.types.str
(lib.types.nullOr (lib.types.submodule { (lib.types.nullOr (
lib.types.submodule {
options = { options = {
from_address = lib.mkOption { from_address = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -299,7 +319,8 @@ in
}; };
}; };
}; };
})) }
))
]; ];
}; };
@ -324,8 +345,12 @@ in
``` ```
''; '';
readOnly = true; readOnly = true;
default = { path = "/var/lib/authelia-authelia.${cfg.domain}"; }; default = {
defaultText = { path = "/var/lib/authelia-authelia.example.com"; }; path = "/var/lib/authelia-authelia.${cfg.domain}";
};
defaultText = {
path = "/var/lib/authelia-authelia.example.com";
};
}; };
mountRedis = lib.mkOption { mountRedis = lib.mkOption {
@ -343,7 +368,9 @@ in
``` ```
''; '';
readOnly = true; readOnly = true;
default = { path = "/var/lib/redis-authelia"; }; default = {
path = "/var/lib/redis-authelia";
};
}; };
debug = lib.mkOption { debug = lib.mkOption {
@ -395,7 +422,9 @@ in
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path; 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_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 = { settings = {
server.address = "tcp://127.0.0.1:${toString listenPort}"; 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 # Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis
session = { session = {
name = "authelia_session"; name = "authelia_session";
cookies = [{ cookies = [
{
domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}"; domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}";
authelia_url = "https://${cfg.subdomain}.${cfg.domain}"; authelia_url = "https://${cfg.subdomain}.${cfg.domain}";
}]; }
];
same_site = "lax"; same_site = "lax";
expiration = "1h"; expiration = "1h";
inactivity = "5m"; inactivity = "5m";
@ -468,7 +499,11 @@ in
networks = [ networks = [
{ {
name = "internal"; 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 = [ rules = [
@ -479,7 +514,8 @@ in
"^/api/.*" "^/api/.*"
]; ];
} }
] ++ cfg.rules; ]
++ cfg.rules;
}; };
telemetry = { telemetry = {
metrics = { metrics = {
@ -489,17 +525,25 @@ in
}; };
log.level = if cfg.debug then "debug" else "info"; log.level = if cfg.debug then "debug" else "info";
} // { }
// {
identity_providers.oidc = { identity_providers.oidc = {
claims_policies = { claims_policies = {
# This default claim should go away at some point. # 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 # 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" ]; default.id_token = [
} // cfg.extraOidcClaimsPolicies; "email"
"preferred_username"
"name"
"groups"
];
}
// cfg.extraOidcClaimsPolicies;
scopes = cfg.extraOidcScopes; scopes = cfg.extraOidcScopes;
authorization_policies = cfg.extraOidcAuthorizationPolicies; authorization_policies = cfg.extraOidcAuthorizationPolicies;
}; };
} // lib.optionalAttrs (cfg.extraDefinitions != {}) { }
// lib.optionalAttrs (cfg.extraDefinitions != { }) {
definitions = cfg.extraDefinitions; definitions = cfg.extraDefinitions;
}; };
@ -508,7 +552,8 @@ in
systemd.services."authelia-${fqdn}".preStart = systemd.services."authelia-${fqdn}".preStart =
let let
mkCfg = clients: mkCfg =
clients:
lib.shb.replaceSecrets { lib.shb.replaceSecrets {
userConfig = { userConfig = {
identity_providers.oidc.clients = clients; identity_providers.oidc.clients = clients;
@ -517,9 +562,12 @@ in
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { }); generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { });
}; };
in 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' ${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} = { services.nginx.virtualHosts.${fqdn} = {
forceSSL = !(isNull cfg.ssl); forceSSL = !(isNull cfg.ssl);
@ -579,7 +627,8 @@ in
after = [ "authelia-${fqdn}.service" ]; after = [ "authelia-${fqdn}.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ]; enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [ extraArgs = [
"--set" "verbose_pattern=/api" "--set"
"verbose_pattern=/api"
]; ];
}; };
@ -610,13 +659,16 @@ in
} }
]; ];
systemd.targets."authelia-${fqdn}" = let systemd.targets."authelia-${fqdn}" =
let
services = [ services = [
"authelia-${fqdn}.service" "authelia-${fqdn}.service"
] ++ lib.optionals cfg.debug [ ]
++ lib.optionals cfg.debug [
config.shb.mitmdump.instances."authelia-${fqdn}".serviceName config.shb.mitmdump.instances."authelia-${fqdn}".serviceName
]; ];
in { in
{
after = services; after = services;
requires = services; requires = services;

View file

@ -1,4 +1,10 @@
{ config, pkgs, lib, utils, ... }: {
config,
pkgs,
lib,
utils,
...
}:
let let
cfg = config.shb.borgbackup; cfg = config.shb.borgbackup;
@ -17,7 +23,9 @@ let
type = lib.types.path; 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"; borg_keys_directory = "/run/secrets/borgmatic/keys";
sourceDirectories = lib.mkOption { sourceDirectories = lib.mkOption {
@ -39,7 +47,8 @@ let
repositories = lib.mkOption { repositories = lib.mkOption {
description = "Repositories to back this instance to."; description = "Repositories to back this instance to.";
type = lib.types.nonEmptyListOf (lib.types.submodule { type = lib.types.nonEmptyListOf (
lib.types.submodule {
options = { options = {
path = lib.mkOption { path = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -60,12 +69,18 @@ let
}; };
}; };
}; };
}); }
);
}; };
retention = lib.mkOption { retention = lib.mkOption {
description = "Retention options."; 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 = { default = {
keep_within = "1d"; keep_within = "1d";
keep_hourly = 24; keep_hourly = 24;
@ -113,7 +128,8 @@ let
}; };
}; };
repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (lib.strings.removePrefix "/" name); repoSlugName =
name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (lib.strings.removePrefix "/" name);
in in
{ {
@ -133,9 +149,11 @@ in
instances = lib.mkOption { instances = lib.mkOption {
description = "Each instance is a backup setting"; description = "Each instance is a backup setting";
default = { }; default = { };
type = lib.types.attrsOf (lib.types.submodule { type = lib.types.attrsOf (
lib.types.submodule {
options = instanceOptions; options = instanceOptions;
}); }
);
}; };
borgServer = lib.mkOption { borgServer = lib.mkOption {
@ -157,7 +175,11 @@ in
default = 15; default = 15;
}; };
ioSchedulingClass = lib.mkOption { 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."; description = "ionice scheduling class, defaults to best-effort IO.";
default = "best-effort"; default = "best-effort";
}; };
@ -174,7 +196,8 @@ in
config = lib.mkIf (cfg.instances != { }) ( config = lib.mkIf (cfg.instances != { }) (
let let
enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances; enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances;
in lib.mkMerge [ in
lib.mkMerge [
# Secrets configuration # Secrets configuration
{ {
users.users = { users.users = {
@ -195,32 +218,54 @@ in
sops.secrets = sops.secrets =
let 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; sopsFile = instance.keySopsFile;
mode = "0440"; mode = "0440";
owner = cfg.user; owner = cfg.user;
group = cfg.group; 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; sopsFile = instance.keySopsFile;
mode = "0440"; mode = "0440";
owner = cfg.user; owner = cfg.user;
group = cfg.group; 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}" = { "${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; sopsFile = instance.keySopsFile;
mode = "0440"; mode = "0440";
owner = cfg.user; owner = cfg.user;
group = cfg.group; group = cfg.group;
}; };
}) instance.repositories)) }
) instance.repositories
)
)
); );
in in
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances)); lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances));
@ -238,7 +283,10 @@ in
User = cfg.user; User = cfg.user;
Group = cfg.group; Group = cfg.group;
ExecStartPre = [ "" ]; # Do not sleep before starting. 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 # For borgmatic, since we have only one service, we need to merge all environmentFile
# from all instances. # from all instances.
EnvironmentFile = lib.mapAttrsToList (name: value: value.environmentFile) enabledInstances; EnvironmentFile = lib.mapAttrsToList (name: value: value.environmentFile) enabledInstances;
@ -248,15 +296,17 @@ in
systemd.packages = lib.mkIf (enabledInstances != { }) [ pkgs.borgmatic ]; systemd.packages = lib.mkIf (enabledInstances != { }) [ pkgs.borgmatic ];
environment.systemPackages = ( environment.systemPackages = (
lib.optionals cfg.borgServer [ pkgs.borgbackup ] lib.optionals cfg.borgServer [ pkgs.borgbackup ]
++ lib.optionals (enabledInstances != {}) [ pkgs.borgbackup pkgs.borgmatic ] ++ lib.optionals (enabledInstances != { }) [
pkgs.borgbackup
pkgs.borgmatic
]
); );
environment.etc = environment.etc =
let let
mkSettings = name: instance: { mkSettings = name: instance: {
"borgmatic.d/${name}.yaml".text = lib.generators.toYAML { } { "borgmatic.d/${name}.yaml".text = lib.generators.toYAML { } {
location = location = {
{
source_directories = instance.sourceDirectories; source_directories = instance.sourceDirectories;
repositories = map ({ path, ... }: path) instance.repositories; repositories = map ({ path, ... }: path) instance.repositories;
} }
@ -291,5 +341,6 @@ in
in in
lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings enabledInstances); lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings enabledInstances);
} }
]); ]
);
} }

View file

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

View file

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

View file

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

View file

@ -1,15 +1,34 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
inherit (lib) mapAttrs' mkOption nameValuePair types; inherit (lib)
inherit (types) attrsOf listOf port submodule str; mapAttrs'
mkOption
nameValuePair
types
;
inherit (types)
attrsOf
listOf
port
submodule
str
;
cfg = config.shb.mitmdump; cfg = config.shb.mitmdump;
mitmdumpScript = pkgs.writers.writePython3Bin "mitmdump" mitmdumpScript =
pkgs.writers.writePython3Bin "mitmdump"
{ {
libraries = let libraries =
let
p = pkgs.python3Packages; p = pkgs.python3Packages;
in [ in
[
p.systemd p.systemd
p.mitmproxy p.mitmproxy
]; ];
@ -81,8 +100,8 @@ let
proc.wait() proc.wait()
''; '';
logger = toString (pkgs.writers.writeText "loggerAddon.py" logger = toString (
'' pkgs.writers.writeText "loggerAddon.py" ''
import logging import logging
from collections.abc import Sequence from collections.abc import Sequence
from mitmproxy import ctx, http from mitmproxy import ctx, http
@ -131,7 +150,8 @@ let
addons = [RegexLogger()] addons = [RegexLogger()]
''); ''
);
in in
{ {
options.shb.mitmdump = { options.shb.mitmdump = {
@ -148,7 +168,10 @@ in
instances = mkOption { instances = mkOption {
default = { }; default = { };
description = "Mitmdump instance."; description = "Mitmdump instance.";
type = attrsOf (submodule ({ name, ... }: { type = attrsOf (
submodule (
{ name, ... }:
{
options = { options = {
package = lib.mkPackageOption pkgs "mitmproxy" { }; package = lib.mkPackageOption pkgs "mitmproxy" { };
@ -230,12 +253,16 @@ in
example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]''; example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]'';
}; };
}; };
})); }
)
);
}; };
}; };
config = { config = {
systemd.services = mapAttrs' (name: cfg': nameValuePair "mitmdump-${name}" { systemd.services = mapAttrs' (
name: cfg':
nameValuePair "mitmdump-${name}" {
environment = { environment = {
"HOME" = "/var/lib/private/mitmdump-${name}"; "HOME" = "/var/lib/private/mitmdump-${name}";
"MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump"; "MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump";
@ -250,7 +277,8 @@ in
WorkingDirectory = "/var/lib/mitmdump-${name}"; WorkingDirectory = "/var/lib/mitmdump-${name}";
StateDirectory = "mitmdump-${name}"; StateDirectory = "mitmdump-${name}";
ExecStart = let ExecStart =
let
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons; addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
extraArgs = lib.concatStringsSep " " cfg'.extraArgs; extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
in in
@ -259,7 +287,8 @@ in
requires = cfg'.after; requires = cfg'.after;
after = cfg'.after; after = cfg'.after;
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
}) cfg.instances; }
) cfg.instances;
shb.mitmdump.addons = { shb.mitmdump.addons = {
inherit logger; inherit logger;

View file

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

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.nginx; cfg = config.shb.nginx;
@ -44,7 +49,8 @@ let
type = lib.types.listOf (lib.types.attrsOf lib.types.anything); type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
default = [ ]; default = [ ];
description = "Authelia rule configuration"; description = "Authelia rule configuration";
example = lib.literalExpression ''[{ example = lib.literalExpression ''
[{
policy = "two_factor"; policy = "two_factor";
subject = ["group:service_user"]; subject = ["group:service_user"];
}]''; }]'';
@ -82,7 +88,10 @@ in
}; };
config = { config = {
networking.firewall.allowedTCPPorts = [ 80 443 ]; networking.firewall.allowedTCPPorts = [
80
443
];
services.nginx.enable = true; services.nginx.enable = true;
services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn"; services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn";

View file

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

View file

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

View file

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

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.certs; cfg = config.shb.certs;
@ -6,7 +11,13 @@ let
contracts = pkgs.callPackage ../contracts { }; contracts = pkgs.callPackage ../contracts { };
inherit (builtins) dirOf; inherit (builtins) dirOf;
inherit (lib) flatten mapAttrsToList optionalAttrs optionals unique; inherit (lib)
flatten
mapAttrsToList
optionalAttrs
optionals
unique
;
in in
{ {
options.shb.certs = { options.shb.certs = {
@ -20,7 +31,10 @@ in
cas.selfsigned = lib.mkOption { cas.selfsigned = lib.mkOption {
description = "Generate a self-signed Certificate Authority."; description = "Generate a self-signed Certificate Authority.";
default = { }; default = { };
type = lib.types.attrsOf (lib.types.submodule ({ config, ...}: { type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options = { options = {
name = lib.mkOption { name = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -54,12 +68,17 @@ in
default = "shb-certs-ca-${config._module.args.name}.service"; default = "shb-certs-ca-${config._module.args.name}.service";
}; };
}; };
})); }
)
);
}; };
certs.selfsigned = lib.mkOption { certs.selfsigned = lib.mkOption {
description = "Generate self-signed certificates signed by a Certificate Authority."; description = "Generate self-signed certificates signed by a Certificate Authority.";
default = { }; default = { };
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: { type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options = { options = {
ca = lib.mkOption { ca = lib.mkOption {
type = lib.types.nullOr contracts.ssl.cas; type = lib.types.nullOr contracts.ssl.cas;
@ -135,13 +154,18 @@ in
example = [ "nginx.service" ]; example = [ "nginx.service" ];
}; };
}; };
})); }
)
);
}; };
certs.letsencrypt = lib.mkOption { certs.letsencrypt = lib.mkOption {
description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/)."; description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/).";
default = { }; default = { };
type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: { type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options = { options = {
domain = lib.mkOption { domain = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -300,7 +324,9 @@ in
default = false; default = false;
}; };
}; };
})); }
)
);
}; };
}; };
@ -311,7 +337,8 @@ in
lib.mkMerge [ lib.mkMerge [
# Config for self-signed CA. # Config for self-signed CA.
{ {
systemd.services = lib.mapAttrs' (_name: caCfg: systemd.services = lib.mapAttrs' (
_name: caCfg:
lib.nameValuePair (serviceName caCfg.systemdService) { lib.nameValuePair (serviceName caCfg.systemdService) {
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
wants = [ config.shb.certs.systemdService ]; wants = [ config.shb.certs.systemdService ];
@ -352,7 +379,8 @@ in
} }
# Config for self-signed CA bundle. # 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" ]; wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
script = '' 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-bundle.crt
cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-certificates.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-bundle.crt
cat "$file" >> /etc/ssl/certs/ca-certificates.crt cat "$file" >> /etc/ssl/certs/ca-certificates.crt
done done
''; '';
}); }
);
} }
# Config for self-signed cert. # Config for self-signed cert.
{ {
systemd.services = lib.mapAttrs' (_name: certCfg: systemd.services = lib.mapAttrs' (
_name: certCfg:
lib.nameValuePair (serviceName certCfg.systemdService) { lib.nameValuePair (serviceName certCfg.systemdService) {
after = [ certCfg.ca.systemdService ]; after = [ certCfg.ca.systemdService ];
requires = [ certCfg.ca.systemdService ]; requires = [ certCfg.ca.systemdService ];
@ -382,8 +414,7 @@ in
script = script =
let let
extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains); extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains);
chmod = cert: chmod = cert: ''
''
chown root:${certCfg.group} ${cert} chown root:${certCfg.group} ${cert}
chmod 640 ${cert} chmod 640 ${cert}
''; '';
@ -432,89 +463,140 @@ in
} }
# Config for Let's Encrypt cert. # 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)) [ ${certCfg.makeAvailableToUser}.extraGroups = lib.mkIf (!(isNull certCfg.makeAvailableToUser)) [
config.security.acme.defaults.group 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 security.acme.certs =
extraDomainsCfg = certCfg: map (name: { let
extraDomainsCfg =
certCfg:
map (name: {
"${name}" = { "${name}" = {
email = certCfg.adminEmail; email = certCfg.adminEmail;
enableDebugLogs = certCfg.debug; enableDebugLogs = certCfg.debug;
server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory"; server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
}; };
}) certCfg.extraDomains; }) certCfg.extraDomains;
in lib.mkMerge (flatten (mapAttrsToList (name: certCfg: in
[{ lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
[
{
"${name}" = { "${name}" = {
extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains; extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains;
email = certCfg.adminEmail; email = certCfg.adminEmail;
enableDebugLogs = certCfg.debug; enableDebugLogs = certCfg.debug;
server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory"; 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) dnsProvider dnsResolver;
inherit (certCfg) group reloadServices; inherit (certCfg) group reloadServices;
credentialsFile = certCfg.credentialsFile; credentialsFile = certCfg.credentialsFile;
}; };
}] }
]
++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg) ++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg)
) cfg.certs.letsencrypt)); ) cfg.certs.letsencrypt
)
);
services.nginx = let services.nginx =
extraDomainsCfg = extraDomains: map (name: { let
extraDomainsCfg =
extraDomains:
map (name: {
virtualHosts."${name}" = { virtualHosts."${name}" = {
# addSSL = true; # addSSL = true;
enableACME = true; enableACME = true;
}; };
}) extraDomains; }) extraDomains;
in lib.mkMerge (flatten (mapAttrsToList (name: certCfg: in
lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
lib.optionals (certCfg.dnsProvider == null) ( lib.optionals (certCfg.dnsProvider == null) (
[{ [
{
virtualHosts."${name}" = { virtualHosts."${name}" = {
# addSSL = true; # addSSL = true;
enableACME = true; enableACME = true;
}; };
}] }
]
++ extraDomainsCfg certCfg.extraDomains ++ extraDomainsCfg certCfg.extraDomains
)) cfg.certs.letsencrypt)); )
) cfg.certs.letsencrypt
)
);
systemd.services = let systemd.services =
extraDomainsCfg = certCfg: flatten (map (name: let
lib.optionals (certCfg.additionalEnvironment != {} && certCfg.dnsProvider == null) [{ extraDomainsCfg =
certCfg:
flatten (
map (
name:
lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [
{
"acme-${name}".environment = certCfg.additionalEnvironment; "acme-${name}".environment = certCfg.additionalEnvironment;
}] }
++ lib.optionals (certCfg.afterAndWants != [] && certCfg.dnsProvider == null) [{ ]
++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [
{
"acme-${name}" = { "acme-${name}" = {
after = certCfg.afterAndWants; after = certCfg.afterAndWants;
wants = certCfg.afterAndWants; wants = certCfg.afterAndWants;
}; };
}] }
) certCfg.extraDomains); ]
in lib.mkMerge (flatten (mapAttrsToList (name: certCfg: ) certCfg.extraDomains
lib.optionals (certCfg.additionalEnvironment != {} && certCfg.dnsProvider == null) [{ );
in
lib.mkMerge (
flatten (
mapAttrsToList (
name: certCfg:
lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [
{
"acme-${certCfg.domain}".environment = certCfg.additionalEnvironment; "acme-${certCfg.domain}".environment = certCfg.additionalEnvironment;
}] }
++ lib.optionals (certCfg.afterAndWants != [] && certCfg.dnsProvider == null) [{ ]
++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [
{
"acme-${certCfg.domain}" = { "acme-${certCfg.domain}" = {
after = certCfg.afterAndWants; after = certCfg.afterAndWants;
wants = certCfg.afterAndWants; wants = certCfg.afterAndWants;
}; };
}] }
]
++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg) ++ 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; enable = true;
listenAddress = "127.0.0.1"; listenAddress = "127.0.0.1";
user = "acme"; user = "acme";
paths = let paths =
pathCfg = name: certCfg:
let 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. # Not sure this will work for all cases.
mainPath = dirOf (dirOf certCfg.paths.cert); mainPath = dirOf (dirOf certCfg.paths.cert);
extraDomainsPath = map (x: "${mainPath}/${x}") certCfg.extraDomains; extraDomainsPath = map (x: "${mainPath}/${x}") certCfg.extraDomains;
@ -524,17 +606,22 @@ in
unique (flatten (mapAttrsToList pathCfg cfg.certs.letsencrypt)); unique (flatten (mapAttrsToList pathCfg cfg.certs.letsencrypt));
}; };
services.prometheus.scrapeConfigs = let services.prometheus.scrapeConfigs =
scrapeCfg = name: certCfg: [{ let
scrapeCfg = name: certCfg: [
{
job_name = "node-cert-${name}"; job_name = "node-cert-${name}";
static_configs = [{ static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node-cert.port}" ]; targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node-cert.port}" ];
labels = { labels = {
"hostname" = config.networking.hostName; "hostname" = config.networking.hostName;
"domain" = certCfg.domain; "domain" = certCfg.domain;
}; };
}]; }
}]; ];
}
];
in 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. # 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; with lib;
let let
cfg = config.shb.tinyproxy; cfg = config.shb.tinyproxy;
mkValueStringTinyproxy = with lib; v: mkValueStringTinyproxy =
if true == v then "yes" with lib;
else if false == v then "no" v:
else generators.mkValueStringDefault {} v; if true == v then
"yes"
else if false == v then
"no"
else
generators.mkValueStringDefault { } v;
mkKeyValueTinyproxy = { mkKeyValueTinyproxy =
mkValueString ? mkValueStringDefault {} {
}: sep: k: v: mkValueString ? mkValueStringDefault { },
if null == v then "" }:
else "${lib.strings.escape [sep] k}${sep}${mkValueString v}"; 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 { mkKeyValue = mkKeyValueTinyproxy {
mkValueString = mkValueStringTinyproxy; mkValueString = mkValueStringTinyproxy;
} " "; } " ";
listsAsDuplicateKeys = true; listsAsDuplicateKeys = true;
}); }
);
configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings; configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings;
in in
@ -47,7 +60,8 @@ in
Configuration for [tinyproxy](https://tinyproxy.github.io/). Configuration for [tinyproxy](https://tinyproxy.github.io/).
''; '';
default = { }; default = { };
example = literalExpression ''{ example = literalExpression ''
{
Port 8888; Port 8888;
Listen 127.0.0.1; Listen 127.0.0.1;
Timeout 600; Timeout 600;
@ -55,7 +69,9 @@ in
Anonymous = ['"Host"' '"Authorization"']; Anonymous = ['"Host"' '"Authorization"'];
ReversePath = '"/example/" "http://www.example.com/"'; ReversePath = '"/example/" "http://www.example.com/"';
}''; }'';
type = types.submodule ({name, ...}: { type = types.submodule (
{ name, ... }:
{
freeformType = settingsFormat.type; freeformType = settingsFormat.type;
options = { options = {
Listen = mkOption { Listen = mkOption {
@ -91,7 +107,8 @@ in
''; '';
}; };
}; };
}); }
);
}; };
}; };
}; };
@ -107,7 +124,9 @@ in
config = { config = {
systemd.services = systemd.services =
let let
instanceConfig = name: c: mkIf c.enable { instanceConfig =
name: c:
mkIf c.enable {
"tinyproxy-${name}" = { "tinyproxy-${name}" = {
description = "TinyProxy daemon - instance ${name}"; description = "TinyProxy daemon - instance ${name}";
after = [ "network.target" ]; after = [ "network.target" ];
@ -126,12 +145,15 @@ in
RestartMaxDelaySec = "10s"; RestartMaxDelaySec = "10s";
ConfigurationDirectory = "tinyproxy"; ConfigurationDirectory = "tinyproxy";
}; };
preStart = concatStringsSep "\n" ([ preStart = concatStringsSep "\n" (
[
"cat ${configFile name c} > /etc/tinyproxy/${name}.conf" "cat ${configFile name c} > /etc/tinyproxy/${name}.conf"
] ++ optionals (c.dynamicBindFile != "") [ ]
++ optionals (c.dynamicBindFile != "") [
"echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf" "echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf"
"cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf" "cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf"
]); ]
);
}; };
}; };
in in

View file

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

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.zfs; cfg = config.shb.zfs;
@ -25,7 +30,8 @@ in
example = lib.literalExpression '' example = lib.literalExpression ''
shb.zfs."safe/postgresql".path = "/var/lib/postgresql"; shb.zfs."safe/postgresql".path = "/var/lib/postgresql";
''; '';
type = lib.types.attrsOf (lib.types.submodule { type = lib.types.attrsOf (
lib.types.submodule {
options = { options = {
enable = lib.mkEnableOption "shb.zfs.datasets"; enable = lib.mkEnableOption "shb.zfs.datasets";
@ -40,19 +46,23 @@ in
description = "Path this dataset should be mounted on."; description = "Path this dataset should be mounted on.";
}; };
}; };
}); }
);
}; };
}; };
config = { config = {
assertions = [ 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"; message = "Cannot have both datasets.poolName and defaultPoolName set to null";
} }
]; ];
system.activationScripts = lib.mapAttrs' (name: cfg': system.activationScripts = lib.mapAttrs' (
name: cfg':
let let
dataset = (if cfg'.poolName != null then cfg'.poolName else cfg.defaultPoolName) + "/" + name; dataset = (if cfg'.poolName != null then cfg'.poolName else cfg.defaultPoolName) + "/" + name;
in in
@ -68,6 +78,7 @@ in
mountpoint=${cfg'.path} \ mountpoint=${cfg'.path} \
${dataset} ${dataset}
''; '';
}) cfg.datasets; }
) cfg.datasets;
}; };
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -4,9 +4,11 @@ let
inherit (lib.types) anything; inherit (lib.types) anything;
mkContractFunctions = mkContractFunctions =
{ mkRequest, {
mkRequest,
mkResult, mkResult,
}: { }:
{
mkRequester = requestCfg: { mkRequester = requestCfg: {
request = mkRequest requestCfg; request = mkRequest requestCfg;
@ -14,13 +16,16 @@ let
}; };
mkProvider = mkProvider =
{ resultCfg, {
resultCfg,
settings ? { }, settings ? { },
}: { }:
{
request = mkRequest { }; request = mkRequest { };
result = mkResult resultCfg; result = mkResult resultCfg;
} // optionalAttrs (settings != {}) { inherit settings; }; }
// optionalAttrs (settings != { }) { inherit settings; };
contract = { contract = {
request = mkRequest { }; request = mkRequest { };
@ -36,7 +41,8 @@ let
}; };
}; };
importContract = module: importContract =
module:
let let
importedModule = pkgs.callPackage module { }; importedModule = pkgs.callPackage module { };
in in

View file

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

View file

@ -3,7 +3,8 @@ let
inherit (lib) getAttrFromPath setAttrByPath; inherit (lib) getAttrFromPath setAttrByPath;
inherit (lib) mkIf; inherit (lib) mkIf;
in in
{ name, {
name,
configRoot, configRoot,
settingsCfg, # str -> attrset settingsCfg, # str -> attrset
modules ? [ ], modules ? [ ],
@ -11,16 +12,24 @@ in
group ? "root", group ? "root",
mode ? "0400", mode ? "0400",
restartUnits ? [ "myunit.service" ], restartUnits ? [ "myunit.service" ],
}: lib.shb.runNixOSTest { }:
lib.shb.runNixOSTest {
name = "secret_${name}_${owner}_${group}_${mode}"; name = "secret_${name}_${owner}_${group}_${mode}";
nodes.machine = { config, ... }: { nodes.machine =
{ config, ... }:
{
imports = [ lib.shb.baseImports ] ++ modules; imports = [ lib.shb.baseImports ] ++ modules;
config = lib.mkMerge [ config = lib.mkMerge [
(setAttrByPath configRoot { (setAttrByPath configRoot {
A = { A = {
request = { request = {
inherit owner group mode restartUnits; inherit
owner
group
mode
restartUnits
;
}; };
settings = settingsCfg "secretA"; settings = settingsCfg "secretA";
}; };
@ -34,7 +43,8 @@ in
]; ];
}; };
testScript = { nodes, ... }: testScript =
{ nodes, ... }:
let let
result = (getAttrFromPath configRoot nodes.machine)."A".result; result = (getAttrFromPath configRoot nodes.machine)."A".result;
in in

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.arr; cfg = config.shb.arr;
@ -20,7 +25,10 @@ let
description = "Path to api key secret file."; description = "Path to api key secret file.";
}; };
LogLevel = lib.mkOption { LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"]; type = lib.types.enum [
"debug"
"info"
];
description = "Log level."; description = "Log level.";
default = "info"; default = "info";
}; };
@ -78,7 +86,10 @@ let
description = "Path to api key secret file."; description = "Path to api key secret file.";
}; };
LogLevel = lib.mkOption { LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"]; type = lib.types.enum [
"debug"
"info"
];
description = "Log level."; description = "Log level.";
default = "info"; default = "info";
}; };
@ -127,7 +138,10 @@ let
freeformType = apps.bazarr.settingsFormat.type; freeformType = apps.bazarr.settingsFormat.type;
options = { options = {
LogLevel = lib.mkOption { LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"]; type = lib.types.enum [
"debug"
"info"
];
description = "Log level."; description = "Log level.";
default = "info"; default = "info";
}; };
@ -152,7 +166,10 @@ let
freeformType = apps.readarr.settingsFormat.type; freeformType = apps.readarr.settingsFormat.type;
options = { options = {
LogLevel = lib.mkOption { LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"]; type = lib.types.enum [
"debug"
"info"
];
description = "Log level."; description = "Log level.";
default = "info"; default = "info";
}; };
@ -176,7 +193,10 @@ let
freeformType = apps.lidarr.settingsFormat.type; freeformType = apps.lidarr.settingsFormat.type;
options = { options = {
LogLevel = lib.mkOption { LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"]; type = lib.types.enum [
"debug"
"info"
];
description = "Log level."; description = "Log level.";
default = "info"; default = "info";
}; };
@ -214,7 +234,12 @@ let
default = null; default = null;
}; };
ProxyType = lib.mkOption { ProxyType = lib.mkOption {
type = lib.types.enum [ "-1" "0" "1" "2" ]; type = lib.types.enum [
"-1"
"0"
"1"
"2"
];
default = "-1"; default = "-1";
description = '' description = ''
-1 = disabled -1 = disabled
@ -256,8 +281,17 @@ let
}; };
}; };
vhosts = { extraBypassResources ? [] }: c: { vhosts =
inherit (c) subdomain domain authEndpoint ssl; {
extraBypassResources ? [ ],
}:
c: {
inherit (c)
subdomain
domain
authEndpoint
ssl
;
upstream = "http://127.0.0.1:${toString c.settings.Port}"; upstream = "http://127.0.0.1:${toString c.settings.Port}";
autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [ autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [
@ -277,7 +311,10 @@ let
]; ];
}; };
appOption = name: c: lib.nameValuePair name (lib.mkOption { appOption =
name: c:
lib.nameValuePair name (
lib.mkOption {
description = "Configuration for ${name}"; description = "Configuration for ${name}";
default = { }; default = { };
type = lib.types.submodule { type = lib.types.submodule {
@ -326,13 +363,19 @@ let
sourceDirectories = [ sourceDirectories = [
cfg.${name}.dataDir cfg.${name}.dataDir
]; ];
excludePatterns = [".db-shm" ".db-wal" ".mono"]; excludePatterns = [
".db-shm"
".db-wal"
".mono"
];
}; };
}; };
}; };
} // (c.moreOptions or {}); }
// (c.moreOptions or { });
}; };
}); }
);
in in
{ {
imports = [ imports = [
@ -356,7 +399,8 @@ in
}; };
systemd.services.radarr.preStart = lib.shb.replaceSecrets { systemd.services.radarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled { // (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses"; AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External"; AuthenticationMethod = "External";
@ -366,7 +410,8 @@ in
}; };
shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.nginx.vhosts = [ (vhosts { } cfg') ];
})) }
))
(lib.mkIf cfg.sonarr.enable ( (lib.mkIf cfg.sonarr.enable (
let let
@ -385,7 +430,8 @@ in
}; };
systemd.services.sonarr.preStart = lib.shb.replaceSecrets { systemd.services.sonarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled { // (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses"; AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External"; AuthenticationMethod = "External";
@ -395,7 +441,8 @@ in
}; };
shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.nginx.vhosts = [ (vhosts { } cfg') ];
})) }
))
(lib.mkIf cfg.bazarr.enable ( (lib.mkIf cfg.bazarr.enable (
let let
@ -411,7 +458,8 @@ in
extraGroups = [ "media" ]; extraGroups = [ "media" ];
}; };
systemd.services.bazarr.preStart = lib.shb.replaceSecrets { systemd.services.bazarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled { // (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses"; AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External"; AuthenticationMethod = "External";
@ -421,7 +469,8 @@ in
}; };
shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.nginx.vhosts = [ (vhosts { } cfg') ];
})) }
))
(lib.mkIf cfg.readarr.enable ( (lib.mkIf cfg.readarr.enable (
let let
@ -442,7 +491,8 @@ in
}; };
shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.nginx.vhosts = [ (vhosts { } cfg') ];
})) }
))
(lib.mkIf cfg.lidarr.enable ( (lib.mkIf cfg.lidarr.enable (
let let
@ -458,7 +508,8 @@ in
extraGroups = [ "media" ]; extraGroups = [ "media" ];
}; };
systemd.services.lidarr.preStart = lib.shb.replaceSecrets { systemd.services.lidarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled { // (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses"; AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External"; AuthenticationMethod = "External";
@ -468,7 +519,8 @@ in
}; };
shb.nginx.vhosts = [ (vhosts { } cfg') ]; shb.nginx.vhosts = [ (vhosts { } cfg') ];
})) }
))
(lib.mkIf cfg.jackett.enable ( (lib.mkIf cfg.jackett.enable (
let let
@ -489,9 +541,12 @@ in
generator = apps.jackett.settingsFormat.generate; generator = apps.jackett.settingsFormat.generate;
}; };
shb.nginx.vhosts = [ (vhosts { shb.nginx.vhosts = [
(vhosts {
extraBypassResources = [ "^/dl.*" ]; extraBypassResources = [ "^/dl.*" ];
} cfg') ]; } cfg')
})) ];
}
))
]; ];
} }

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.audiobookshelf; cfg = config.shb.audiobookshelf;
@ -87,7 +92,10 @@ in
}; };
authorization_policy = lib.mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -133,14 +141,24 @@ in
}; };
logLevel = lib.mkOption { 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."; description = "Enable logging.";
default = false; default = false;
example = true; example = true;
}; };
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [{ config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.audiobookshelf = { services.audiobookshelf = {
enable = true; enable = true;
@ -175,7 +193,6 @@ in
''; '';
}; };
shb.authelia.extraDefinitions = { shb.authelia.extraDefinitions = {
user_attributes.${roleClaim}.expression = user_attributes.${roleClaim}.expression =
''"${cfg.sso.adminUserGroup}" in groups ? ["admin"] : ("${cfg.sso.userGroup}" in groups ? ["user"] : [""])''; ''"${cfg.sso.adminUserGroup}" in groups ? ["admin"] : ("${cfg.sso.userGroup}" in groups ? ["user"] : [""])'';
@ -203,14 +220,23 @@ in
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback" "https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback"
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect" "https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect"
]; ];
scopes = [ "openid" "profile" "email" "groups" "${roleClaim}" ]; scopes = [
"openid"
"profile"
"email"
"groups"
"${roleClaim}"
];
require_pkce = true; require_pkce = true;
pkce_challenge_method = "S256"; pkce_challenge_method = "S256";
userinfo_signed_response_alg = "none"; userinfo_signed_response_alg = "none";
token_endpoint_auth_method = "client_secret_basic"; token_endpoint_auth_method = "client_secret_basic";
} }
]; ];
} { }
systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig; {
}]); systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig;
}
]
);
} }

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.deluge; cfg = config.shb.deluge;
@ -7,9 +12,15 @@ let
fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdn = "${cfg.subdomain}.${cfg.domain}";
authGenerator = users: authGenerator =
users:
let let
genLine = name: { password, priority ? 10 }: genLine =
name:
{
password,
priority ? 10,
}:
"${name}:${password}:${toString priority}"; "${name}:${password}:${toString priority}";
lines = lib.mapAttrsToList genLine users; lines = lib.mapAttrsToList genLine users;
@ -57,7 +68,10 @@ in
daemonListenPorts = lib.mkOption { daemonListenPorts = lib.mkOption {
type = lib.types.listOf lib.types.int; type = lib.types.listOf lib.types.int;
description = "Deluge daemon listen ports"; description = "Deluge daemon listen ports";
default = [ 6881 6889 ]; default = [
6881
6889
];
}; };
webPort = lib.mkOption { webPort = lib.mkOption {
@ -176,14 +190,16 @@ in
extraUsers = lib.mkOption { extraUsers = lib.mkOption {
description = "Users having access to this deluge instance. Attrset of username to user options."; 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 = { options = {
password = lib.mkOption { password = lib.mkOption {
type = lib.shb.secretFileType; type = lib.shb.secretFileType;
description = "File containing the user password."; description = "File containing the user password.";
}; };
}; };
}); }
);
}; };
localclientPassword = lib.mkOption { localclientPassword = lib.mkOption {
@ -198,12 +214,17 @@ in
prometheusScraperPassword = lib.mkOption { prometheusScraperPassword = lib.mkOption {
description = "Password for prometheus scraper. Setting this option will activate the prometheus deluge exporter."; 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 { options = contracts.secret.mkRequester {
owner = "deluge"; owner = "deluge";
restartUnits = [ "deluged.service" "prometheus.service" ]; restartUnits = [
"deluged.service"
"prometheus.service"
];
}; };
}); }
);
default = null; default = null;
}; };
@ -262,14 +283,24 @@ in
}; };
logLevel = lib.mkOption { 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."; description = "Enable logging.";
default = null; default = null;
example = "info"; example = "info";
}; };
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [{ config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.deluge = { services.deluge = {
enable = true; enable = true;
declarative = true; declarative = true;
@ -292,7 +323,8 @@ in
}; };
outgoing_interface = cfg.outgoingInterface; outgoing_interface = cfg.outgoingInterface;
enabled_plugins = cfg.enabledPlugins enabled_plugins =
cfg.enabledPlugins
++ lib.optional (lib.any (x: x.enable) [ ++ lib.optional (lib.any (x: x.enable) [
config.services.radarr config.services.radarr
config.services.sonarr config.services.sonarr
@ -316,7 +348,8 @@ in
max_upload_speed max_upload_speed
max_upload_speed_per_torrent max_upload_speed_per_torrent
dont_count_slow_torrents; dont_count_slow_torrents
;
new_release_check = false; new_release_check = false;
}; };
@ -327,22 +360,31 @@ in
web.port = cfg.webPort; web.port = cfg.webPort;
}; };
systemd.services.deluged.preStart = lib.mkBefore (lib.shb.replaceSecrets { systemd.services.deluged.preStart = lib.mkBefore (
userConfig = cfg.extraUsers // { lib.shb.replaceSecrets {
userConfig =
cfg.extraUsers
// {
localclient.password.source = config.shb.deluge.localclientPassword.result.path; 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; prometheus_scraper.password.source = config.shb.deluge.prometheusScraperPassword.result.path;
}); });
resultPath = "${cfg.dataDir}/.config/deluge/authTemplate"; resultPath = "${cfg.dataDir}/.config/deluge/authTemplate";
generator = name: value: pkgs.writeText "delugeAuth" (authGenerator value); 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" "${config.services.deluge.package}/bin/deluged"
"--do-not-daemonize" "--do-not-daemonize"
"--config ${cfg.dataDir}/.config/deluge" "--config ${cfg.dataDir}/.config/deluge"
] ++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}") ]
)); ++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}")
)
);
systemd.tmpfiles.rules = systemd.tmpfiles.rules =
let let
@ -356,7 +398,8 @@ in
]; ];
shb.nginx.vhosts = [ shb.nginx.vhosts = [
({ (
{
inherit (cfg) subdomain domain ssl; inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}"; upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [ autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
@ -373,13 +416,17 @@ in
subject = [ "group:deluge_user" ]; subject = [ "group:deluge_user" ];
} }
]; ];
} // (lib.optionalAttrs (cfg.authEndpoint != null) { }
// (lib.optionalAttrs (cfg.authEndpoint != null) {
inherit (cfg) authEndpoint; inherit (cfg) authEndpoint;
})) })
)
]; ];
} { }
{
systemd.services.deluged.serviceConfig = cfg.extraServiceConfig; systemd.services.deluged.serviceConfig = cfg.extraServiceConfig;
} (lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) { }
(lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) {
services.prometheus.exporters.deluge = { services.prometheus.exporters.deluge = {
enable = true; enable = true;
@ -393,15 +440,18 @@ in
services.prometheus.scrapeConfigs = [ services.prometheus.scrapeConfigs = [
{ {
job_name = "deluge"; job_name = "deluge";
static_configs = [{ static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.deluge.port}" ]; targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.deluge.port}" ];
labels = { labels = {
"hostname" = config.networking.hostName; "hostname" = config.networking.hostName;
"domain" = cfg.domain; "domain" = cfg.domain;
}; };
}]; }
];
} }
]; ];
}) })
]); ]
);
} }

View file

@ -1,18 +1,53 @@
{ config, options, pkgs, lib, ... }: {
config,
options,
pkgs,
lib,
...
}:
let let
cfg = config.shb.forgejo; 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)
inherit (lib.types) attrsOf bool enum listOf nullOr package port submodule str; 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 in
{ {
imports = [ imports = [
../blocks/nginx.nix ../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 = { shb.forgejo.users = {
"forgejoadmin" = { "forgejoadmin" = {
isAdmin = true; isAdmin = true;
@ -164,7 +199,10 @@ in
}; };
authorization_policy = mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -205,7 +243,8 @@ in
}; };
email = mkOption { 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. This is only set when the user is created, changing this later on will have no effect.
''; '';
@ -254,7 +293,6 @@ in
''; '';
}; };
hostPackages = mkOption { hostPackages = mkOption {
type = listOf package; type = listOf package;
default = with pkgs; [ default = with pkgs; [
@ -295,7 +333,8 @@ in
user = options.services.forgejo.user.value; user = options.services.forgejo.user.value;
sourceDirectories = [ sourceDirectories = [
options.services.forgejo.dump.backupDir.value options.services.forgejo.dump.backupDir.value
] ++ optionals (cfg.repositoryRoot != null) [ ]
++ optionals (cfg.repositoryRoot != null) [
cfg.repositoryRoot cfg.repositoryRoot
]; ];
}; };
@ -317,7 +356,9 @@ in
``` ```
''; '';
readOnly = true; readOnly = true;
default = { path = config.services.forgejo.stateDir; }; default = {
path = config.services.forgejo.stateDir;
};
}; };
smtp = mkOption { smtp = mkOption {
@ -390,10 +431,12 @@ in
# https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113 # https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113
systemd.services.forgejo.serviceConfig.Type = mkForce "exec"; systemd.services.forgejo.serviceConfig.Type = mkForce "exec";
shb.nginx.vhosts = [{ shb.nginx.vhosts = [
{
inherit (cfg) domain subdomain ssl; inherit (cfg) domain subdomain ssl;
upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}"; upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}";
}]; }
];
}) })
(mkIf cfg.enable { (mkIf cfg.enable {
@ -420,9 +463,11 @@ in
systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices; systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices;
systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices; systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices;
# The delimiter in the `cut` command is a TAB! # The delimiter in the `cut` command is a TAB!
systemd.services.forgejo.preStart = let systemd.services.forgejo.preStart =
let
provider = "SHB-${cfg.ldap.provider}"; provider = "SHB-${cfg.ldap.provider}";
in '' in
''
auth="${getExe config.services.forgejo.package} admin auth" auth="${getExe config.services.forgejo.package} admin auth"
echo "Trying to find existing ldap configuration for ${provider}"... echo "Trying to find existing ldap configuration for ${provider}"...
@ -497,9 +542,11 @@ in
}; };
# The delimiter in the `cut` command is a TAB! # The delimiter in the `cut` command is a TAB!
systemd.services.forgejo.preStart = let systemd.services.forgejo.preStart =
let
provider = "SHB-${cfg.sso.provider}"; provider = "SHB-${cfg.sso.provider}";
in '' in
''
auth="${getExe config.services.forgejo.package} admin auth" auth="${getExe config.services.forgejo.package} admin auth"
echo "Trying to find existing sso configuration for ${provider}"... echo "Trying to find existing sso configuration for ${provider}"...
@ -529,16 +576,19 @@ in
''; '';
shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [ shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [
(let (
let
provider = "SHB-${cfg.sso.provider}"; provider = "SHB-${cfg.sso.provider}";
in { in
{
client_id = cfg.sso.clientID; client_id = cfg.sso.clientID;
client_name = "Forgejo"; client_name = "Forgejo";
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
public = false; public = false;
authorization_policy = cfg.sso.authorization_policy; authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ]; redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ];
}) }
)
]; ];
}) })
@ -552,7 +602,8 @@ in
systemd.services.forgejo.preStart = '' systemd.services.forgejo.preStart = ''
admin="${getExe config.services.forgejo.package} admin user" admin="${getExe config.services.forgejo.package} admin user"
'' + concatMapStringsSep "\n" (u: '' ''
+ concatMapStringsSep "\n" (u: ''
if ! $admin list | grep "${u.name}"; then 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})" $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 else
@ -584,9 +635,11 @@ in
instances.local = { instances.local = {
enable = true; enable = true;
name = "local"; name = "local";
url = let url =
let
protocol = if cfg.ssl != null then "https" else "http"; 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. tokenFile = ""; # Empty variable to satisfy an assertion.
labels = [ labels = [
# "ubuntu-latest:docker://node:16-bullseye" # "ubuntu-latest:docker://node:16-bullseye"

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.grocy; cfg = config.shb.grocy;
@ -37,7 +42,24 @@ in
}; };
culture = lib.mkOption { 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"; default = "en";
description = '' description = ''
Display language of the frontend. Display language of the frontend.
@ -78,14 +100,24 @@ in
}; };
logLevel = lib.mkOption { 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."; description = "Enable logging.";
default = false; default = false;
example = true; example = true;
}; };
}; };
config = lib.mkIf cfg.enable (lib.mkMerge [{ config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.grocy = { services.grocy = {
enable = true; enable = true;
hostName = fqdn; hostName = fqdn;
@ -105,7 +137,10 @@ in
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert; sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key; sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
}; };
} { }
systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig; {
}]); systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig;
}
]
);
} }

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.hledger; cfg = config.shb.hledger;
@ -108,13 +113,20 @@ in
shb.nginx.vhosts = [ 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}"; upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}";
autheliaRules = [{ autheliaRules = [
{
domain = fqdn; domain = fqdn;
policy = "two_factor"; policy = "two_factor";
subject = [ "group:hledger_user" ]; subject = [ "group:hledger_user" ];
}]; }
];
} }
]; ];
}; };

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.home-assistant; cfg = config.shb.home-assistant;
@ -24,9 +29,7 @@ let
nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config); nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config);
configWithSecretsIncludes = configWithSecretsIncludes = nonSecrets // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
nonSecrets
// (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
in in
{ {
options.shb.home-assistant = { options.shb.home-assistant = {
@ -56,28 +59,49 @@ in
freeformType = lib.types.attrsOf lib.types.str; freeformType = lib.types.attrsOf lib.types.str;
options = { options = {
name = lib.mkOption { 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."; description = "Name of the Home Assistant instance.";
}; };
country = lib.mkOption { 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."; description = "Two letter country code where this instance is located.";
}; };
latitude = lib.mkOption { 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."; description = "Latitude where this instance is located.";
}; };
longitude = lib.mkOption { 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."; description = "Longitude where this instance is located.";
}; };
time_zone = lib.mkOption { 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."; description = "Timezone of this instance.";
example = "America/Los_Angeles"; example = "America/Los_Angeles";
}; };
unit_system = lib.mkOption { 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."; description = "Unit system of this instance.";
example = "metric"; example = "metric";
}; };
@ -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 https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.openwakeword
''; '';
type = lib.types.anything; type = lib.types.anything;
default = { enable = false; }; default = {
enable = false;
};
}; };
}; };
}; };
@ -240,7 +266,10 @@ in
{ {
type = "command_line"; type = "command_line";
command = ldap_auth_script + "/bin/ldap_auth.sh"; 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; meta = true;
} }
]); ]);
@ -286,7 +315,11 @@ in
{ {
name = "random_joke"; name = "random_joke";
platform = "rest"; platform = "rest";
json_attributes = ["joke" "id" "status"]; json_attributes = [
"joke"
"id"
"status"
];
value_template = "{{ value_json.joke }}"; value_template = "{{ value_json.joke }}";
resource = "https://icanhazdadjoke.com/"; resource = "https://icanhazdadjoke.com/";
scan_interval = "3600"; scan_interval = "3600";
@ -324,7 +357,8 @@ in
}; };
systemd.services.home-assistant.preStart = systemd.services.home-assistant.preStart =
(let (
let
# TODO: this probably does not work anymore # TODO: this probably does not work anymore
onboarding = pkgs.writeText "onboarding" '' onboarding = pkgs.writeText "onboarding" ''
{ {
@ -342,11 +376,13 @@ in
''; '';
storage = "${config.services.home-assistant.configDir}"; storage = "${config.services.home-assistant.configDir}";
file = "${storage}/.storage/onboarding"; file = "${storage}/.storage/onboarding";
in '' in
''
if [ ! -f ${file} ]; then if [ ! -f ${file} ]; then
mkdir -p ''$(dirname ${file}) && cp ${onboarding} ${file} mkdir -p ''$(dirname ${file}) && cp ${onboarding} ${file}
fi fi
'') ''
)
+ (lib.shb.replaceSecrets { + (lib.shb.replaceSecrets {
userConfig = cfg.config; userConfig = cfg.config;
resultPath = "${config.services.home-assistant.configDir}/secrets.yaml"; resultPath = "${config.services.home-assistant.configDir}/secrets.yaml";

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.immich; cfg = config.shb.immich;
@ -12,21 +17,29 @@ let
# TODO: Quota management, see https://github.com/ibizaman/selfhostblocks/pull/523#discussion_r2309421694 # TODO: Quota management, see https://github.com/ibizaman/selfhostblocks/pull/523#discussion_r2309421694
#quotaClaim = "immich_quota"; #quotaClaim = "immich_quota";
scopes = [ "openid" "email" "profile" "groups" "immich_scope"]; scopes = [
"openid"
"email"
"profile"
"groups"
"immich_scope"
];
dataFolder = cfg.mediaLocation; dataFolder = cfg.mediaLocation;
ssoFqdnWithPort = if isNull cfg.sso.port ssoFqdnWithPort =
then cfg.sso.endpoint if isNull cfg.sso.port then cfg.sso.endpoint else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
# Generate Immich configuration file only for SHB-managed settings # 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) { // lib.optionalAttrs (cfg.sso.enable) {
oauth = { oauth = {
enabled = true; enabled = true;
issuerUrl = "${ssoFqdnWithPort}"; issuerUrl = "${ssoFqdnWithPort}";
clientId = cfg.sso.clientID; clientId = cfg.sso.clientID;
roleClaim = roleClaim; roleClaim = roleClaim;
clientSecret = { source = cfg.sso.sharedSecret.result.path; }; clientSecret = {
source = cfg.sso.sharedSecret.result.path;
};
scope = builtins.concatStringsSep " " scopes; scope = builtins.concatStringsSep " " scopes;
storageLabelClaim = cfg.sso.storageLabelClaim; storageLabelClaim = cfg.sso.storageLabelClaim;
#storageQuotaClaim = quotaClaim; # TODO (commented out, otherwise defaults to 0 bytes!) #storageQuotaClaim = quotaClaim; # TODO (commented out, otherwise defaults to 0 bytes!)
@ -49,7 +62,9 @@ let
host = cfg.smtp.host; host = cfg.smtp.host;
port = cfg.smtp.port; port = cfg.smtp.port;
username = cfg.smtp.username; username = cfg.smtp.username;
password = { source = cfg.smtp.password.result.path; }; password = {
source = cfg.smtp.password.result.path;
};
ignoreTLS = cfg.smtp.ignoreTLS; ignoreTLS = cfg.smtp.ignoreTLS;
secure = cfg.smtp.secure; secure = cfg.smtp.secure;
}; };
@ -69,8 +84,25 @@ let
permissions = "u=r,g=,o="; permissions = "u=r,g=,o=";
} }
); );
inherit (lib) mkEnableOption mkIf lists mkOption optionals; inherit (lib)
inherit (lib.types) attrs attrsOf bool enum listOf nullOr port submodule str path; mkEnableOption
mkIf
lists
mkOption
optionals
;
inherit (lib.types)
attrs
attrsOf
bool
enum
listOf
nullOr
port
submodule
str
path
;
in in
{ {
imports = [ imports = [
@ -154,7 +186,9 @@ in
``` ```
''; '';
readOnly = true; readOnly = true;
default = { path = dataFolder; }; default = {
path = dataFolder;
};
}; };
backup = mkOption { backup = mkOption {
@ -222,7 +256,11 @@ in
enable = mkEnableOption "SSO integration."; enable = mkEnableOption "SSO integration.";
provider = mkOption { provider = mkOption {
type = enum [ "Authelia" "Keycloak" "Generic" ]; type = enum [
"Authelia"
"Keycloak"
"Generic"
];
description = "OIDC provider name, used for display."; description = "OIDC provider name, used for display.";
default = "Authelia"; default = "Authelia";
}; };
@ -311,7 +349,10 @@ in
}; };
authorization_policy = mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -452,9 +493,11 @@ in
REDIS_HOSTNAME = "127.0.0.1"; REDIS_HOSTNAME = "127.0.0.1";
REDIS_PORT = "6379"; REDIS_PORT = "6379";
REDIS_DBINDEX = "0"; REDIS_DBINDEX = "0";
} // lib.optionalAttrs (cfg.jwtSecretFile != null) { }
// lib.optionalAttrs (cfg.jwtSecretFile != null) {
JWT_SECRET_FILE = cfg.jwtSecretFile.result.path; 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; IMMICH_CONFIG_FILE = configFile;
}; };
}; };
@ -465,7 +508,9 @@ in
]; ];
# Configuration setup service - generates config only for SHB-managed settings # 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"; description = "Setup Immich configuration for SHB-managed settings";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
before = [ "immich-server.service" ]; before = [ "immich-server.service" ];
@ -484,7 +529,10 @@ in
}; };
# Add immich user to video and render groups for hardware acceleration # 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 # PostgreSQL extensions are automatically handled by the Immich service
@ -499,7 +547,10 @@ in
{ {
domain = fqdn; domain = fqdn;
policy = cfg.sso.authorization_policy; 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; authEndpoint = lib.mkIf (cfg.sso.enable) cfg.sso.endpoint;
@ -515,10 +566,20 @@ in
# Ensure services start in correct order # Ensure services start in correct order
systemd.services.immich-server = { systemd.services.immich-server = {
after = [ "postgresql.service" "redis-immich.service" ] after = [
++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ]; "postgresql.service"
requires = [ "postgresql.service" "redis-immich.service" ] "redis-immich.service"
++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.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 { systemd.services.immich-machine-learning = mkIf cfg.machineLearning.enable {
@ -530,7 +591,8 @@ in
# Immich expects all users that get a token to be granted access. So users can either be part of the # 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 # "admin" group or the "user" group. Users that are not part of either should be blocked by
# the ID provider (Authelia). # 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 = { shb.authelia.extraOidcClaimsPolicies.immich_policy = {
custom_claims = { custom_claims = {
@ -541,7 +603,6 @@ in
claims = [ roleClaim ]; claims = [ roleClaim ];
}; };
shb.authelia.oidcClients = lists.optionals (cfg.sso.enable && cfg.sso.provider == "Authelia") [ shb.authelia.oidcClients = lists.optionals (cfg.sso.enable && cfg.sso.provider == "Authelia") [
{ {
client_id = cfg.sso.clientID; client_id = cfg.sso.clientID;

View file

@ -1,4 +1,9 @@
{ config, lib, pkgs, ...}: {
config,
lib,
pkgs,
...
}:
let let
inherit (lib) types; inherit (lib) types;
@ -91,7 +96,8 @@ in
admin = lib.mkOption { admin = lib.mkOption {
description = "Default admin user info. Only needed if LDAP or SSO is not configured."; description = "Default admin user info. Only needed if LDAP or SSO is not configured.";
default = null; default = null;
type = types.nullOr (types.submodule { type = types.nullOr (
types.submodule {
options = { options = {
username = lib.mkOption { username = lib.mkOption {
description = "Username of the default admin user."; description = "Username of the default admin user.";
@ -110,7 +116,8 @@ in
}; };
}; };
}; };
}); }
);
}; };
ldap = lib.mkOption { ldap = lib.mkOption {
@ -203,7 +210,10 @@ in
}; };
authorization_policy = lib.mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -245,7 +255,8 @@ in
sourceDirectories = [ sourceDirectories = [
config.services.jellyfin.dataDir config.services.jellyfin.dataDir
]; ];
sourceDirectoriesText = ''[ sourceDirectoriesText = ''
[
"services.jellyfin.dataDir" "services.jellyfin.dataDir"
]''; ]'';
}; };
@ -254,7 +265,10 @@ in
}; };
imports = [ imports = [
(lib.mkRenamedOptionModule [ "shb" "jellyfin" "adminPassword" ] [ "shb" "jellyfin" "admin" "password" ]) (lib.mkRenamedOptionModule
[ "shb" "jellyfin" "adminPassword" ]
[ "shb" "jellyfin" "admin" "password" ]
)
]; ];
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
@ -269,7 +283,10 @@ in
networking.firewall = { networking.firewall = {
# from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery # from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery
allowedUDPPorts = [ 1900 7359 ]; allowedUDPPorts = [
1900
7359
];
}; };
services.nginx.enable = true; services.nginx.enable = true;
@ -383,7 +400,8 @@ in
''; '';
}; };
services.prometheus.scrapeConfigs = [{ services.prometheus.scrapeConfigs = [
{
job_name = "jellyfin"; job_name = "jellyfin";
static_configs = [ static_configs = [
{ {
@ -394,7 +412,8 @@ in
}; };
} }
]; ];
}]; }
];
# LDAP config but you need to install the plugin by hand # LDAP config but you need to install the plugin by hand
@ -560,8 +579,7 @@ in
</NetworkConfiguration> </NetworkConfiguration>
''; '';
in 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 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." echo "A ${config.services.jellyfin.configDir}/logging.json file exists already, this indicates probably an existing installation. Please remove it before continuing."
exit 1 exit 1
@ -576,7 +594,8 @@ in
replacements = [ replacements = [
]; ];
}) })
+ lib.strings.optionalString cfg.ldap.enable (lib.shb.replaceSecretsScript { + lib.strings.optionalString cfg.ldap.enable (
lib.shb.replaceSecretsScript {
file = ldapConfig; file = ldapConfig;
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml"; resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml";
replacements = [ replacements = [
@ -585,8 +604,10 @@ in
source = cfg.ldap.adminPassword.result.path; 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; file = ssoConfig;
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/SSO-Auth.xml"; resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/SSO-Auth.xml";
replacements = [ replacements = [
@ -595,15 +616,19 @@ in
source = cfg.sso.sharedSecret.result.path; 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; file = brandingConfig;
resultPath = "${config.services.jellyfin.dataDir}/config/branding.xml"; resultPath = "${config.services.jellyfin.dataDir}/config/branding.xml";
replacements = [ replacements = [
]; ];
}); }
);
systemd.services.jellyfin.serviceConfig.ExecStartPost = let systemd.services.jellyfin.serviceConfig.ExecStartPost =
let
# We must always wait for the service to be fully initialized, # We must always wait for the service to be fully initialized,
# even if we're planning on changing the config and restarting. # even if we're planning on changing the config and restarting.
waitForCurl = pkgs.writeShellApplication { waitForCurl = pkgs.writeShellApplication {

View file

@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.shb.karakeep; cfg = config.shb.karakeep;
@ -91,7 +96,10 @@ in
}; };
authorization_policy = lib.mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -102,7 +110,11 @@ in
options = contracts.secret.mkRequester { options = contracts.secret.mkRequester {
owner = "karakeep"; owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets. # 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"
];
}; };
}; };
}; };
@ -142,7 +154,11 @@ in
options = contracts.secret.mkRequester { options = contracts.secret.mkRequester {
owner = "karakeep"; owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets. # 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 { options = contracts.secret.mkRequester {
owner = "karakeep"; owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets. # 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 { (lib.mkIf cfg.enable {
services.karakeep = { services.karakeep = {
enable = true; enable = true;
@ -168,7 +189,8 @@ in
extraEnvironment = { extraEnvironment = {
PORT = toString cfg.port; PORT = toString cfg.port;
DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS
} // cfg.environment; }
// cfg.environment;
}; };
shb.nginx.vhosts = [ shb.nginx.vhosts = [
@ -182,19 +204,23 @@ in
# This is needed otherwise the MEILI_MASTER_KEY is generated randomly on first start # This is needed otherwise the MEILI_MASTER_KEY is generated randomly on first start
# instead of using the value from the cfg.meilisearchMasterKey option. # instead of using the value from the cfg.meilisearchMasterKey option.
systemd.services.karakeep-init = { systemd.services.karakeep-init = {
script = lib.mkForce ((lib.shb.replaceSecrets { script = lib.mkForce (
(lib.shb.replaceSecrets {
userConfig = { userConfig = {
MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path; MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path;
NEXTAUTH_SECRET.source = cfg.nextauthSecret.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; OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path;
}; };
resultPath = "/var/lib/karakeep/settings.env"; resultPath = "/var/lib/karakeep/settings.env";
generator = lib.shb.toEnvVar; generator = lib.shb.toEnvVar;
}) + '' })
+ ''
export DATA_DIR="$STATE_DIRECTORY" export DATA_DIR="$STATE_DIRECTORY"
exec ${config.services.karakeep.package}/lib/karakeep/migrate exec ${config.services.karakeep.package}/lib/karakeep/migrate
''); ''
);
}; };
}) })
(lib.mkIf cfg.enable { (lib.mkIf cfg.enable {
@ -222,7 +248,11 @@ in
{ {
client_id = cfg.sso.clientID; client_id = cfg.sso.clientID;
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
scopes = [ "openid" "email" "profile" ]; scopes = [
"openid"
"email"
"profile"
];
authorization_policy = "karakeep"; authorization_policy = "karakeep";
redirect_uris = [ redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/api/auth/callback/custom" "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 let
cfg = config.shb.nextcloud; cfg = config.shb.nextcloud;
@ -7,12 +12,17 @@ let
fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}"; fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}";
protocol = if !(isNull cfg.ssl) then "https" else "http"; 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; 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"; occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ";
in in
@ -71,7 +81,10 @@ in
version = lib.mkOption { version = lib.mkOption {
description = "Nextcloud version to choose from."; description = "Nextcloud version to choose from.";
type = lib.types.enum [ 31 32 ]; type = lib.types.enum [
31
32
];
default = 31; default = 31;
}; };
@ -105,7 +118,6 @@ in
}; };
}; };
maxUploadSize = lib.mkOption { maxUploadSize = lib.mkOption {
default = "4G"; default = "4G";
type = lib.types.str; type = lib.types.str;
@ -337,7 +349,8 @@ in
userLocalMount = lib.mkOption { userLocalMount = lib.mkOption {
default = null; default = null;
description = "If set, adds a local mount as external storage."; 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 = { options = {
directory = lib.mkOption { directory = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -354,10 +367,14 @@ in
Path of the mount in Nextcloud. Use `/` to mount as the root. Path of the mount in Nextcloud. Use `/` to mount as the root.
''; '';
default = ""; default = "";
example = [ "home" "/" ]; example = [
"home"
"/"
];
}; };
}; };
}); }
);
}; };
}; };
}; };
@ -371,7 +388,8 @@ in
the given host. the given host.
''; '';
default = { }; default = { };
type = lib.types.nullOr (lib.types.submodule { type = lib.types.nullOr (
lib.types.submodule {
options = { options = {
enable = lib.mkEnableOption "LDAP app."; enable = lib.mkEnableOption "LDAP app.";
@ -429,7 +447,8 @@ in
default = 50; default = 50;
}; };
}; };
}); }
);
}; };
sso = lib.mkOption { sso = lib.mkOption {
@ -469,7 +488,10 @@ in
}; };
authorization_policy = lib.mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -491,7 +513,6 @@ in
}; };
}; };
secretForAuthelia = lib.mkOption { secretForAuthelia = lib.mkOption {
description = "OIDC shared secret. Content must be the same as `secretFile` option."; description = "OIDC shared secret. Content must be the same as `secretFile` option.";
type = lib.types.submodule { type = lib.types.submodule {
@ -502,7 +523,6 @@ in
}; };
}; };
fallbackDefaultAuth = lib.mkOption { fallbackDefaultAuth = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
description = '' description = ''
@ -599,7 +619,6 @@ in
''; '';
}; };
backup = lib.mkOption { backup = lib.mkOption {
description = '' description = ''
Backup configuration. Backup configuration.
@ -726,9 +745,11 @@ in
extraAppsEnable = true; extraAppsEnable = true;
appstoreEnable = true; appstoreEnable = true;
settings = let settings =
let
protocol = if !(isNull cfg.ssl) then "https" else "http"; protocol = if !(isNull cfg.ssl) then "https" else "http";
in { in
{
"default_phone_region" = cfg.defaultPhoneRegion; "default_phone_region" = cfg.defaultPhoneRegion;
"overwrite.cli.url" = "${protocol}://${fqdn}"; "overwrite.cli.url" = "${protocol}://${fqdn}";
@ -773,7 +794,8 @@ in
"redis.session.locking_enabled" = "1"; "redis.session.locking_enabled" = "1";
"redis.session.lock_retries" = "-1"; "redis.session.lock_retries" = "-1";
"redis.session.lock_wait_time" = "10000"; "redis.session.lock_wait_time" = "10000";
} // lib.optionalAttrs (! (isNull cfg.tracing)) { }
// lib.optionalAttrs (!(isNull cfg.tracing)) {
# "xdebug.remote_enable" = "on"; # "xdebug.remote_enable" = "on";
# "xdebug.remote_host" = "127.0.0.1"; # "xdebug.remote_host" = "127.0.0.1";
# "xdebug.remote_port" = "9000"; # "xdebug.remote_port" = "9000";
@ -837,7 +859,9 @@ in
port = cfg.phpFpmPrometheusExporter.port; port = cfg.phpFpmPrometheusExporter.port;
listenAddress = "127.0.0.1"; listenAddress = "127.0.0.1";
extraFlags = [ 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"
]; ];
}; };
@ -856,13 +880,15 @@ in
services.prometheus.scrapeConfigs = [ services.prometheus.scrapeConfigs = [
{ {
job_name = "phpfpm-nextcloud"; job_name = "phpfpm-nextcloud";
static_configs = [{ static_configs = [
{
targets = [ "127.0.0.1:${toString cfg.phpFpmPrometheusExporter.port}" ]; targets = [ "127.0.0.1:${toString cfg.phpFpmPrometheusExporter.port}" ];
labels = { labels = {
"hostname" = config.networking.hostName; "hostname" = config.networking.hostName;
"domain" = cfg.domain; "domain" = cfg.domain;
}; };
}]; }
];
} }
]; ];
}) })
@ -964,7 +990,8 @@ in
systemd.services.nextcloud-setup.script = '' systemd.services.nextcloud-setup.script = ''
${occ} app:install files_external || : ${occ} app:install files_external || :
${occ} app:enable files_external ${occ} app:enable files_external
'' + lib.optionalString (cfg.apps.externalStorage.userLocalMount != null) ( ''
+ lib.optionalString (cfg.apps.externalStorage.userLocalMount != null) (
let let
cfg' = cfg.apps.externalStorage.userLocalMount; cfg' = cfg.apps.externalStorage.userLocalMount;
jq = "${pkgs.jq}/bin/jq"; jq = "${pkgs.jq}/bin/jq";
@ -979,7 +1006,8 @@ in
null::null \ null::null \
--config datadir='${cfg'.directory}' --config datadir='${cfg'.directory}'
fi fi
''); ''
);
}) })
(lib.mkIf (cfg.enable && cfg.apps.ldap.enable) { (lib.mkIf (cfg.enable && cfg.apps.ldap.enable) {
@ -988,7 +1016,8 @@ in
let let
cfg' = cfg.apps.ldap; cfg' = cfg.apps.ldap;
cID = "s" + toString cfg'.configID; cID = "s" + toString cfg'.configID;
in '' in
''
${occ} app:install user_ldap || : ${occ} app:install user_ldap || :
${occ} app:enable user_ldap ${occ} app:enable user_ldap
@ -1055,7 +1084,8 @@ in
''; '';
}) })
(let (
let
scopes = [ scopes = [
"openid" "openid"
"profile" "profile"
@ -1063,7 +1093,8 @@ in
"groups" "groups"
"nextcloud_userinfo" "nextcloud_userinfo"
]; ];
in lib.mkIf (cfg.enable && cfg.apps.sso.enable) { in
lib.mkIf (cfg.enable && cfg.apps.sso.enable) {
assertions = [ assertions = [
{ {
assertion = cfg.ssl != null; assertion = cfg.ssl != null;
@ -1080,8 +1111,7 @@ in
before = [ "nextcloud-setup.service" ]; before = [ "nextcloud-setup.service" ];
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
serviceConfig.User = "nextcloud"; serviceConfig.User = "nextcloud";
script = script = ''
''
mkdir -p ${cfg.dataDir}/config mkdir -p ${cfg.dataDir}/config
cat <<EOF > "${cfg.dataDir}/config/secretFile" cat <<EOF > "${cfg.dataDir}/config/secretFile"
{ {
@ -1157,7 +1187,8 @@ in
}; };
shb.authelia.extraDefinitions = { 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" = { shb.authelia.extraOidcClaimsPolicies."nextcloud_userinfo" = {
custom_claims = { custom_claims = {
@ -1187,11 +1218,11 @@ in
token_endpoint_auth_method = "client_secret_basic"; token_endpoint_auth_method = "client_secret_basic";
} }
]; ];
}) }
)
(lib.mkIf (cfg.enable && cfg.autoDisableMaintenanceModeOnStart) { (lib.mkIf (cfg.enable && cfg.autoDisableMaintenanceModeOnStart) {
systemd.services.nextcloud-setup.preStart = systemd.services.nextcloud-setup.preStart = lib.mkBefore ''
lib.mkBefore ''
if [[ -e /var/lib/nextcloud/config/config.php ]]; then if [[ -e /var/lib/nextcloud/config/config.php ]]; then
${occ} maintenance:mode --no-interaction --quiet --off ${occ} maintenance:mode --no-interaction --quiet --off
fi fi
@ -1199,8 +1230,7 @@ in
}) })
(lib.mkIf (cfg.enable && cfg.alwaysApplyExpensiveMigrations) { (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 if [[ -e /var/lib/nextcloud/config/config.php ]]; then
${occ} maintenance:repair --include-expensive ${occ} maintenance:repair --include-expensive
fi fi
@ -1209,17 +1239,19 @@ in
# Great source of inspiration: # Great source of inspiration:
# https://github.com/Shawn8901/nix-configuration/blob/538c18d9ecbf7c7e649b1540c0d40881bada6690/modules/nixos/private/nextcloud/memories.nix#L226 # https://github.com/Shawn8901/nix-configuration/blob/538c18d9ecbf7c7e649b1540c0d40881bada6690/modules/nixos/private/nextcloud/memories.nix#L226
(lib.mkIf cfg.apps.memories.enable (lib.mkIf cfg.apps.memories.enable (
(let let
cfg' = cfg.apps.memories; cfg' = cfg.apps.memories;
exiftool = pkgs.exiftool.overrideAttrs (f: p: { exiftool = pkgs.exiftool.overrideAttrs (
f: p: {
version = "12.70"; version = "12.70";
src = pkgs.fetchurl { src = pkgs.fetchurl {
url = "https://exiftool.org/Image-ExifTool-12.70.tar.gz"; url = "https://exiftool.org/Image-ExifTool-12.70.tar.gz";
hash = "sha256-TLJSJEXMPj870TkExq6uraX8Wl4kmNerrSlX3LQsr/4="; hash = "sha256-TLJSJEXMPj870TkExq6uraX8Wl4kmNerrSlX3LQsr/4=";
}; };
}); }
);
in in
{ {
assertions = [ assertions = [
@ -1265,10 +1297,11 @@ in
DeviceAllow = [ "/dev/dri/renderD128 rwm" ]; DeviceAllow = [ "/dev/dri/renderD128 rwm" ];
PrivateDevices = lib.mkForce false; PrivateDevices = lib.mkForce false;
}; };
})) }
))
(lib.mkIf cfg.apps.recognize.enable (lib.mkIf cfg.apps.recognize.enable (
(let let
cfg' = cfg.apps.recognize; cfg' = cfg.apps.recognize;
in in
{ {
@ -1276,8 +1309,7 @@ in
inherit (nextcloudApps) recognize; 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 nice_binary --value ${pkgs.coreutils}/bin/nice
${occ} config:app:set recognize node_binary --value ${pkgs.nodejs}/bin/node ${occ} config:app:set recognize node_binary --value ${pkgs.nodejs}/bin/node
${occ} config:app:set recognize faces.enabled --value true ${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.enabled --value true
${occ} config:app:set recognize musicnn.batchSize --value 100 ${occ} config:app:set recognize musicnn.batchSize --value 100
''; '';
})) }
))
]; ];
} }

View file

@ -1,11 +1,22 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.shb.open-webui; cfg = config.shb.open-webui;
contracts = pkgs.callPackage ../contracts { }; contracts = pkgs.callPackage ../contracts { };
roleClaim = "openwebui_groups"; roleClaim = "openwebui_groups";
oauthScopes = [ "openid" "email" "profile" "groups" "${roleClaim}" ]; oauthScopes = [
"openid"
"email"
"profile"
"groups"
"${roleClaim}"
];
in in
{ {
imports = [ imports = [
@ -103,7 +114,10 @@ in
}; };
authorization_policy = lib.mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -149,7 +163,8 @@ in
}; };
}; };
config = (lib.mkMerge [ config = (
lib.mkMerge [
(lib.mkIf cfg.enable { (lib.mkIf cfg.enable {
users.users.open-webui = { users.users.open-webui = {
isSystemUser = true; isSystemUser = true;
@ -173,7 +188,8 @@ in
SCARF_NO_ANALYTICS = "True"; SCARF_NO_ANALYTICS = "True";
ENABLE_VERSION_UPDATE_CHECK = "False"; ENABLE_VERSION_UPDATE_CHECK = "False";
} // cfg.environment; }
// cfg.environment;
}; };
systemd.services.open-webui.path = [ systemd.services.open-webui.path = [
@ -268,5 +284,6 @@ in
requiredBy = [ "open-webui.service" ]; requiredBy = [ "open-webui.service" ];
}; };
}) })
]); ]
);
} }

View file

@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
let let
cfg = config.shb.pinchflat; cfg = config.shb.pinchflat;
@ -57,7 +62,10 @@ in
}; };
timeZone = lib.mkOption { 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."; description = "Timezone of this instance.";
example = "America/Los_Angeles"; example = "America/Los_Angeles";
}; };
@ -99,7 +107,10 @@ in
}; };
authorization_policy = lib.mkOption { 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."; description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor"; default = "one_factor";
}; };
@ -142,7 +153,9 @@ in
# This should be using a contract instead of setting the option directly. # This should be using a contract instead of setting the option directly.
shb.lldap = lib.mkIf config.shb.lldap.enable { shb.lldap = lib.mkIf config.shb.lldap.enable {
ensureGroups = { ${cfg.ldap.userGroup} = {}; }; ensureGroups = {
${cfg.ldap.userGroup} = { };
};
}; };
systemd.services.pinchflat-pre = { systemd.services.pinchflat-pre = {

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.shb.vaultwarden; cfg = config.shb.vaultwarden;
@ -7,7 +12,11 @@ let
fqdn = "${cfg.subdomain}.${cfg.domain}"; 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 in
{ {
imports = [ imports = [
@ -55,7 +64,10 @@ in
mode = "0440"; mode = "0440";
owner = "vaultwarden"; owner = "vaultwarden";
group = "postgres"; group = "postgres";
restartUnits = [ "vaultwarden.service" "postgresql.service" ]; restartUnits = [
"vaultwarden.service"
"postgresql.service"
];
}; };
}; };
}; };
@ -63,7 +75,8 @@ in
smtp = lib.mkOption { smtp = lib.mkOption {
description = "SMTP options."; description = "SMTP options.";
default = null; default = null;
type = lib.types.nullOr (lib.types.submodule { type = lib.types.nullOr (
lib.types.submodule {
options = { options = {
from_address = lib.mkOption { from_address = lib.mkOption {
type = lib.types.str; type = lib.types.str;
@ -80,7 +93,11 @@ in
description = "SMTP host to send the emails to."; description = "SMTP host to send the emails to.";
}; };
security = lib.mkOption { 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."; description = "Security expected by SMTP host.";
default = "starttls"; default = "starttls";
}; };
@ -109,7 +126,8 @@ in
}; };
}; };
}; };
}); }
);
}; };
mount = lib.mkOption { mount = lib.mkOption {
@ -127,7 +145,9 @@ in
``` ```
''; '';
readOnly = true; readOnly = true;
default = { path = dataFolder; }; default = {
path = dataFolder;
};
}; };
backup = lib.mkOption { backup = lib.mkOption {
@ -170,7 +190,8 @@ in
ROCKET_LOG = if cfg.debug then "trace" else "info"; ROCKET_LOG = if cfg.debug then "trace" else "info";
ROCKET_ADDRESS = "127.0.0.1"; ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = cfg.port; ROCKET_PORT = cfg.port;
} // lib.optionalAttrs (cfg.smtp != null) { }
// lib.optionalAttrs (cfg.smtp != null) {
SMTP_FROM = cfg.smtp.from_address; SMTP_FROM = cfg.smtp.from_address;
SMTP_FROM_NAME = cfg.smtp.from_name; SMTP_FROM_NAME = cfg.smtp.from_name;
SMTP_HOST = cfg.smtp.host; SMTP_HOST = cfg.smtp.host;
@ -189,12 +210,12 @@ in
]; ];
# Needed to be able to write template config. # Needed to be able to write template config.
systemd.services.vaultwarden.serviceConfig.ProtectHome = lib.mkForce false; systemd.services.vaultwarden.serviceConfig.ProtectHome = lib.mkForce false;
systemd.services.vaultwarden.preStart = systemd.services.vaultwarden.preStart = lib.shb.replaceSecrets {
lib.shb.replaceSecrets {
userConfig = { userConfig = {
DATABASE_URL.source = cfg.databasePassword.result.path; DATABASE_URL.source = cfg.databasePassword.result.path;
DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden"; 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; SMTP_PASSWORD.source = cfg.smtp.password.result.path;
}; };
resultPath = "${dataFolder}/vaultwarden.env"; resultPath = "${dataFolder}/vaultwarden.env";
@ -203,7 +224,12 @@ in
shb.nginx.vhosts = [ 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}"; upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}";
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [ autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
{ {

View file

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

View file

@ -24,7 +24,9 @@ in
configWithTemplates = lib.shb.withReplacements userConfig; 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; replacements = lib.shb.getReplacements userConfig;
@ -54,7 +56,8 @@ in
in in
lib.shb.runNixOSTest { lib.shb.runNixOSTest {
name = "lib-template"; name = "lib-template";
nodes.machine = { config, pkgs, ... }: nodes.machine =
{ config, pkgs, ... }:
{ {
imports = [ imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix") (pkgs'.path + "/nixos/modules/profiles/headless.nix")
@ -62,7 +65,12 @@ in
{ {
options = { options = {
libtest.config = lib.mkOption { 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 import json
from collections import ChainMap from collections import ChainMap
from xml.etree import ElementTree from xml.etree import ElementTree

View file

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

View file

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

View file

@ -6,7 +6,9 @@ in
peerWithoutUser = lib.shb.runNixOSTest { peerWithoutUser = lib.shb.runNixOSTest {
name = "postgresql-peerWithoutUser"; name = "postgresql-peerWithoutUser";
nodes.machine = { config, pkgs, ... }: { nodes.machine =
{ config, pkgs, ... }:
{
imports = [ imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix") (pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -21,7 +23,9 @@ in
]; ];
}; };
testScript = { nodes, ... }: '' testScript =
{ nodes, ... }:
''
start_all() start_all()
machine.wait_for_unit("postgresql.service") machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432) machine.wait_for_open_port(5432)
@ -43,7 +47,9 @@ in
peerAuth = lib.shb.runNixOSTest { peerAuth = lib.shb.runNixOSTest {
name = "postgresql-peerAuth"; name = "postgresql-peerAuth";
nodes.machine = { config, pkgs, ... }: { nodes.machine =
{ config, pkgs, ... }:
{
imports = [ imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix") (pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -65,7 +71,9 @@ in
]; ];
}; };
testScript = { nodes, ... }: '' testScript =
{ nodes, ... }:
''
start_all() start_all()
machine.wait_for_unit("postgresql.service") machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432) machine.wait_for_open_port(5432)
@ -93,7 +101,9 @@ in
tcpIPWithoutPasswordAuth = lib.shb.runNixOSTest { tcpIPWithoutPasswordAuth = lib.shb.runNixOSTest {
name = "postgresql-tcpIpWithoutPasswordAuth"; name = "postgresql-tcpIpWithoutPasswordAuth";
nodes.machine = { config, pkgs, ... }: { nodes.machine =
{ config, pkgs, ... }:
{
imports = [ imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix") (pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -109,7 +119,9 @@ in
]; ];
}; };
testScript = { nodes, ... }: '' testScript =
{ nodes, ... }:
''
start_all() start_all()
machine.wait_for_unit("postgresql.service") machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432) machine.wait_for_open_port(5432)
@ -131,7 +143,9 @@ in
tcpIPPasswordAuth = lib.shb.runNixOSTest { tcpIPPasswordAuth = lib.shb.runNixOSTest {
name = "postgresql-tcpIPPasswordAuth"; name = "postgresql-tcpIPPasswordAuth";
nodes.machine = { config, pkgs, ... }: { nodes.machine =
{ config, pkgs, ... }:
{
imports = [ imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix") (pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
@ -158,7 +172,9 @@ in
]; ];
}; };
testScript = { nodes, ... }: '' testScript =
{ nodes, ... }:
''
start_all() start_all()
machine.wait_for_unit("postgresql.service") machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432) machine.wait_for_open_port(5432)

View file

@ -2,10 +2,14 @@
let 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}"; name = "restic_backupAndRestore_${user}";
nodes.machine = { config, ... }: { nodes.machine =
{ config, ... }:
{
imports = [ imports = [
testLib.baseImports testLib.baseImports
@ -64,7 +68,8 @@ let
"/opt/files/B" "/opt/files/B"
]; ];
hooks.beforeBackup = ['' hooks.beforeBackup = [
''
echo $RUNTIME_DIRECTORY echo $RUNTIME_DIRECTORY
if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then
if ! [ -f /run/secrets_restic/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 exit 12
fi fi
fi fi
'']; ''
];
}; };
}; };
}; };
@ -87,11 +93,14 @@ let
extraPythonPackages = p: [ p.dictdiffer ]; extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true; skipTypeCheck = true;
testScript = { nodes, ... }: let testScript =
{ nodes, ... }:
let
provider = nodes.machine.shb.restic.instances."testinstance"; provider = nodes.machine.shb.restic.instances."testinstance";
backupService = provider.result.backupService; backupService = provider.result.backupService;
restoreScript = provider.result.restoreScript; restoreScript = provider.result.restoreScript;
in '' in
''
from dictdiffer import diff from dictdiffer import diff
def list_files(dir): def list_files(dir):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,17 @@
{ pkgs, lib, ... }: { pkgs, lib, ... }:
let let
anyOpt = default: lib.mkOption { anyOpt =
default:
lib.mkOption {
type = lib.types.anything; type = lib.types.anything;
inherit default; inherit default;
}; };
testConfig = m: testConfig =
m:
let let
cfg = (lib.evalModules { cfg =
(lib.evalModules {
specialArgs = { inherit pkgs; }; specialArgs = { inherit pkgs; };
modules = [ modules = [
{ {
@ -20,7 +24,8 @@ let
m m
]; ];
}).config; }).config;
in { in
{
inherit (cfg) systemd services; inherit (cfg) systemd services;
}; };
in in

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,16 +2,22 @@
let let
commonTestScript = lib.shb.mkScripts { commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.grocy.ssl); hasSSL = { node, ... }: !(isNull node.config.shb.grocy.ssl);
waitForServices = { ... }: [ waitForServices =
{ ... }:
[
"phpfpm-grocy.service" "phpfpm-grocy.service"
"nginx.service" "nginx.service"
]; ];
waitForUnixSocket = { node, ... }: [ waitForUnixSocket =
{ node, ... }:
[
node.config.services.phpfpm.pools.grocy.socket node.config.services.phpfpm.pools.grocy.socket
]; ];
}; };
basic = { config, ... }: { basic =
{ config, ... }:
{
imports = [ imports = [
lib.shb.baseModule lib.shb.baseModule
../../modules/services/grocy.nix ../../modules/services/grocy.nix
@ -27,7 +33,9 @@ let
}; };
}; };
clientLogin = { config, ... }: { clientLogin =
{ config, ... }:
{
imports = [ imports = [
lib.shb.baseModule lib.shb.baseModule
lib.shb.clientLoginModule lib.shb.clientLoginModule
@ -44,19 +52,29 @@ let
passwordFieldLabelRegex = "Password"; passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "OK"; loginButtonNameRegex = "OK";
testLoginWith = [ 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()" "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_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.get_by_role('button', name=re.compile('OK'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Grocy'))" "expect(page).to_have_title(re.compile('Grocy'))"
]; } ];
}
]; ];
}; };
}; };
https = { config, ...}: { https =
{ config, ... }:
{
shb.grocy = { shb.grocy = {
ssl = config.shb.certs.certs.selfsigned.n; ssl = config.shb.certs.certs.selfsigned.n;
}; };

View file

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

View file

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

View file

@ -5,12 +5,25 @@ let
commonTestScript = lib.shb.accessScript { commonTestScript = lib.shb.accessScript {
hasSSL = { node, ... }: !(isNull node.config.shb.immich.ssl); hasSSL = { node, ... }: !(isNull node.config.shb.immich.ssl);
waitForServices = { ... }: [ "immich-server.service" "postgresql.service" "nginx.service" ]; waitForServices =
waitForPorts = { ... }: [ 2283 80 ]; { ... }:
[
"immich-server.service"
"postgresql.service"
"nginx.service"
];
waitForPorts =
{ ... }:
[
2283
80
];
waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}" ]; waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}" ];
}; };
base = { config, ... }: { base =
{ config, ... }:
{
imports = [ imports = [
lib.shb.baseModule lib.shb.baseModule
../../modules/services/immich.nix ../../modules/services/immich.nix
@ -34,13 +47,17 @@ let
environment.systemPackages = [ pkgs.curl ]; environment.systemPackages = [ pkgs.curl ];
}; };
basic = { config, ... }: { basic =
{ config, ... }:
{
imports = [ base ]; imports = [ base ];
test.hasSSL = false; test.hasSSL = false;
}; };
https = { config, ... }: { https =
{ config, ... }:
{
imports = [ imports = [
base base
lib.shb.certs lib.shb.certs
@ -50,14 +67,18 @@ let
shb.immich.ssl = config.shb.certs.certs.selfsigned.n; shb.immich.ssl = config.shb.certs.certs.selfsigned.n;
}; };
backup = { config, ... }: { backup =
{ config, ... }:
{
imports = [ imports = [
https https
(lib.shb.backup config.shb.immich.backup) (lib.shb.backup config.shb.immich.backup)
]; ];
}; };
sso = { config, ... }: { sso =
{ config, ... }:
{
imports = [ imports = [
https https
lib.shb.ldap lib.shb.ldap
@ -135,10 +156,18 @@ in
nodes.server = 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); hasSSL = args: !(isNull args.node.config.shb.immich.ssl);
waitForServices = args: [ "immich-server.service" "postgresql.service" "nginx.service" ]; waitForServices = args: [
waitForPorts = args: [ 2283 80 ]; "immich-server.service"
"postgresql.service"
"nginx.service"
];
waitForPorts = args: [
2283
80
];
waitForUrls = args: [ "${args.proto_fqdn}" ]; waitForUrls = args: [ "${args.proto_fqdn}" ];
}).backup; }).backup;
}; };

View file

@ -4,17 +4,25 @@ let
commonTestScript = lib.shb.mkScripts { commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl); hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl);
waitForServices = { ... }: [ waitForServices =
{ ... }:
[
"jellyfin.service" "jellyfin.service"
"nginx.service" "nginx.service"
]; ];
waitForPorts = { node, ... }: [ waitForPorts =
{ node, ... }:
[
port port
]; ];
waitForUrls = { proto_fqdn, ... }: [ waitForUrls =
{ proto_fqdn, ... }:
[
"${proto_fqdn}/System/Info/Public" "${proto_fqdn}/System/Info/Public"
]; ];
extraScript = { node, ... }: '' extraScript =
{ node, ... }:
''
headers = unline_with(" ", """ headers = unline_with(" ", """
-H 'Content-Type: application/json' -H 'Content-Type: application/json'
-H 'Authorization: MediaBrowser Client="Android TV", Device="Nvidia Shield", DeviceId="ZQ9YQHHrUzk24vV", Version="0.15.3"' -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 = [ imports = [
lib.shb.baseModule lib.shb.baseModule
../../modules/services/jellyfin.nix ../../modules/services/jellyfin.nix
@ -65,7 +75,9 @@ let
]; ];
}; };
clientLogin = { config, ... }: { clientLogin =
{ config, ... }:
{
imports = [ imports = [
lib.shb.clientLoginModule lib.shb.clientLoginModule
]; ];
@ -103,7 +115,9 @@ let
}; };
}; };
https = { config, ... }: { https =
{ config, ... }:
{
shb.jellyfin = { shb.jellyfin = {
ssl = config.shb.certs.certs.selfsigned.n; ssl = config.shb.certs.certs.selfsigned.n;
}; };
@ -112,7 +126,9 @@ let
}; };
}; };
ldap = { config, ... }: { ldap =
{ config, ... }:
{
shb.jellyfin = { shb.jellyfin = {
ldap = { ldap = {
enable = true; enable = true;
@ -129,7 +145,9 @@ let
}; };
}; };
sso = { config, ... }: { sso =
{ config, ... }:
{
shb.jellyfin = { shb.jellyfin = {
sso = { sso = {
enable = true; enable = true;
@ -150,7 +168,10 @@ let
}; };
}; };
jellyfinTest = name: { nodes, testScript }: lib.shb.runNixOSTest { jellyfinTest =
name:
{ nodes, testScript }:
lib.shb.runNixOSTest {
name = "jellyfin_${name}"; name = "jellyfin_${name}";
interactive.nodes.server = { interactive.nodes.server = {
@ -180,7 +201,9 @@ in
}; };
backup = jellyfinTest "backup" { backup = jellyfinTest "backup" {
nodes.server = { config, ... }: { nodes.server =
{ config, ... }:
{
imports = [ imports = [
basic basic
(lib.shb.backup config.shb.jellyfin.backup) (lib.shb.backup config.shb.jellyfin.backup)
@ -201,7 +224,9 @@ in
]; ];
}; };
nodes.client = { config, lib, ... }: { nodes.client =
{ config, lib, ... }:
{
imports = [ imports = [
lib.shb.baseModule lib.shb.baseModule
clientLogin clientLogin
@ -226,7 +251,9 @@ in
}; };
sso = jellyfinTest "sso" { sso = jellyfinTest "sso" {
nodes.server = { config, pkgs, ... }: { nodes.server =
{ config, pkgs, ... }:
{
imports = [ imports = [
basic basic
lib.shb.certs lib.shb.certs

View file

@ -7,19 +7,25 @@ let
commonTestScript = testLib.mkScripts { commonTestScript = testLib.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.karakeep.ssl); hasSSL = { node, ... }: !(isNull node.config.shb.karakeep.ssl);
waitForServices = { ... }: [ waitForServices =
{ ... }:
[
"karakeep-init.service" "karakeep-init.service"
"karakeep-browser.service" "karakeep-browser.service"
"karakeep-web.service" "karakeep-web.service"
"karakeep-workers.service" "karakeep-workers.service"
"nginx.service" "nginx.service"
]; ];
waitForPorts = { node, ... }: [ waitForPorts =
{ node, ... }:
[
node.config.shb.karakeep.port node.config.shb.karakeep.port
]; ];
}; };
basic = { config, ... }: { basic =
{ config, ... }:
{
imports = [ imports = [
testLib.baseModule testLib.baseModule
../../modules/blocks/hardcodedsecret.nix ../../modules/blocks/hardcodedsecret.nix
@ -53,13 +59,17 @@ let
}; };
}; };
https = { config, ... }: { https =
{ config, ... }:
{
shb.karakeep = { shb.karakeep = {
ssl = config.shb.certs.certs.selfsigned.n; ssl = config.shb.certs.certs.selfsigned.n;
}; };
}; };
ldap = { config, ... }: { ldap =
{ config, ... }:
{
shb.karakeep = { shb.karakeep = {
ldap = { ldap = {
userGroup = "user_group"; userGroup = "user_group";
@ -67,7 +77,9 @@ let
}; };
}; };
clientLoginSso = { config, ... }: { clientLoginSso =
{ config, ... }:
{
imports = [ imports = [
testLib.baseModule testLib.baseModule
testLib.clientLoginModule testLib.clientLoginModule
@ -85,36 +97,62 @@ let
passwordFieldLabelRegex = "Password"; passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n"; loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [ 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)" "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()" "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_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_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('new item')).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)" "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()" "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_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_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('new item')).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)" "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? # "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)" "expect(page.get_by_text(re.compile('login failed'))).to_be_visible(timeout=10000)"
]; } ];
}
]; ];
}; };
}; };
sso = { config, ... }: { sso =
{ config, ... }:
{
shb.karakeep = { shb.karakeep = {
sso = { sso = {
enable = true; enable = true;
@ -153,7 +191,9 @@ in
backup = pkgs.testers.runNixOSTest { backup = pkgs.testers.runNixOSTest {
name = "karakeep_backup"; name = "karakeep_backup";
nodes.server = { config, ... }: { nodes.server =
{ config, ... }:
{
imports = [ imports = [
basic basic
(testLib.backup config.shb.karakeep.backup) (testLib.backup config.shb.karakeep.backup)
@ -191,7 +231,9 @@ in
virtualisation.memorySize = 4096; virtualisation.memorySize = 4096;
}; };
nodes.server = { config, pkgs, ... }: { nodes.server =
{ config, pkgs, ... }:
{
imports = [ imports = [
basic basic
testLib.certs testLib.certs

View file

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

View file

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

View file

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

View file

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

View file

@ -2,11 +2,15 @@
let let
commonTestScript = lib.shb.mkScripts { commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.vaultwarden.ssl); hasSSL = { node, ... }: !(isNull node.config.shb.vaultwarden.ssl);
waitForServices = { ... }: [ waitForServices =
{ ... }:
[
"vaultwarden.service" "vaultwarden.service"
"nginx.service" "nginx.service"
]; ];
waitForPorts = { node, ... }: [ waitForPorts =
{ node, ... }:
[
8222 8222
5432 5432
]; ];
@ -17,7 +21,9 @@ let
# 4. go to the Vaultwarden /admin endpoint # 4. go to the Vaultwarden /admin endpoint
# 5. create a Vaultwarden user # 5. create a Vaultwarden user
# 6. now login with that new user to Vaultwarden # 6. now login with that new user to Vaultwarden
extraScript = { node, proto_fqdn, ... }: '' extraScript =
{ node, proto_fqdn, ... }:
''
with subtest("prelogin"): with subtest("prelogin"):
response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """ response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """
{"email": "me@example.com"} {"email": "me@example.com"}
@ -42,7 +48,9 @@ let
''; '';
}; };
basic = { config, ... }: { basic =
{ config, ... }:
{
test = { test = {
subdomain = "v"; subdomain = "v";
}; };
@ -64,7 +72,9 @@ let
# }; # };
}; };
https = { config, ... }: { https =
{ config, ... }:
{
shb.vaultwarden = { shb.vaultwarden = {
ssl = config.shb.certs.certs.selfsigned.n; ssl = config.shb.certs.certs.selfsigned.n;
}; };
@ -78,7 +88,9 @@ let
# # }; # # };
# }; # };
sso = { config, ... }: { sso =
{ config, ... }:
{
shb.vaultwarden = { shb.vaultwarden = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
}; };
@ -142,7 +154,9 @@ in
sso = lib.shb.runNixOSTest { sso = lib.shb.runNixOSTest {
name = "vaultwarden_sso"; name = "vaultwarden_sso";
nodes.server = { config, ... }: { nodes.server =
{ config, ... }:
{
imports = [ imports = [
lib.shb.baseModule lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix ../../modules/blocks/hardcodedsecret.nix
@ -159,12 +173,16 @@ in
nodes.client = { }; nodes.client = { };
testScript = commonTestScript.access.override { testScript = commonTestScript.access.override {
waitForPorts = { node, ... }: [ waitForPorts =
{ node, ... }:
[
8222 8222
5432 5432
9091 9091
]; ];
extraScript = { node, proto_fqdn, ... }: '' extraScript =
{ node, proto_fqdn, ... }:
''
with subtest("unauthenticated access is not granted to /admin"): 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") 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 { backup = lib.shb.runNixOSTest {
name = "vaultwarden_backup"; name = "vaultwarden_backup";
nodes.server = { config, ... }: { nodes.server =
{ config, ... }:
{
imports = [ imports = [
lib.shb.baseModule lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix ../../modules/blocks/hardcodedsecret.nix