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,17 +5,17 @@ let
targetPort = 2222;
in
{
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
];
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
boot.loader.grub.enable = true;
boot.kernelModules = [ "kvm-intel" ];
system.stateVersion = "22.11";
# Options above are generate by running nixos-generate-config on the VM.
# Needed otherwise deploy will say system won't be able to boot.
boot.loader.grub.device = "/dev/vdb";
# Needed to avoid getting into not available disk space in /boot.
@ -26,7 +26,10 @@ in
# Options above are needed to deploy in a VM.
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# We need to create the user we will deploy with.
users.users.${targetUser} = {
@ -41,9 +44,11 @@ in
# The user we're deploying with must be able to run sudo without password.
security.sudo.extraRules = [
{ users = [ targetUser ];
{
users = [ targetUser ];
commands = [
{ command = "ALL";
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];

View file

@ -6,148 +6,161 @@
sops-nix.url = "github:Mic92/sops-nix";
};
outputs = inputs@{ self, selfhostblocks, sops-nix }:
outputs =
inputs@{
self,
selfhostblocks,
sops-nix,
}:
let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
inherit (selfhostblocks.lib.${system}) pkgs;
basic = { config, ... }: {
imports = [
./configuration.nix
selfhostblocks.nixosModules.authelia
selfhostblocks.nixosModules.home-assistant
selfhostblocks.nixosModules.sops
selfhostblocks.nixosModules.ssl
sops-nix.nixosModules.default
];
basic =
{ config, ... }:
{
imports = [
./configuration.nix
selfhostblocks.nixosModules.authelia
selfhostblocks.nixosModules.home-assistant
selfhostblocks.nixosModules.sops
selfhostblocks.nixosModules.ssl
sops-nix.nixosModules.default
];
sops.defaultSopsFile = ./secrets.yaml;
sops.defaultSopsFile = ./secrets.yaml;
shb.home-assistant = {
enable = true;
domain = "example.com";
subdomain = "ha";
config = {
name = "SHB Home Assistant";
country.source = config.shb.sops.secret."home-assistant/country".result.path;
latitude.source = config.shb.sops.secret."home-assistant/latitude".result.path;
longitude.source = config.shb.sops.secret."home-assistant/longitude".result.path;
time_zone.source = config.shb.sops.secret."home-assistant/time_zone".result.path;
unit_system = "metric";
shb.home-assistant = {
enable = true;
domain = "example.com";
subdomain = "ha";
config = {
name = "SHB Home Assistant";
country.source = config.shb.sops.secret."home-assistant/country".result.path;
latitude.source = config.shb.sops.secret."home-assistant/latitude".result.path;
longitude.source = config.shb.sops.secret."home-assistant/longitude".result.path;
time_zone.source = config.shb.sops.secret."home-assistant/time_zone".result.path;
unit_system = "metric";
};
};
shb.sops.secret."home-assistant/country".request = {
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
shb.sops.secret."home-assistant/latitude".request = {
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
shb.sops.secret."home-assistant/longitude".request = {
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
shb.sops.secret."home-assistant/time_zone".request = {
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
nixpkgs.config.permittedInsecurePackages = [
"openssl-1.1.1w"
];
};
ldap =
{ config, ... }:
{
shb.lldap = {
enable = true;
domain = "example.com";
subdomain = "ldap";
ldapPort = 3890;
webUIListenPort = 17170;
dcdomain = "dc=example,dc=com";
ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result;
jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result;
};
shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request;
shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request;
shb.home-assistant.ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.webUIListenPort;
userGroup = "homeassistant_user";
};
};
shb.sops.secret."home-assistant/country".request = {
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
shb.sops.secret."home-assistant/latitude".request = {
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
shb.sops.secret."home-assistant/longitude".request = {
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
shb.sops.secret."home-assistant/time_zone".request = {
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
nixpkgs.config.permittedInsecurePackages = [
"openssl-1.1.1w"
];
};
ldap = { config, ... }: {
shb.lldap = {
enable = true;
domain = "example.com";
subdomain = "ldap";
ldapPort = 3890;
webUIListenPort = 17170;
dcdomain = "dc=example,dc=com";
ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result;
jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result;
};
shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request;
shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request;
shb.home-assistant.ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.webUIListenPort;
userGroup = "homeassistant_user";
};
};
sopsConfig = {
sops.age.keyFile = "/etc/sops/my_key";
environment.etc."sops/my_key".source = ./keys.txt;
};
in
{
nixosConfigurations = {
basic = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
basic
sopsConfig
];
};
ldap = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
basic
ldap
sopsConfig
];
};
{
nixosConfigurations = {
basic = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
basic
sopsConfig
];
};
colmena = {
meta = {
nixpkgs = import nixpkgs' {
system = "x86_64-linux";
};
specialArgs = inputs;
};
basic = { config, ... }: {
imports = [
basic
];
# Used by colmena to know which target host to deploy to.
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
ldap = { config, ... }: {
imports = [
basic
ldap
];
# Used by colmena to know which target host to deploy to.
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
ldap = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
basic
ldap
sopsConfig
];
};
};
colmena = {
meta = {
nixpkgs = import nixpkgs' {
system = "x86_64-linux";
};
specialArgs = inputs;
};
basic =
{ config, ... }:
{
imports = [
basic
];
# Used by colmena to know which target host to deploy to.
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
ldap =
{ config, ... }:
{
imports = [
basic
ldap
];
# Used by colmena to know which target host to deploy to.
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
};
};
}

View file

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

View file

@ -5,17 +5,17 @@ let
targetPort = 2222;
in
{
imports =
[ # Include the results of the hardware scan.
./hardware-configuration.nix
];
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
boot.loader.grub.enable = true;
boot.kernelModules = [ "kvm-intel" ];
system.stateVersion = "22.11";
# Options above are generate by running nixos-generate-config on the VM.
# Needed otherwise deploy will say system won't be able to boot.
boot.loader.grub.device = "/dev/vdb";
# Needed to avoid getting into not available disk space in /boot.
@ -27,7 +27,10 @@ in
# Options above are needed to deploy in a VM.
nix.settings.experimental-features = [ "nix-command" "flakes" ];
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# We need to create the user we will deploy with.
users.users.${targetUser} = {
@ -42,9 +45,11 @@ in
# The user we're deploying with must be able to run sudo without password.
security.sudo.extraRules = [
{ users = [ targetUser ];
{
users = [ targetUser ];
commands = [
{ command = "ALL";
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];

View file

@ -6,236 +6,256 @@
sops-nix.url = "github:Mic92/sops-nix";
};
outputs = inputs@{ self, selfhostblocks, sops-nix }:
outputs =
inputs@{
self,
selfhostblocks,
sops-nix,
}:
let
system = "x86_64-linux";
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
inherit (selfhostblocks.lib.${system}) pkgs;
basic = { config, ... }: {
imports = [
./configuration.nix
selfhostblocks.nixosModules.authelia
selfhostblocks.nixosModules.nextcloud-server
selfhostblocks.nixosModules.nginx
selfhostblocks.nixosModules.sops
selfhostblocks.nixosModules.ssl
sops-nix.nixosModules.default
];
basic =
{ config, ... }:
{
imports = [
./configuration.nix
selfhostblocks.nixosModules.authelia
selfhostblocks.nixosModules.nextcloud-server
selfhostblocks.nixosModules.nginx
selfhostblocks.nixosModules.sops
selfhostblocks.nixosModules.ssl
sops-nix.nixosModules.default
];
sops.defaultSopsFile = ./secrets.yaml;
sops.defaultSopsFile = ./secrets.yaml;
shb.nextcloud = {
enable = true;
domain = "example.com";
subdomain = "n";
dataDir = "/var/lib/nextcloud";
tracing = null;
defaultPhoneRegion = "US";
shb.nextcloud = {
enable = true;
domain = "example.com";
subdomain = "n";
dataDir = "/var/lib/nextcloud";
tracing = null;
defaultPhoneRegion = "US";
# This option is only needed because we do not access Nextcloud at the default port in the VM.
port = 8080;
# This option is only needed because we do not access Nextcloud at the default port in the VM.
port = 8080;
adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result;
adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result;
apps = {
previewgenerator.enable = true;
};
};
shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request;
# Set to true for more debug info with `journalctl -f -u nginx`.
shb.nginx.accessLog = true;
shb.nginx.debugLog = false;
};
ldap = { config, ... }: {
shb.lldap = {
enable = true;
domain = "example.com";
subdomain = "ldap";
ldapPort = 3890;
webUIListenPort = 17170;
dcdomain = "dc=example,dc=com";
ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result;
jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result;
};
shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request;
shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request;
shb.nextcloud.apps.ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminName = "admin";
adminPassword.result = config.shb.sops.secret."nextcloud/ldap_admin_password".result;
userGroup = "nextcloud_user";
};
shb.sops.secret."nextcloud/ldap_admin_password" = {
request = config.shb.nextcloud.apps.ldap.adminPassword.request;
settings.key = "lldap/user_password";
};
};
sso = { config, lib, ... }: {
shb.certs = {
cas.selfsigned.myca = {
name = "My CA";
};
certs.selfsigned = {
n = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "*.example.com";
group = "nginx";
apps = {
previewgenerator.enable = true;
};
};
};
shb.nextcloud = {
port = lib.mkForce null;
ssl = config.shb.certs.certs.selfsigned.n;
};
shb.lldap.ssl = config.shb.certs.certs.selfsigned.n;
shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request;
services.dnsmasq = {
enable = true;
settings = {
domain-needed = true;
# no-resolv = true;
bogus-priv = true;
address =
map (hostname: "/${hostname}/127.0.0.1") [
# Set to true for more debug info with `journalctl -f -u nginx`.
shb.nginx.accessLog = true;
shb.nginx.debugLog = false;
};
ldap =
{ config, ... }:
{
shb.lldap = {
enable = true;
domain = "example.com";
subdomain = "ldap";
ldapPort = 3890;
webUIListenPort = 17170;
dcdomain = "dc=example,dc=com";
ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result;
jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result;
};
shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request;
shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request;
shb.nextcloud.apps.ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminName = "admin";
adminPassword.result = config.shb.sops.secret."nextcloud/ldap_admin_password".result;
userGroup = "nextcloud_user";
};
shb.sops.secret."nextcloud/ldap_admin_password" = {
request = config.shb.nextcloud.apps.ldap.adminPassword.request;
settings.key = "lldap/user_password";
};
};
sso =
{ config, lib, ... }:
{
shb.certs = {
cas.selfsigned.myca = {
name = "My CA";
};
certs.selfsigned = {
n = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "*.example.com";
group = "nginx";
};
};
};
shb.nextcloud = {
port = lib.mkForce null;
ssl = config.shb.certs.certs.selfsigned.n;
};
shb.lldap.ssl = config.shb.certs.certs.selfsigned.n;
services.dnsmasq = {
enable = true;
settings = {
domain-needed = true;
# no-resolv = true;
bogus-priv = true;
address = map (hostname: "/${hostname}/127.0.0.1") [
"example.com"
"n.example.com"
"ldap.example.com"
"auth.example.com"
];
};
};
shb.authelia = {
enable = true;
domain = "example.com";
subdomain = "auth";
ssl = config.shb.certs.certs.selfsigned.n;
ldapPort = config.shb.lldap.ldapPort;
ldapHostname = "127.0.0.1";
dcdomain = config.shb.lldap.dcdomain;
secrets = {
jwtSecret.result = config.shb.sops.secret."authelia/jwt_secret".result;
ldapAdminPassword.result = config.shb.sops.secret."authelia/ldap_admin_password".result;
sessionSecret.result = config.shb.sops.secret."authelia/session_secret".result;
storageEncryptionKey.result = config.shb.sops.secret."authelia/storage_encryption_key".result;
identityProvidersOIDCHMACSecret.result = config.shb.sops.secret."authelia/hmac_secret".result;
identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secret."authelia/private_key".result;
};
};
shb.sops.secret."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request;
shb.sops.secret."authelia/ldap_admin_password" = {
request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.key = "lldap/user_password";
};
shb.sops.secret."authelia/session_secret".request =
config.shb.authelia.secrets.sessionSecret.request;
shb.sops.secret."authelia/storage_encryption_key".request =
config.shb.authelia.secrets.storageEncryptionKey.request;
shb.sops.secret."authelia/hmac_secret".request =
config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
shb.sops.secret."authelia/private_key".request =
config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
shb.nextcloud.apps.sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
clientID = "nextcloud";
fallbackDefaultAuth = true;
secret.result = config.shb.sops.secret."nextcloud/sso/secret".result;
secretForAuthelia.result = config.shb.sops.secret."authelia/nextcloud_sso_secret".result;
};
shb.sops.secret."nextcloud/sso/secret".request = config.shb.nextcloud.apps.sso.secret.request;
shb.sops.secret."authelia/nextcloud_sso_secret" = {
request = config.shb.nextcloud.apps.sso.secretForAuthelia.request;
settings.key = "nextcloud/sso/secret";
};
};
shb.authelia = {
enable = true;
domain = "example.com";
subdomain = "auth";
ssl = config.shb.certs.certs.selfsigned.n;
ldapPort = config.shb.lldap.ldapPort;
ldapHostname = "127.0.0.1";
dcdomain = config.shb.lldap.dcdomain;
secrets = {
jwtSecret.result = config.shb.sops.secret."authelia/jwt_secret".result;
ldapAdminPassword.result = config.shb.sops.secret."authelia/ldap_admin_password".result;
sessionSecret.result = config.shb.sops.secret."authelia/session_secret".result;
storageEncryptionKey.result = config.shb.sops.secret."authelia/storage_encryption_key".result;
identityProvidersOIDCHMACSecret.result = config.shb.sops.secret."authelia/hmac_secret".result;
identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secret."authelia/private_key".result;
};
};
shb.sops.secret."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request;
shb.sops.secret."authelia/ldap_admin_password" = {
request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.key = "lldap/user_password";
};
shb.sops.secret."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request;
shb.sops.secret."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request;
shb.sops.secret."authelia/hmac_secret".request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
shb.sops.secret."authelia/private_key".request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
shb.nextcloud.apps.sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
clientID = "nextcloud";
fallbackDefaultAuth = true;
secret.result = config.shb.sops.secret."nextcloud/sso/secret".result;
secretForAuthelia.result = config.shb.sops.secret."authelia/nextcloud_sso_secret".result;
};
shb.sops.secret."nextcloud/sso/secret".request = config.shb.nextcloud.apps.sso.secret.request;
shb.sops.secret."authelia/nextcloud_sso_secret" = {
request = config.shb.nextcloud.apps.sso.secretForAuthelia.request;
settings.key = "nextcloud/sso/secret";
};
};
sopsConfig = {
sops.age.keyFile = "/etc/sops/my_key";
environment.etc."sops/my_key".source = ./keys.txt;
};
in
{
nixosConfigurations = {
basic = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
sopsConfig
basic
];
};
ldap = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
sopsConfig
basic
ldap
];
};
sso = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
sopsConfig
basic
ldap
sso
];
};
{
nixosConfigurations = {
basic = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
sopsConfig
basic
];
};
colmena = {
meta = {
nixpkgs = import nixpkgs' {
system = "x86_64-linux";
};
specialArgs = inputs;
};
basic = { config, ... }: {
imports = [
basic
];
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
ldap = { config, ... }: {
imports = [
basic
ldap
];
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
sso = { config, ... }: {
imports = [
basic
ldap
sso
];
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
ldap = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
sopsConfig
basic
ldap
];
};
sso = pkgs.nixosSystem {
system = "x86_64-linux";
modules = [
sopsConfig
basic
ldap
sso
];
};
};
colmena = {
meta = {
nixpkgs = import nixpkgs' {
system = "x86_64-linux";
};
specialArgs = inputs;
};
basic =
{ config, ... }:
{
imports = [
basic
];
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
ldap =
{ config, ... }:
{
imports = [
basic
ldap
];
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
sso =
{ config, ... }:
{
imports = [
basic
ldap
sso
];
deployment = {
targetHost = "example";
targetUser = "nixos";
targetPort = 2222;
};
};
};
};
}

View file

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

View file

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

555
flake.nix
View file

@ -11,66 +11,89 @@
};
};
outputs = inputs@{ self, nixpkgs, nix-flake-tests, flake-utils, nmdsrc, ... }: flake-utils.lib.eachDefaultSystem (system:
let
originPkgs = nixpkgs.legacyPackages.${system};
shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [
# Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged.
./patches/lldap.patch
outputs =
inputs@{
self,
nixpkgs,
nix-flake-tests,
flake-utils,
nmdsrc,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
originPkgs = nixpkgs.legacyPackages.${system};
shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [
# Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged.
./patches/lldap.patch
# Leaving commented out as an example.
# (originPkgs.fetchpatch {
# url = "https://github.com/NixOS/nixpkgs/pull/317107.patch";
# hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk=";
# })
];
patchNixpkgs = {
nixpkgs,
patches,
system,
}: nixpkgs.legacyPackages.${system}.applyPatches {
name = "nixpkgs-patched";
src = nixpkgs;
inherit patches;
};
patchedNixpkgs = (patchNixpkgs {
nixpkgs = inputs.nixpkgs;
patches = shbPatches;
inherit system;
});
pkgs = import patchedNixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [
(final: prev: {
lib = prev.lib // {
shb = self.lib.${system};
evalModules = args: ((prev.lib.makeOverridable prev.lib.evalModules) args).override (prevAttrs: {
specialArgs = (prevAttrs.specialArgs or {}) // { inherit (pkgs) lib; };
});
};
nixosSystem = args: ((prev.lib.makeOverridable (import "${patchedNixpkgs}/nixos/lib/eval-config.nix")) args).override (prevAttrs: {
inherit (pkgs) lib;
});
})
# Leaving commented out as an example.
# (originPkgs.fetchpatch {
# url = "https://github.com/NixOS/nixpkgs/pull/317107.patch";
# hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk=";
# })
];
};
patchNixpkgs =
{
nixpkgs,
patches,
system,
}:
nixpkgs.legacyPackages.${system}.applyPatches {
name = "nixpkgs-patched";
src = nixpkgs;
inherit patches;
};
patchedNixpkgs = (
patchNixpkgs {
nixpkgs = inputs.nixpkgs;
patches = shbPatches;
inherit system;
}
);
pkgs = import patchedNixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [
(final: prev: {
lib = prev.lib // {
shb = self.lib.${system};
evalModules =
args:
((prev.lib.makeOverridable prev.lib.evalModules) args).override (prevAttrs: {
specialArgs = (prevAttrs.specialArgs or { }) // {
inherit (pkgs) lib;
};
});
};
nixosSystem =
args:
((prev.lib.makeOverridable (import "${patchedNixpkgs}/nixos/lib/eval-config.nix")) args).override
(prevAttrs: {
inherit (pkgs) lib;
});
})
];
};
# The contract dummies are used to show options for contracts.
contractDummyModules = [
modules/contracts/backup/dummyModule.nix
modules/contracts/ssl/dummyModule.nix
];
in
# The contract dummies are used to show options for contracts.
contractDummyModules = [
modules/contracts/backup/dummyModule.nix
modules/contracts/ssl/dummyModule.nix
];
in
{
formatter = pkgs.nixfmt-tree;
packages.manualHtml = pkgs.callPackage ./docs {
inherit nmdsrc;
allModules = self.nixosModules.default.imports
++ [
self.nixosModules.sops
] ++ contractDummyModules;
allModules =
self.nixosModules.default.imports
++ [
self.nixosModules.sops
]
++ contractDummyModules;
release = builtins.readFile ./VERSION;
substituteVersionIn = [
@ -82,7 +105,10 @@
"blocks/lldap" = ./modules/blocks/lldap.nix;
"blocks/ssl" = {
module = ./modules/blocks/ssl.nix;
optionRoot = [ "shb" "certs" ];
optionRoot = [
"shb"
"certs"
];
};
"blocks/mitmdump" = ./modules/blocks/mitmdump.nix;
"blocks/monitoring" = ./modules/blocks/monitoring.nix;
@ -99,154 +125,196 @@
"services/karakeep" = ./modules/services/karakeep.nix;
"services/nextcloud-server" = {
module = ./modules/services/nextcloud-server.nix;
optionRoot = [ "shb" "nextcloud" ];
optionRoot = [
"shb"
"nextcloud"
];
};
"services/open-webui" = ./modules/services/open-webui.nix;
"services/pinchflat" = ./modules/services/pinchflat.nix;
"services/vaultwarden" = ./modules/services/vaultwarden.nix;
"contracts/backup" = {
module = ./modules/contracts/backup/dummyModule.nix;
optionRoot = [ "shb" "contracts" "backup" ];
optionRoot = [
"shb"
"contracts"
"backup"
];
};
"contracts/databasebackup" = {
module = ./modules/contracts/databasebackup/dummyModule.nix;
optionRoot = [ "shb" "contracts" "databasebackup" ];
optionRoot = [
"shb"
"contracts"
"databasebackup"
];
};
"contracts/secret" = {
module = ./modules/contracts/secret/dummyModule.nix;
optionRoot = [ "shb" "contracts" "secret" ];
optionRoot = [
"shb"
"contracts"
"secret"
];
};
"contracts/ssl" = {
module = ./modules/contracts/ssl/dummyModule.nix;
optionRoot = [ "shb" "contracts" "ssl" ];
optionRoot = [
"shb"
"contracts"
"ssl"
];
};
};
};
# Documentation redirect generation tool - scans HTML files for anchor mappings
packages.generateRedirects =
let
# Python patch to inject redirect collector
pythonPatch = pkgs.writeText "nixos-render-docs-patch.py" ''
# Load redirect collector patch
try:
import sys, os
sys.path.insert(0, os.path.dirname(__file__) + '/..')
import missing_refs_collector
except Exception as e:
print(f"Warning: Failed to load redirect collector: {e}", file=sys.stderr)
'';
# Patched nixos-render-docs that collects redirects during HTML generation
nixos-render-docs-patched = pkgs.writeShellApplication {
name = "nixos-render-docs";
runtimeInputs = [ pkgs.nixos-render-docs ];
text = ''
TEMP_DIR=$(mktemp -d); trap 'rm -rf "$TEMP_DIR"' EXIT
cp -r ${pkgs.nixos-render-docs}/${pkgs.python3.sitePackages}/nixos_render_docs "$TEMP_DIR/"
chmod -R +w "$TEMP_DIR"
cp ${./docs/generate-redirects-nixos-render-docs.py} "$TEMP_DIR/missing_refs_collector.py"
echo '{}' > "$TEMP_DIR/empty_redirects.json"
cat ${pythonPatch} >> "$TEMP_DIR/nixos_render_docs/__init__.py"
ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
--redirects) ARGS+=("$1" "$TEMP_DIR/empty_redirects.json"); shift 2 ;;
*) ARGS+=("$1"); shift ;;
esac
done
export PYTHONPATH="$TEMP_DIR:''${PYTHONPATH:-}"
nixos-render-docs "''${ARGS[@]}"
packages.generateRedirects =
let
# Python patch to inject redirect collector
pythonPatch = pkgs.writeText "nixos-render-docs-patch.py" ''
# Load redirect collector patch
try:
import sys, os
sys.path.insert(0, os.path.dirname(__file__) + '/..')
import missing_refs_collector
except Exception as e:
print(f"Warning: Failed to load redirect collector: {e}", file=sys.stderr)
'';
};
in
(self.packages.${system}.manualHtml.override {
nixos-render-docs = nixos-render-docs-patched;
}).overrideAttrs (old: {
installPhase = ''
${old.installPhase}
ln -sf share/doc/selfhostblocks/redirects.json $out/redirects.json
'';
});
# Patched nixos-render-docs that collects redirects during HTML generation
nixos-render-docs-patched = pkgs.writeShellApplication {
name = "nixos-render-docs";
runtimeInputs = [ pkgs.nixos-render-docs ];
text = ''
TEMP_DIR=$(mktemp -d); trap 'rm -rf "$TEMP_DIR"' EXIT
cp -r ${pkgs.nixos-render-docs}/${pkgs.python3.sitePackages}/nixos_render_docs "$TEMP_DIR/"
chmod -R +w "$TEMP_DIR"
cp ${./docs/generate-redirects-nixos-render-docs.py} "$TEMP_DIR/missing_refs_collector.py"
echo '{}' > "$TEMP_DIR/empty_redirects.json"
cat ${pythonPatch} >> "$TEMP_DIR/nixos_render_docs/__init__.py"
ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
--redirects) ARGS+=("$1" "$TEMP_DIR/empty_redirects.json"); shift 2 ;;
*) ARGS+=("$1"); shift ;;
esac
done
export PYTHONPATH="$TEMP_DIR:''${PYTHONPATH:-}"
nixos-render-docs "''${ARGS[@]}"
'';
};
in
(self.packages.${system}.manualHtml.override {
nixos-render-docs = nixos-render-docs-patched;
}).overrideAttrs
(old: {
installPhase = ''
${old.installPhase}
ln -sf share/doc/selfhostblocks/redirects.json $out/redirects.json
'';
});
lib =
(pkgs.callPackage ./lib {})
// (pkgs.callPackage ./test/common.nix {})
(pkgs.callPackage ./lib { })
// (pkgs.callPackage ./test/common.nix { })
// {
contracts = pkgs.callPackage ./modules/contracts {};
contracts = pkgs.callPackage ./modules/contracts { };
patches = shbPatches;
inherit patchNixpkgs patchedNixpkgs pkgs;
};
checks =
let
inherit (pkgs.lib) foldl foldlAttrs removeAttrs mergeAttrs optionalAttrs;
inherit (pkgs.lib)
foldl
foldlAttrs
removeAttrs
mergeAttrs
optionalAttrs
;
importFiles = files:
map (m: pkgs.callPackage m {}) files;
importFiles = files: map (m: pkgs.callPackage m { }) files;
mergeTests = foldl mergeAttrs {};
mergeTests = foldl mergeAttrs { };
flattenAttrs = root: attrset: foldlAttrs (acc: name: value: acc // {
"${root}_${name}" = value;
}) {} attrset;
flattenAttrs =
root: attrset:
foldlAttrs (
acc: name: value:
acc
// {
"${root}_${name}" = value;
}
) { } attrset;
vm_test = name: path: flattenAttrs "vm_${name}" (
removeAttrs (pkgs.callPackage path {}) [ "override" "overrideDerivation" ]
);
in (optionalAttrs (system == "x86_64-linux") ({
modules = pkgs.lib.shb.check {
inherit pkgs;
tests =
mergeTests (importFiles [
vm_test =
name: path:
flattenAttrs "vm_${name}" (
removeAttrs (pkgs.callPackage path { }) [
"override"
"overrideDerivation"
]
);
in
(optionalAttrs (system == "x86_64-linux") (
{
modules = pkgs.lib.shb.check {
inherit pkgs;
tests = mergeTests (importFiles [
./test/modules/davfs.nix
# TODO: Make this not use IFD
./test/modules/lib.nix
]);
};
};
# TODO: Make this not use IFD
lib = nix-flake-tests.lib.check {
inherit pkgs;
tests = pkgs.callPackage ./test/modules/lib.nix {};
};
}
// (vm_test "arr" ./test/services/arr.nix)
// (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix)
// (vm_test "deluge" ./test/services/deluge.nix)
// (vm_test "forgejo" ./test/services/forgejo.nix)
// (vm_test "grocy" ./test/services/grocy.nix)
// (vm_test "hledger" ./test/services/hledger.nix)
// (vm_test "immich" ./test/services/immich.nix)
// (vm_test "homeassistant" ./test/services/home-assistant.nix)
// (vm_test "jellyfin" ./test/services/jellyfin.nix)
// (vm_test "karakeep" ./test/services/karakeep.nix)
// (vm_test "monitoring" ./test/services/monitoring.nix)
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
// (vm_test "open-webui" ./test/services/open-webui.nix)
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
# TODO: Make this not use IFD
lib = nix-flake-tests.lib.check {
inherit pkgs;
tests = pkgs.callPackage ./test/modules/lib.nix { };
};
}
// (vm_test "arr" ./test/services/arr.nix)
// (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix)
// (vm_test "deluge" ./test/services/deluge.nix)
// (vm_test "forgejo" ./test/services/forgejo.nix)
// (vm_test "grocy" ./test/services/grocy.nix)
// (vm_test "hledger" ./test/services/hledger.nix)
// (vm_test "immich" ./test/services/immich.nix)
// (vm_test "homeassistant" ./test/services/home-assistant.nix)
// (vm_test "jellyfin" ./test/services/jellyfin.nix)
// (vm_test "karakeep" ./test/services/karakeep.nix)
// (vm_test "monitoring" ./test/services/monitoring.nix)
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
// (vm_test "open-webui" ./test/services/open-webui.nix)
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
// (vm_test "authelia" ./test/blocks/authelia.nix)
// (vm_test "lldap" ./test/blocks/lldap.nix)
// (vm_test "lib" ./test/blocks/lib.nix)
// (vm_test "mitmdump" ./test/blocks/mitmdump.nix)
// (vm_test "postgresql" ./test/blocks/postgresql.nix)
// (vm_test "restic" ./test/blocks/restic.nix)
// (vm_test "ssl" ./test/blocks/ssl.nix)
// (vm_test "authelia" ./test/blocks/authelia.nix)
// (vm_test "lldap" ./test/blocks/lldap.nix)
// (vm_test "lib" ./test/blocks/lib.nix)
// (vm_test "mitmdump" ./test/blocks/mitmdump.nix)
// (vm_test "postgresql" ./test/blocks/postgresql.nix)
// (vm_test "restic" ./test/blocks/restic.nix)
// (vm_test "ssl" ./test/blocks/ssl.nix)
// (vm_test "contracts-backup" ./test/contracts/backup.nix)
// (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix)
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
// (vm_test "contracts-backup" ./test/contracts/backup.nix)
// (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix)
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
));
# To see the traces, run:
# nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
packages.playwright =
pkgs.callPackage ({ stdenvNoCC, makeWrapper, playwright }: stdenvNoCC.mkDerivation {
packages.playwright = pkgs.callPackage (
{
stdenvNoCC,
makeWrapper,
playwright,
}:
stdenvNoCC.mkDerivation {
name = "playwright";
src = playwright;
@ -261,97 +329,104 @@
--set PLAYWRIGHT_BROWSERS_PATH ${pkgs.playwright-driver.browsers} \
--set PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS true
'';
}) {};
}
) { };
# Run "nix run .#update-redirects" to regenerate docs/redirects.json
apps.update-redirects = {
type = "app";
program = "${pkgs.writeShellApplication {
name = "update-redirects";
runtimeInputs = [ pkgs.nix pkgs.jq ];
text = ''
echo "=== SelfHostBlocks Redirects Updater ==="
echo "Generating fresh ./docs/redirects.json..."
nix build .#generateRedirects || { echo "Error: Failed to generate redirects" >&2; exit 1; }
[[ -f result/redirects.json ]] || { echo "Error: Generated redirects file not found" >&2; exit 1; }
echo "Generated $(jq 'keys | length' result/redirects.json) redirects"
[[ -f docs/redirects.json ]] && cp docs/redirects.json docs/redirects.json.backup && echo "Created backup"
cp result/redirects.json docs/redirects.json
echo " Updated docs/redirects.json"
echo "To verify: nix build .#manualHtml"
'';
}}/bin/update-redirects";
program = "${
pkgs.writeShellApplication {
name = "update-redirects";
runtimeInputs = [
pkgs.nix
pkgs.jq
];
text = ''
echo "=== SelfHostBlocks Redirects Updater ==="
echo "Generating fresh ./docs/redirects.json..."
nix build .#generateRedirects || { echo "Error: Failed to generate redirects" >&2; exit 1; }
[[ -f result/redirects.json ]] || { echo "Error: Generated redirects file not found" >&2; exit 1; }
echo "Generated $(jq 'keys | length' result/redirects.json) redirects"
[[ -f docs/redirects.json ]] && cp docs/redirects.json docs/redirects.json.backup && echo "Created backup"
cp result/redirects.json docs/redirects.json
echo " Updated docs/redirects.json"
echo "To verify: nix build .#manualHtml"
'';
}
}/bin/update-redirects";
};
}
) // {
herculesCI.ciSystems = [ "x86_64-linux" ];
)
// {
herculesCI.ciSystems = [ "x86_64-linux" ];
nixosModules.default = {
imports = [
# blocks
self.nixosModules.authelia
self.nixosModules.davfs
self.nixosModules.hardcodedsecret
self.nixosModules.lldap
self.nixosModules.mitmdump
self.nixosModules.monitoring
self.nixosModules.nginx
self.nixosModules.postgresql
self.nixosModules.restic
self.nixosModules.ssl
self.nixosModules.tinyproxy
self.nixosModules.vpn
self.nixosModules.zfs
nixosModules.default = {
imports = [
# blocks
self.nixosModules.authelia
self.nixosModules.davfs
self.nixosModules.hardcodedsecret
self.nixosModules.lldap
self.nixosModules.mitmdump
self.nixosModules.monitoring
self.nixosModules.nginx
self.nixosModules.postgresql
self.nixosModules.restic
self.nixosModules.ssl
self.nixosModules.tinyproxy
self.nixosModules.vpn
self.nixosModules.zfs
# services
self.nixosModules.arr
self.nixosModules.audiobookshelf
self.nixosModules.deluge
self.nixosModules.forgejo
self.nixosModules.grocy
self.nixosModules.hledger
self.nixosModules.immich
self.nixosModules.home-assistant
self.nixosModules.jellyfin
self.nixosModules.karakeep
self.nixosModules.nextcloud-server
self.nixosModules.open-webui
self.nixosModules.pinchflat
self.nixosModules.vaultwarden
];
# services
self.nixosModules.arr
self.nixosModules.audiobookshelf
self.nixosModules.deluge
self.nixosModules.forgejo
self.nixosModules.grocy
self.nixosModules.hledger
self.nixosModules.immich
self.nixosModules.home-assistant
self.nixosModules.jellyfin
self.nixosModules.karakeep
self.nixosModules.nextcloud-server
self.nixosModules.open-webui
self.nixosModules.pinchflat
self.nixosModules.vaultwarden
];
};
nixosModules.authelia = modules/blocks/authelia.nix;
nixosModules.davfs = modules/blocks/davfs.nix;
nixosModules.hardcodedsecret = modules/blocks/hardcodedsecret.nix;
nixosModules.lldap = modules/blocks/lldap.nix;
nixosModules.mitmdump = modules/blocks/mitmdump.nix;
nixosModules.monitoring = modules/blocks/monitoring.nix;
nixosModules.nginx = modules/blocks/nginx.nix;
nixosModules.postgresql = modules/blocks/postgresql.nix;
nixosModules.restic = modules/blocks/restic.nix;
nixosModules.ssl = modules/blocks/ssl.nix;
nixosModules.sops = modules/blocks/sops.nix;
nixosModules.tinyproxy = modules/blocks/tinyproxy.nix;
nixosModules.vpn = modules/blocks/vpn.nix;
nixosModules.zfs = modules/blocks/zfs.nix;
nixosModules.arr = modules/services/arr.nix;
nixosModules.audiobookshelf = modules/services/audiobookshelf.nix;
nixosModules.deluge = modules/services/deluge.nix;
nixosModules.forgejo = modules/services/forgejo.nix;
nixosModules.grocy = modules/services/grocy.nix;
nixosModules.hledger = modules/services/hledger.nix;
nixosModules.immich = modules/services/immich.nix;
nixosModules.home-assistant = modules/services/home-assistant.nix;
nixosModules.jellyfin = modules/services/jellyfin.nix;
nixosModules.karakeep = modules/services/karakeep.nix;
nixosModules.nextcloud-server = modules/services/nextcloud-server.nix;
nixosModules.open-webui = modules/services/open-webui.nix;
nixosModules.pinchflat = modules/services/pinchflat.nix;
nixosModules.vaultwarden = modules/services/vaultwarden.nix;
};
nixosModules.authelia = modules/blocks/authelia.nix;
nixosModules.davfs = modules/blocks/davfs.nix;
nixosModules.hardcodedsecret = modules/blocks/hardcodedsecret.nix;
nixosModules.lldap = modules/blocks/lldap.nix;
nixosModules.mitmdump = modules/blocks/mitmdump.nix;
nixosModules.monitoring = modules/blocks/monitoring.nix;
nixosModules.nginx = modules/blocks/nginx.nix;
nixosModules.postgresql = modules/blocks/postgresql.nix;
nixosModules.restic = modules/blocks/restic.nix;
nixosModules.ssl = modules/blocks/ssl.nix;
nixosModules.sops = modules/blocks/sops.nix;
nixosModules.tinyproxy = modules/blocks/tinyproxy.nix;
nixosModules.vpn = modules/blocks/vpn.nix;
nixosModules.zfs = modules/blocks/zfs.nix;
nixosModules.arr = modules/services/arr.nix;
nixosModules.audiobookshelf = modules/services/audiobookshelf.nix;
nixosModules.deluge = modules/services/deluge.nix;
nixosModules.forgejo = modules/services/forgejo.nix;
nixosModules.grocy = modules/services/grocy.nix;
nixosModules.hledger = modules/services/hledger.nix;
nixosModules.immich = modules/services/immich.nix;
nixosModules.home-assistant = modules/services/home-assistant.nix;
nixosModules.jellyfin = modules/services/jellyfin.nix;
nixosModules.karakeep = modules/services/karakeep.nix;
nixosModules.nextcloud-server = modules/services/nextcloud-server.nix;
nixosModules.open-webui = modules/services/open-webui.nix;
nixosModules.pinchflat = modules/services/pinchflat.nix;
nixosModules.vaultwarden = modules/services/vaultwarden.nix;
};
}

View file

@ -9,7 +9,14 @@ rec {
# - resultPath is the location the config file should have on the filesystem.
# - generator is a function taking two arguments name and value and returning path in the nix
# nix store where the
replaceSecrets = { userConfig, resultPath, generator, user ? null, permissions ? "u=r,g=r,o=" }:
replaceSecrets =
{
userConfig,
resultPath,
generator,
user ? null,
permissions ? "u=r,g=r,o=",
}:
let
configWithTemplates = withReplacements userConfig;
@ -17,28 +24,47 @@ rec {
replacements = getReplacements userConfig;
in
replaceSecretsScript {
file = nonSecretConfigFile;
inherit resultPath replacements;
inherit user permissions;
};
replaceSecretsScript {
file = nonSecretConfigFile;
inherit resultPath replacements;
inherit user permissions;
};
replaceSecretsFormatAdapter = format: format.generate;
replaceSecretsGeneratorAdapter = generator: name: value: pkgs.writeText "generator " (generator value);
toEnvVar = replaceSecretsGeneratorAdapter (v: (lib.generators.toINIWithGlobalSection {} { globalSection = v; }));
replaceSecretsGeneratorAdapter =
generator: name: value:
pkgs.writeText "generator " (generator value);
toEnvVar = replaceSecretsGeneratorAdapter (
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
);
template = file: newPath: replacements: replaceSecretsScript {
inherit file replacements;
resultPath = newPath;
};
template =
file: newPath: replacements:
replaceSecretsScript {
inherit file replacements;
resultPath = newPath;
};
genReplacement = secret:
genReplacement =
secret:
let
t = { transform ? null, ... }: if isNull transform then x: x else transform;
t =
{
transform ? null,
...
}:
if isNull transform then x: x else transform;
in
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
replaceSecretsScript = { file, resultPath, replacements, user ? null, permissions ? "u=r,g=r,o=" }:
replaceSecretsScript =
{
file,
resultPath,
replacements,
user ? null,
permissions ? "u=r,g=r,o=",
}:
let
templatePath = resultPath + ".template";
@ -47,15 +73,17 @@ rec {
# step. Otherwise, the $(cat ...) commands inside the sed
# replacements could fail but not fail individually but
# not fail the whole script.
checkPermissions = concatMapStringsSep "\n" (pattern: "cat ${pattern.source} > /dev/null") replacements;
checkPermissions = concatMapStringsSep "\n" (
pattern: "cat ${pattern.source} > /dev/null"
) replacements;
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (map genReplacement replacements);
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (
map genReplacement replacements
);
sedCmd = if replacements == []
then "cat"
else "${pkgs.gnused}/bin/sed ${sedPatterns}";
sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}";
in
''
''
set -euo pipefail
${checkPermissions}
@ -64,12 +92,14 @@ rec {
ln -fs ${file} ${templatePath}
rm -f ${resultPath}
touch ${resultPath}
'' + (lib.optionalString (user != null) ''
''
+ (lib.optionalString (user != null) ''
chown ${user} ${resultPath}
'') + ''
'')
+ ''
${sedCmd} ${templatePath} > ${resultPath}
chmod ${permissions} ${resultPath}
'';
'';
secretFileType = lib.types.submodule {
options = {
@ -83,35 +113,34 @@ rec {
description = "An optional function to transform the secret.";
default = null;
example = lib.literalExpression ''
v: "prefix-$${v}-suffix"
v: "prefix-$${v}-suffix"
'';
};
};
};
secretName = names:
"%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
secretName =
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
withReplacements = attrs:
withReplacements =
attrs:
let
valueOrReplacement = name: value:
if !(builtins.isAttrs value && value ? "source")
then value
else secretName name;
valueOrReplacement =
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
in
mapAttrsRecursiveCond (v: ! v ? "source") valueOrReplacement attrs;
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
getReplacements = attrs:
getReplacements =
attrs:
let
addNameField = name: value:
if !(builtins.isAttrs value && value ? "source")
then value
else value // { name = name; };
addNameField =
name: value:
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs;
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
in
collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
mapAttrsRecursiveCond =
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
@ -121,20 +150,23 @@ rec {
# Attribute set or list to recursively map over.
set:
let
recurse = path: val:
if builtins.isAttrs val && cond val
then lib.attrsets.mapAttrs (n: v: recurse (path ++ [n]) v) val
else if builtins.isList val && cond val
then lib.lists.imap0 (i: v: recurse (path ++ [(builtins.toString i)]) v) val
else f path val;
in recurse [] set;
recurse =
path: val:
if builtins.isAttrs val && cond val then
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
else if builtins.isList val && cond val then
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
else
f path val;
in
recurse [ ] set;
# Like lib.attrsets.collect but also recurses on lists.
collect =
# Given an attribute's value, determine if recursion should stop.
pred:
# The attribute set to recursively collect.
attrs:
# Given an attribute's value, determine if recursion should stop.
pred:
# The attribute set to recursively collect.
attrs:
if pred attrs then
[ attrs ]
else if builtins.isAttrs attrs then
@ -142,111 +174,158 @@ rec {
else if builtins.isList attrs then
lib.lists.concatMap (collect pred) attrs
else
[];
[ ];
indent = i: str: lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
indent =
i: str:
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
# Generator for XML
formatXML = {
enclosingRoot ? null
}: {
type = with lib.types; let
valueType = nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
]) // {
description = "XML value";
};
in valueType;
formatXML =
{
enclosingRoot ? null,
}:
{
type =
with lib.types;
let
valueType =
nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
])
// {
description = "XML value";
};
in
valueType;
generate = name: value: pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
value = builtins.toJSON (
if enclosingRoot == null then
value
else
{ ${enclosingRoot} = value; });
passAsFile = [ "value" ];
} (pkgs.writers.writePython3 "dict2xml" {
libraries = with python3.pkgs; [ python dict2xml ];
} ''
import os
import json
from dict2xml import dict2xml
generate =
name: value:
pkgs.callPackage (
{ runCommand, python3 }:
runCommand "config"
{
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
passAsFile = [ "value" ];
}
(
pkgs.writers.writePython3 "dict2xml"
{
libraries = with python3.pkgs; [
python
dict2xml
];
}
''
import os
import json
from dict2xml import dict2xml
with open(os.environ["valuePath"]) as f:
content = json.loads(f.read())
if content is None:
print("Could not parse env var valuePath as json")
os.exit(2)
with open(os.environ["out"], "w") as out:
out.write(dict2xml(content))
'')) {};
with open(os.environ["valuePath"]) as f:
content = json.loads(f.read())
if content is None:
print("Could not parse env var valuePath as json")
os.exit(2)
with open(os.environ["out"], "w") as out:
out.write(dict2xml(content))
''
)
) { };
};
};
parseXML = xml:
parseXML =
xml:
let
xmlToJsonFile = pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
inherit xml;
passAsFile = [ "xml" ];
} (pkgs.writers.writePython3 "xml2json" {
libraries = with python3.pkgs; [ python ];
} ''
import os
import json
from collections import ChainMap
from xml.etree import ElementTree
def xml_to_dict_recursive(root):
all_descendants = list(root)
if len(all_descendants) == 0:
return {root.tag: root.text}
else:
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
return {root.tag: dict(merged_dict)}
xmlToJsonFile = pkgs.callPackage (
{ runCommand, python3 }:
runCommand "config"
{
inherit xml;
passAsFile = [ "xml" ];
}
(
pkgs.writers.writePython3 "xml2json"
{
libraries = with python3.pkgs; [ python ];
}
''
import os
import json
from collections import ChainMap
from xml.etree import ElementTree
with open(os.environ["xmlPath"]) as f:
root = ElementTree.XML(f.read())
xml = xml_to_dict_recursive(root)
j = json.dumps(xml)
def xml_to_dict_recursive(root):
all_descendants = list(root)
if len(all_descendants) == 0:
return {root.tag: root.text}
else:
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
return {root.tag: dict(merged_dict)}
with open(os.environ["out"], "w") as out:
out.write(j)
'')) {};
with open(os.environ["xmlPath"]) as f:
root = ElementTree.XML(f.read())
xml = xml_to_dict_recursive(root)
j = json.dumps(xml)
with open(os.environ["out"], "w") as out:
out.write(j)
''
)
) { };
in
builtins.fromJSON (builtins.readFile xmlToJsonFile);
builtins.fromJSON (builtins.readFile xmlToJsonFile);
renameAttrName = attrset: from: to:
(lib.attrsets.filterAttrs (name: v: name == from) attrset) // {
renameAttrName =
attrset: from: to:
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
// {
${to} = attrset.${from};
};
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
# with a nicer diff display function.
check = { pkgs, tests }:
check =
{ pkgs, tests }:
let
formatValue = val:
if (builtins.isList val || builtins.isAttrs val) then builtins.toJSON val
else builtins.toString val;
formatValue =
val:
if (builtins.isList val || builtins.isAttrs val) then
builtins.toJSON val
else
builtins.toString val;
resultToString = { name, expected, result }:
builtins.readFile (pkgs.runCommand "nix-flake-tests-error" {
expected = formatValue expected;
result = formatValue result;
passAsFile = [ "expected" "result" ];
} ''
echo "${name} failed (- expected, + result)" > $out
cp ''${expectedPath} ''${expectedPath}.json
cp ''${resultPath} ''${resultPath}.json
${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
'');
resultToString =
{
name,
expected,
result,
}:
builtins.readFile (
pkgs.runCommand "nix-flake-tests-error"
{
expected = formatValue expected;
result = formatValue result;
passAsFile = [
"expected"
"result"
];
}
''
echo "${name} failed (- expected, + result)" > $out
cp ''${expectedPath} ''${expectedPath}.json
cp ''${resultPath} ''${resultPath}.json
${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
''
);
results = pkgs.lib.runTests tests;
in
@ -255,8 +334,14 @@ rec {
else
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
genConfigOutOfBandSystemd = { config, configLocation, generator, user ? null, permissions ? "u=r,g=r,o=" }:
genConfigOutOfBandSystemd =
{
config,
configLocation,
generator,
user ? null,
permissions ? "u=r,g=r,o=",
}:
{
loadCredentials = getLoadCredentials "source" config;
preStart = lib.mkBefore (replaceSecrets {
@ -267,34 +352,35 @@ rec {
});
};
updateToLoadCredentials = sourceField: rootDir: attrs:
updateToLoadCredentials =
sourceField: rootDir: attrs:
let
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
valueOrLoadCredential = path: value:
if ! (hasPlaceholderField value)
then value
else value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
valueOrLoadCredential =
path: value:
if !(hasPlaceholderField value) then
value
else
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
in
mapAttrsRecursiveCond (v: ! (hasPlaceholderField v)) valueOrLoadCredential attrs;
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
getLoadCredentials = sourceField: attrs:
getLoadCredentials =
sourceField: attrs:
let
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
addPathField = path: value:
if ! (hasPlaceholderField value)
then value
else value // { inherit path; };
addPathField =
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
secretsWithPath = mapAttrsRecursiveCond (v: ! (hasPlaceholderField v)) addPathField attrs;
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
genLoadCredentials = secret:
"${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
in
map genLoadCredentials allSecrets;
map genLoadCredentials allSecrets;
anyNotNull = any (x: x != null);
}

View file

@ -1,10 +1,16 @@
{ config, options, pkgs, lib, ... }:
{
config,
options,
pkgs,
lib,
...
}:
let
cfg = config.shb.authelia;
opt = options.shb.authelia;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}";
@ -148,25 +154,25 @@ in
extraOidcClaimsPolicies = lib.mkOption {
description = "Extra OIDC claims policies.";
type = lib.types.attrsOf lib.types.attrs;
default = {};
default = { };
};
extraOidcScopes = lib.mkOption {
description = "Extra OIDC scopes.";
type = lib.types.attrsOf lib.types.attrs;
default = {};
default = { };
};
extraOidcAuthorizationPolicies = lib.mkOption {
description = "Extra OIDC authorization policies.";
type = lib.types.attrsOf lib.types.attrs;
default = {};
default = { };
};
extraDefinitions = lib.mkOption {
description = "Extra definitions.";
type = lib.types.attrsOf lib.types.attrs;
default = {};
default = { };
};
oidcClients = lib.mkOption {
@ -178,79 +184,92 @@ in
client_secret.source = pkgs.writeText "dummy.secret" "dummy_client_secret";
public = false;
authorization_policy = "one_factor";
redirect_uris = [];
redirect_uris = [ ];
}
];
type = lib.types.listOf (lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.anything;
type = lib.types.listOf (
lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.anything;
options = {
client_id = lib.mkOption {
type = lib.types.str;
description = "Unique identifier of the OIDC client.";
options = {
client_id = lib.mkOption {
type = lib.types.str;
description = "Unique identifier of the OIDC client.";
};
client_name = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Human readable description of the OIDC client.";
default = null;
};
client_secret = lib.mkOption {
type = lib.shb.secretFileType;
description = ''
File containing the shared secret with the OIDC client.
Generate with:
```
nix run nixpkgs#authelia -- \
crypto hash generate pbkdf2 \
--variant sha512 \
--random \
--random.length 72 \
--random.charset rfc3986
```
'';
};
public = lib.mkOption {
type = lib.types.bool;
description = "If the OIDC client is public or not.";
default = false;
apply = v: if v then "true" else "false";
};
authorization_policy = lib.mkOption {
type = lib.types.enum (
[
"one_factor"
"two_factor"
]
++ lib.attrNames cfg.extraOidcAuthorizationPolicies
);
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
redirect_uris = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of uris that are allowed to be redirected to.";
};
scopes = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims";
example = [
"openid"
"profile"
"email"
"groups"
];
default = [ ];
};
claims_policy = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Claim policy.
Defaults to 'default' to provide a backwards compatible experience.
Read [this document](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter) for more information.
'';
default = "default";
};
};
client_name = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Human readable description of the OIDC client.";
default = null;
};
client_secret = lib.mkOption {
type = lib.shb.secretFileType;
description = ''
File containing the shared secret with the OIDC client.
Generate with:
```
nix run nixpkgs#authelia -- \
crypto hash generate pbkdf2 \
--variant sha512 \
--random \
--random.length 72 \
--random.charset rfc3986
```
'';
};
public = lib.mkOption {
type = lib.types.bool;
description = "If the OIDC client is public or not.";
default = false;
apply = v: if v then "true" else "false";
};
authorization_policy = lib.mkOption {
type = lib.types.enum ([ "one_factor" "two_factor" ] ++ lib.attrNames cfg.extraOidcAuthorizationPolicies);
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
redirect_uris = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of uris that are allowed to be redirected to.";
};
scopes = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims";
example = [ "openid" "profile" "email" "groups" ];
default = [];
};
claims_policy = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
Claim policy.
Defaults to 'default' to provide a backwards compatible experience.
Read [this document](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter) for more information.
'';
default = "default";
};
};
});
}
);
};
smtp = lib.mkOption {
@ -263,50 +282,52 @@ in
default = "/tmp/authelia-notifications";
type = lib.types.oneOf [
lib.types.str
(lib.types.nullOr (lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
description = "SMTP address from which the emails originate.";
example = "authelia@mydomain.com";
};
from_name = lib.mkOption {
type = lib.types.str;
description = "SMTP name from which the emails originate.";
default = "Authelia";
};
host = lib.mkOption {
type = lib.types.str;
description = "SMTP host to send the emails to.";
};
port = lib.mkOption {
type = lib.types.port;
description = "SMTP port to send the emails to.";
default = 25;
};
username = lib.mkOption {
type = lib.types.str;
description = "Username to connect to the SMTP host.";
};
password = lib.mkOption {
description = "File containing the password to connect to the SMTP host.";
type = lib.types.submodule {
options = contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
(lib.types.nullOr (
lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
description = "SMTP address from which the emails originate.";
example = "authelia@mydomain.com";
};
from_name = lib.mkOption {
type = lib.types.str;
description = "SMTP name from which the emails originate.";
default = "Authelia";
};
host = lib.mkOption {
type = lib.types.str;
description = "SMTP host to send the emails to.";
};
port = lib.mkOption {
type = lib.types.port;
description = "SMTP port to send the emails to.";
default = 25;
};
username = lib.mkOption {
type = lib.types.str;
description = "Username to connect to the SMTP host.";
};
password = lib.mkOption {
description = "File containing the password to connect to the SMTP host.";
type = lib.types.submodule {
options = contracts.secret.mkRequester {
mode = "0400";
owner = cfg.autheliaUser;
restartUnits = [ "authelia-${fqdn}" ];
};
};
};
};
};
}))
}
))
];
};
rules = lib.mkOption {
type = lib.types.listOf lib.types.anything;
description = "Rule based clients";
default = [];
default = [ ];
};
mount = lib.mkOption {
@ -324,8 +345,12 @@ in
```
'';
readOnly = true;
default = { path = "/var/lib/authelia-authelia.${cfg.domain}"; };
defaultText = { path = "/var/lib/authelia-authelia.example.com"; };
default = {
path = "/var/lib/authelia-authelia.${cfg.domain}";
};
defaultText = {
path = "/var/lib/authelia-authelia.example.com";
};
};
mountRedis = lib.mkOption {
@ -343,7 +368,9 @@ in
```
'';
readOnly = true;
default = { path = "/var/lib/redis-authelia"; };
default = {
path = "/var/lib/redis-authelia";
};
};
debug = lib.mkOption {
@ -370,7 +397,7 @@ in
# Overriding the user name so we don't allow any weird characters anywhere. For example, postgres users do not accept the '.'.
users = {
groups.${autheliaCfg.user} = {};
groups.${autheliaCfg.user} = { };
users.${autheliaCfg.user} = {
isSystemUser = true;
group = autheliaCfg.user;
@ -395,7 +422,9 @@ in
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.password.result.path);
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (
toString cfg.smtp.password.result.path
);
};
settings = {
server.address = "tcp://127.0.0.1:${toString listenPort}";
@ -428,10 +457,12 @@ in
# Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis
session = {
name = "authelia_session";
cookies = [{
domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}";
authelia_url = "https://${cfg.subdomain}.${cfg.domain}";
}];
cookies = [
{
domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}";
authelia_url = "https://${cfg.subdomain}.${cfg.domain}";
}
];
same_site = "lax";
expiration = "1h";
inactivity = "5m";
@ -468,7 +499,11 @@ in
networks = [
{
name = "internal";
networks = [ "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/18" ];
networks = [
"10.0.0.0/8"
"172.16.0.0/12"
"192.168.0.0/18"
];
}
];
rules = [
@ -479,7 +514,8 @@ in
"^/api/.*"
];
}
] ++ cfg.rules;
]
++ cfg.rules;
};
telemetry = {
metrics = {
@ -489,17 +525,25 @@ in
};
log.level = if cfg.debug then "debug" else "info";
} // {
}
// {
identity_providers.oidc = {
claims_policies = {
# This default claim should go away at some point.
# https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter
default.id_token = [ "email" "preferred_username" "name" "groups" ];
} // cfg.extraOidcClaimsPolicies;
default.id_token = [
"email"
"preferred_username"
"name"
"groups"
];
}
// cfg.extraOidcClaimsPolicies;
scopes = cfg.extraOidcScopes;
authorization_policies = cfg.extraOidcAuthorizationPolicies;
};
} // lib.optionalAttrs (cfg.extraDefinitions != {}) {
}
// lib.optionalAttrs (cfg.extraDefinitions != { }) {
definitions = cfg.extraDefinitions;
};
@ -508,18 +552,22 @@ in
systemd.services."authelia-${fqdn}".preStart =
let
mkCfg = clients:
mkCfg =
clients:
lib.shb.replaceSecrets {
userConfig = {
identity_providers.oidc.clients = clients;
};
resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML {});
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { });
};
in
lib.mkBefore (mkCfg cfg.oidcClients + ''
${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'
'');
lib.mkBefore (
mkCfg cfg.oidcClients
+ ''
${pkgs.bash}/bin/bash -c '(while ! ${pkgs.netcat-openbsd}/bin/nc -z -v -w1 ${cfg.ldapHostname} ${toString cfg.ldapPort}; do echo "Waiting for port ${cfg.ldapHostname}:${toString cfg.ldapPort} to open..."; sleep 2; done); sleep 2'
''
);
services.nginx.virtualHosts.${fqdn} = {
forceSSL = !(isNull cfg.ssl);
@ -554,7 +602,7 @@ in
error_page 403 = /error/403;
error_page 404 = /error/404;
}
'';
'';
locations."/api/verify".extraConfig = ''
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -567,7 +615,7 @@ in
proxy_set_header Host $http_x_forwarded_host;
proxy_pass http://127.0.0.1:9091;
'';
'';
};
# I would like this to live outside of the Authelia module.
@ -579,7 +627,8 @@ in
after = [ "authelia-${fqdn}.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [
"--set" "verbose_pattern=/api"
"--set"
"verbose_pattern=/api"
];
};
@ -600,7 +649,7 @@ in
job_name = "authelia";
static_configs = [
{
targets = ["127.0.0.1:9959"];
targets = [ "127.0.0.1:9959" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
@ -610,17 +659,20 @@ in
}
];
systemd.targets."authelia-${fqdn}" = let
services = [
"authelia-${fqdn}.service"
] ++ lib.optionals cfg.debug [
config.shb.mitmdump.instances."authelia-${fqdn}".serviceName
];
in {
after = services;
requires = services;
systemd.targets."authelia-${fqdn}" =
let
services = [
"authelia-${fqdn}.service"
]
++ lib.optionals cfg.debug [
config.shb.mitmdump.instances."authelia-${fqdn}".serviceName
];
in
{
after = services;
requires = services;
wantedBy = [ "multi-user.target" ];
};
wantedBy = [ "multi-user.target" ];
};
};
}

View file

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

View file

@ -7,66 +7,68 @@ in
options.shb.davfs = {
mounts = lib.mkOption {
description = "List of mounts.";
default = [];
type = lib.types.listOf (lib.types.submodule {
options = {
remoteUrl = lib.mkOption {
type = lib.types.str;
description = "Webdav endpoint to connect to.";
example = "https://my.domain.com/dav";
};
default = [ ];
type = lib.types.listOf (
lib.types.submodule {
options = {
remoteUrl = lib.mkOption {
type = lib.types.str;
description = "Webdav endpoint to connect to.";
example = "https://my.domain.com/dav";
};
mountPoint = lib.mkOption {
type = lib.types.str;
description = "Mount point to mount the webdav endpoint on.";
example = "/mnt";
};
mountPoint = lib.mkOption {
type = lib.types.str;
description = "Mount point to mount the webdav endpoint on.";
example = "/mnt";
};
username = lib.mkOption {
type = lib.types.str;
description = "Username to connect to the webdav endpoint.";
};
username = lib.mkOption {
type = lib.types.str;
description = "Username to connect to the webdav endpoint.";
};
passwordFile = lib.mkOption {
type = lib.types.str;
description = "Password to connect to the webdav endpoint.";
};
passwordFile = lib.mkOption {
type = lib.types.str;
description = "Password to connect to the webdav endpoint.";
};
uid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
description = "User owner of the mount point.";
example = 1000;
default = null;
};
uid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
description = "User owner of the mount point.";
example = 1000;
default = null;
};
gid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
description = "Group owner of the mount point.";
example = 1000;
default = null;
};
gid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
description = "Group owner of the mount point.";
example = 1000;
default = null;
};
fileMode = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "File creation mode";
example = "0664";
default = null;
};
fileMode = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "File creation mode";
example = "0664";
default = null;
};
directoryMode = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Directory creation mode";
example = "2775";
default = null;
};
directoryMode = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Directory creation mode";
example = "2775";
default = null;
};
automount = lib.mkOption {
type = lib.types.bool;
description = "Create a systemd automount unit";
default = true;
automount = lib.mkOption {
type = lib.types.bool;
description = "Create a systemd automount unit";
default = true;
};
};
};
});
}
);
};
};
@ -93,6 +95,6 @@ in
mountConfig.TimeoutSec = 15;
};
in
map mkMountCfg cfg.mounts;
map mkMountCfg cfg.mounts;
};
}

View file

@ -1,84 +1,102 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.hardcodedsecret;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
inherit (lib) mapAttrs' mkOption nameValuePair;
inherit (lib.types) attrsOf nullOr str submodule;
inherit (lib.types)
attrsOf
nullOr
str
submodule
;
inherit (pkgs) writeText;
in
{
options.shb.hardcodedsecret = mkOption {
default = {};
default = { };
description = ''
Hardcoded secrets. These should only be used in tests.
'';
example = lib.literalExpression ''
{
mySecret = {
request = {
user = "me";
mode = "0400";
restartUnits = [ "myservice.service" ];
{
mySecret = {
request = {
user = "me";
mode = "0400";
restartUnits = [ "myservice.service" ];
};
settings.content = "My Secret";
};
settings.content = "My Secret";
};
}
}
'';
type = attrsOf (submodule ({ name, ... }: {
options = contracts.secret.mkProvider {
settings = mkOption {
description = ''
Settings specific to the hardcoded secret module.
type = attrsOf (
submodule (
{ name, ... }:
{
options = contracts.secret.mkProvider {
settings = mkOption {
description = ''
Settings specific to the hardcoded secret module.
Give either `content` or `source`.
'';
Give either `content` or `source`.
'';
type = submodule {
options = {
content = mkOption {
type = nullOr str;
description = ''
Content of the secret as a string.
type = submodule {
options = {
content = mkOption {
type = nullOr str;
description = ''
Content of the secret as a string.
This will be stored in the nix store and should only be used for testing or maybe in dev.
'';
default = null;
};
This will be stored in the nix store and should only be used for testing or maybe in dev.
'';
default = null;
};
source = mkOption {
type = nullOr str;
description = ''
Source of the content of the secret as a path in the nix store.
'';
default = null;
source = mkOption {
type = nullOr str;
description = ''
Source of the content of the secret as a path in the nix store.
'';
default = null;
};
};
};
};
};
};
resultCfg = {
path = "/run/hardcodedsecrets/hardcodedsecret_${name}";
};
};
}));
resultCfg = {
path = "/run/hardcodedsecrets/hardcodedsecret_${name}";
};
};
}
)
);
};
config = {
system.activationScripts = mapAttrs' (n: cfg':
system.activationScripts = mapAttrs' (
n: cfg':
let
source = if cfg'.settings.source != null
then cfg'.settings.source
else writeText "hardcodedsecret_${n}_content" cfg'.settings.content;
source =
if cfg'.settings.source != null then
cfg'.settings.source
else
writeText "hardcodedsecret_${n}_content" cfg'.settings.content;
in
nameValuePair "hardcodedsecret_${n}" ''
mkdir -p "$(dirname "${cfg'.result.path}")"
touch "${cfg'.result.path}"
chmod ${cfg'.request.mode} "${cfg'.result.path}"
chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}"
cp ${source} "${cfg'.result.path}"
''
nameValuePair "hardcodedsecret_${n}" ''
mkdir -p "$(dirname "${cfg'.result.path}")"
touch "${cfg'.result.path}"
chmod ${cfg'.request.mode} "${cfg'.result.path}"
chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}"
cp ${source} "${cfg'.result.path}"
''
) cfg;
};
}

View file

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

View file

@ -1,143 +1,163 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mapAttrs' mkOption nameValuePair types;
inherit (types) attrsOf listOf port submodule str;
inherit (lib)
mapAttrs'
mkOption
nameValuePair
types
;
inherit (types)
attrsOf
listOf
port
submodule
str
;
cfg = config.shb.mitmdump;
mitmdumpScript = pkgs.writers.writePython3Bin "mitmdump"
{
libraries = let
p = pkgs.python3Packages;
in [
p.systemd
p.mitmproxy
];
flakeIgnore = [ "E501" ];
}
''
from systemd.daemon import notify
import argparse
import logging
import os
import subprocess
import socket
import sys
import time
logging.basicConfig(level=logging.INFO, format='%(message)s')
def wait_for_port(host, port, timeout=10):
deadline = time.time() + timeout
while time.time() < deadline:
try:
with socket.create_connection((host, port), timeout=0.5):
return True
except Exception:
time.sleep(0.1)
return False
def flatten(xss):
return [x for xs in xss for x in xs]
parser = argparse.ArgumentParser()
parser.add_argument("--listen_host", default="127.0.0.1", help="Host mitmdump will listen on")
parser.add_argument("--listen_port", required=True, help="Port mitmdump will listen on")
parser.add_argument("--upstream_host", default="http://127.0.0.1", help="Host mitmdump will connect to for upstream. Example: http://127.0.0.1 or https://otherhost")
parser.add_argument("--upstream_port", required=True, help="Port mitmdump will connect to for upstream")
args, rest = parser.parse_known_args()
MITMDUMP_BIN = os.environ.get("MITMDUMP_BIN")
if MITMDUMP_BIN is None:
raise Exception("MITMDUMP_BIN env var must be set to the path of the mitmdump binary")
logging.info(f"Waiting for upstream address '{args.upstream_host}:{args.upstream_port}' to be up.")
wait_for_port(args.upstream_host, args.upstream_port, timeout=10)
logging.info(f"Upstream address '{args.upstream_host}:{args.upstream_port}' is up.")
proc = subprocess.Popen(
mitmdumpScript =
pkgs.writers.writePython3Bin "mitmdump"
{
libraries =
let
p = pkgs.python3Packages;
in
[
MITMDUMP_BIN,
"--listen-host", args.listen_host,
"-p", args.listen_port,
"--mode", f"reverse:{args.upstream_host}:{args.upstream_port}",
] + rest,
stdout=sys.stdout,
stderr=sys.stderr,
)
logging.info(f"Waiting for mitmdump instance to start on port {args.listen_port}.")
if wait_for_port("127.0.0.1", args.listen_port, timeout=10):
logging.info(f"Mitmdump is started on port {args.listen_port}.")
notify("READY=1")
else:
proc.terminate()
exit(1)
proc.wait()
'';
logger = toString (pkgs.writers.writeText "loggerAddon.py"
''
import logging
from collections.abc import Sequence
from mitmproxy import ctx, http
import re
p.systemd
p.mitmproxy
];
flakeIgnore = [ "E501" ];
}
''
from systemd.daemon import notify
import argparse
import logging
import os
import subprocess
import socket
import sys
import time
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO, format='%(message)s')
class RegexLogger:
def __init__(self):
self.verbose_patterns = None
def load(self, loader):
loader.add_option(
name="verbose_pattern",
typespec=Sequence[str],
default=[],
help="Regex patterns for verbose logging",
)
def response(self, flow: http.HTTPFlow):
if self.verbose_patterns is None:
self.verbose_patterns = [re.compile(p) for p in ctx.options.verbose_pattern]
matched = any(p.search(flow.request.path) for p in self.verbose_patterns)
if matched:
logger.info(format_flow(flow))
def wait_for_port(host, port, timeout=10):
deadline = time.time() + timeout
while time.time() < deadline:
try:
with socket.create_connection((host, port), timeout=0.5):
return True
except Exception:
time.sleep(0.1)
return False
def format_flow(flow: http.HTTPFlow) -> str:
return (
"\n"
"RequestHeaders:\n"
f" {format_headers(flow.request.headers.items())}\n"
f"RequestBody: {flow.request.get_text()}\n"
f"Status: {flow.response.data.status_code}\n"
"ResponseHeaders:\n"
f" {format_headers(flow.response.headers.items())}\n"
f"ResponseBody: {flow.response.get_text()}\n"
def flatten(xss):
return [x for xs in xss for x in xs]
parser = argparse.ArgumentParser()
parser.add_argument("--listen_host", default="127.0.0.1", help="Host mitmdump will listen on")
parser.add_argument("--listen_port", required=True, help="Port mitmdump will listen on")
parser.add_argument("--upstream_host", default="http://127.0.0.1", help="Host mitmdump will connect to for upstream. Example: http://127.0.0.1 or https://otherhost")
parser.add_argument("--upstream_port", required=True, help="Port mitmdump will connect to for upstream")
args, rest = parser.parse_known_args()
MITMDUMP_BIN = os.environ.get("MITMDUMP_BIN")
if MITMDUMP_BIN is None:
raise Exception("MITMDUMP_BIN env var must be set to the path of the mitmdump binary")
logging.info(f"Waiting for upstream address '{args.upstream_host}:{args.upstream_port}' to be up.")
wait_for_port(args.upstream_host, args.upstream_port, timeout=10)
logging.info(f"Upstream address '{args.upstream_host}:{args.upstream_port}' is up.")
proc = subprocess.Popen(
[
MITMDUMP_BIN,
"--listen-host", args.listen_host,
"-p", args.listen_port,
"--mode", f"reverse:{args.upstream_host}:{args.upstream_port}",
] + rest,
stdout=sys.stdout,
stderr=sys.stderr,
)
logging.info(f"Waiting for mitmdump instance to start on port {args.listen_port}.")
if wait_for_port("127.0.0.1", args.listen_port, timeout=10):
logging.info(f"Mitmdump is started on port {args.listen_port}.")
notify("READY=1")
else:
proc.terminate()
exit(1)
def format_headers(headers) -> str:
return "\n ".join(k + ": " + v for k, v in headers)
proc.wait()
'';
logger = toString (
pkgs.writers.writeText "loggerAddon.py" ''
import logging
from collections.abc import Sequence
from mitmproxy import ctx, http
import re
addons = [RegexLogger()]
'');
logger = logging.getLogger(__name__)
class RegexLogger:
def __init__(self):
self.verbose_patterns = None
def load(self, loader):
loader.add_option(
name="verbose_pattern",
typespec=Sequence[str],
default=[],
help="Regex patterns for verbose logging",
)
def response(self, flow: http.HTTPFlow):
if self.verbose_patterns is None:
self.verbose_patterns = [re.compile(p) for p in ctx.options.verbose_pattern]
matched = any(p.search(flow.request.path) for p in self.verbose_patterns)
if matched:
logger.info(format_flow(flow))
def format_flow(flow: http.HTTPFlow) -> str:
return (
"\n"
"RequestHeaders:\n"
f" {format_headers(flow.request.headers.items())}\n"
f"RequestBody: {flow.request.get_text()}\n"
f"Status: {flow.response.data.status_code}\n"
"ResponseHeaders:\n"
f" {format_headers(flow.response.headers.items())}\n"
f"ResponseBody: {flow.response.get_text()}\n"
)
def format_headers(headers) -> str:
return "\n ".join(k + ": " + v for k, v in headers)
addons = [RegexLogger()]
''
);
in
{
options.shb.mitmdump = {
addons = mkOption {
type = attrsOf str;
default = [];
default = [ ];
description = ''
Addons available to the be added to the mitmdump instance.
@ -146,120 +166,129 @@ in
};
instances = mkOption {
default = {};
default = { };
description = "Mitmdump instance.";
type = attrsOf (submodule ({ name, ... }: {
options = {
package = lib.mkPackageOption pkgs "mitmproxy" {};
type = attrsOf (
submodule (
{ name, ... }:
{
options = {
package = lib.mkPackageOption pkgs "mitmproxy" { };
serviceName = mkOption {
type = str;
description = ''
Name of the mitmdump system service.
'';
default = "mitmdump-${name}.service";
readOnly = true;
};
serviceName = mkOption {
type = str;
description = ''
Name of the mitmdump system service.
'';
default = "mitmdump-${name}.service";
readOnly = true;
};
listenHost = mkOption {
type = str;
default = "127.0.0.1";
description = ''
Host the mitmdump instance will connect on.
'';
};
listenHost = mkOption {
type = str;
default = "127.0.0.1";
description = ''
Host the mitmdump instance will connect on.
'';
};
listenPort = mkOption {
type = port;
description = ''
Port the mitmdump instance will listen on.
listenPort = mkOption {
type = port;
description = ''
Port the mitmdump instance will listen on.
The upstream port from the client's perspective.
'';
};
The upstream port from the client's perspective.
'';
};
upstreamHost = mkOption {
type = str;
default = "http://127.0.0.1";
description = ''
Host the mitmdump instance will connect to.
upstreamHost = mkOption {
type = str;
default = "http://127.0.0.1";
description = ''
Host the mitmdump instance will connect to.
If only an IP or domain is provided,
mitmdump will default to connect using HTTPS.
If this is not wanted, prefix the IP or domain with the 'http://' protocol.
'';
};
If only an IP or domain is provided,
mitmdump will default to connect using HTTPS.
If this is not wanted, prefix the IP or domain with the 'http://' protocol.
'';
};
upstreamPort = mkOption {
type = port;
description = ''
Port the mitmdump instance will connect to.
upstreamPort = mkOption {
type = port;
description = ''
Port the mitmdump instance will connect to.
The port the server is listening on.
'';
};
The port the server is listening on.
'';
};
after = mkOption {
type = listOf str;
default = [];
description = ''
Systemd services that must be started before this mitmdump proxy instance.
after = mkOption {
type = listOf str;
default = [ ];
description = ''
Systemd services that must be started before this mitmdump proxy instance.
You are guaranteed the mitmdump is listening on the `listenPort`
when its systemd service has started.
'';
};
You are guaranteed the mitmdump is listening on the `listenPort`
when its systemd service has started.
'';
};
enabledAddons = mkOption {
type = listOf str;
default = [];
description = ''
Addons to enable on this mitmdump instance.
'';
example = lib.literalExpression ''[ config.shb.mitmdump.addons.logger ]'';
};
enabledAddons = mkOption {
type = listOf str;
default = [ ];
description = ''
Addons to enable on this mitmdump instance.
'';
example = lib.literalExpression ''[ config.shb.mitmdump.addons.logger ]'';
};
extraArgs = mkOption {
type = listOf str;
default = [];
description = ''
Extra arguments to pass to the mitmdump instance.
extraArgs = mkOption {
type = listOf str;
default = [ ];
description = ''
Extra arguments to pass to the mitmdump instance.
See upstream [manual](https://docs.mitmproxy.org/stable/concepts/options/#flow_detail) for all possible options.
'';
example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]'';
};
};
}));
See upstream [manual](https://docs.mitmproxy.org/stable/concepts/options/#flow_detail) for all possible options.
'';
example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]'';
};
};
}
)
);
};
};
config = {
systemd.services = mapAttrs' (name: cfg': nameValuePair "mitmdump-${name}" {
environment = {
"HOME" = "/var/lib/private/mitmdump-${name}";
"MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump";
};
serviceConfig = {
Type = "notify";
Restart = "on-failure";
StandardOutput = "journal";
StandardError = "journal";
systemd.services = mapAttrs' (
name: cfg':
nameValuePair "mitmdump-${name}" {
environment = {
"HOME" = "/var/lib/private/mitmdump-${name}";
"MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump";
};
serviceConfig = {
Type = "notify";
Restart = "on-failure";
StandardOutput = "journal";
StandardError = "journal";
DynamicUser = true;
WorkingDirectory = "/var/lib/mitmdump-${name}";
StateDirectory = "mitmdump-${name}";
DynamicUser = true;
WorkingDirectory = "/var/lib/mitmdump-${name}";
StateDirectory = "mitmdump-${name}";
ExecStart = let
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
in
"${lib.getExe mitmdumpScript} --listen_host ${cfg'.listenHost} --listen_port ${toString cfg'.listenPort} --upstream_host ${cfg'.upstreamHost} --upstream_port ${toString cfg'.upstreamPort} ${addons} ${extraArgs}";
};
requires = cfg'.after;
after = cfg'.after;
wantedBy = [ "multi-user.target" ];
}) cfg.instances;
ExecStart =
let
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
in
"${lib.getExe mitmdumpScript} --listen_host ${cfg'.listenHost} --listen_port ${toString cfg'.listenPort} --upstream_host ${cfg'.upstreamHost} --upstream_port ${toString cfg'.upstreamPort} ${addons} ${extraArgs}";
};
requires = cfg'.after;
after = cfg'.after;
wantedBy = [ "multi-user.target" ];
}
) cfg.instances;
shb.mitmdump.addons = {
inherit logger;

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.monitoring;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -53,7 +58,10 @@ in
};
lokiMajorVersion = lib.mkOption {
type = lib.types.enum [ 2 3 ];
type = lib.types.enum [
2
3
];
description = ''
Switching from version 2 to 3 requires manual intervention
https://grafana.com/docs/loki/latest/setup/upgrade/#main--unreleased. So this let's the user
@ -84,7 +92,7 @@ in
contactPoints = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of email addresses to send alerts to";
default = [];
default = [ ];
};
adminPassword = lib.mkOption {
@ -114,37 +122,39 @@ in
smtp = lib.mkOption {
description = "SMTP options.";
default = null;
type = lib.types.nullOr (lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
description = "SMTP address from which the emails originate.";
example = "vaultwarden@mydomain.com";
type = lib.types.nullOr (
lib.types.submodule {
options = {
from_address = lib.mkOption {
type = lib.types.str;
description = "SMTP address from which the emails originate.";
example = "vaultwarden@mydomain.com";
};
from_name = lib.mkOption {
type = lib.types.str;
description = "SMTP name from which the emails originate.";
default = "Grafana";
};
host = lib.mkOption {
type = lib.types.str;
description = "SMTP host to send the emails to.";
};
port = lib.mkOption {
type = lib.types.port;
description = "SMTP port to send the emails to.";
default = 25;
};
username = lib.mkOption {
type = lib.types.str;
description = "Username to connect to the SMTP host.";
};
passwordFile = lib.mkOption {
type = lib.types.str;
description = "File containing the password to connect to the SMTP host.";
};
};
from_name = lib.mkOption {
type = lib.types.str;
description = "SMTP name from which the emails originate.";
default = "Grafana";
};
host = lib.mkOption {
type = lib.types.str;
description = "SMTP host to send the emails to.";
};
port = lib.mkOption {
type = lib.types.port;
description = "SMTP port to send the emails to.";
default = 25;
};
username = lib.mkOption {
type = lib.types.str;
description = "Username to connect to the SMTP host.";
};
passwordFile = lib.mkOption {
type = lib.types.str;
description = "File containing the password to connect to the SMTP host.";
};
};
});
}
);
};
};
@ -204,12 +214,14 @@ in
services.grafana.provision = {
dashboards.settings = lib.mkIf cfg.provisionDashboards {
apiVersion = 1;
providers = [{
folder = "Self Host Blocks";
options.path = ./monitoring/dashboards;
allowUiUpdates = true;
disableDeletion = true;
}];
providers = [
{
folder = "Self Host Blocks";
options.path = ./monitoring/dashboards;
allowUiUpdates = true;
disableDeletion = true;
}
];
};
datasources.settings = {
apiVersion = 1;
@ -245,26 +257,35 @@ in
};
alerting.contactPoints.settings = {
apiVersion = 1;
contactPoints = [{
inherit (cfg) orgId;
name = "grafana-default-email";
receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [{
uid = "sysadmin";
type = "email";
settings.addresses = lib.concatStringsSep ";" cfg.contactPoints;
}];
}];
contactPoints = [
{
inherit (cfg) orgId;
name = "grafana-default-email";
receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [
{
uid = "sysadmin";
type = "email";
settings.addresses = lib.concatStringsSep ";" cfg.contactPoints;
}
];
}
];
};
alerting.policies.settings = {
apiVersion = 1;
policies = [{
inherit (cfg) orgId;
receiver = "grafana-default-email";
group_by = [ "grafana_folder" "alertname" ];
group_wait = "30s";
group_interval = "5m";
repeat_interval = "4h";
}];
policies = [
{
inherit (cfg) orgId;
receiver = "grafana-default-email";
group_by = [
"grafana_folder"
"alertname"
];
group_wait = "30s";
group_interval = "5m";
repeat_interval = "4h";
}
];
# resetPolicies seems to happen after setting the above policies, effectively rolling back
# any updates.
};
@ -273,18 +294,20 @@ in
rules = builtins.fromJSON (builtins.readFile ./monitoring/rules.json);
ruleIds = map (r: r.uid) rules;
in
{
apiVersion = 1;
groups = [{
{
apiVersion = 1;
groups = [
{
inherit (cfg) orgId;
name = "SysAdmin";
folder = "Self Host Blocks";
interval = "10m";
inherit rules;
}];
# deleteRules seems to happen after creating the above rules, effectively rolling back
# any updates.
};
}
];
# deleteRules seems to happen after creating the above rules, effectively rolling back
# any updates.
};
};
services.prometheus = {
@ -295,36 +318,45 @@ in
services.loki = {
enable = true;
dataDir = "/var/lib/loki";
package = if cfg.lokiMajorVersion == 3 then pkgs.grafana-loki else
# Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4
(pkgs.grafana-loki.overrideAttrs (finalAttrs: previousAttrs: rec {
version = "2.9.6";
package =
if cfg.lokiMajorVersion == 3 then
pkgs.grafana-loki
else
# Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4
(pkgs.grafana-loki.overrideAttrs (
finalAttrs: previousAttrs: rec {
version = "2.9.6";
src = pkgs.fetchFromGitHub {
owner = "grafana";
repo = "loki";
rev = "v${version}";
hash = "sha256-79hK7axHf6soku5DvdXkE/0K4WKc4pnS9VMbVc1FS2I=";
};
src = pkgs.fetchFromGitHub {
owner = "grafana";
repo = "loki";
rev = "v${version}";
hash = "sha256-79hK7axHf6soku5DvdXkE/0K4WKc4pnS9VMbVc1FS2I=";
};
subPackages = [
"cmd/loki"
"cmd/loki-canary"
"clients/cmd/promtail"
"cmd/logcli"
# Removes "cmd/lokitool"
];
subPackages = [
"cmd/loki"
"cmd/loki-canary"
"clients/cmd/promtail"
"cmd/logcli"
# Removes "cmd/lokitool"
];
ldflags = let t = "github.com/grafana/loki/pkg/util/build"; in [
"-s"
"-w"
"-X ${t}.Version=${version}"
"-X ${t}.BuildUser=nix@nixpkgs"
"-X ${t}.BuildDate=unknown"
"-X ${t}.Branch=unknown"
"-X ${t}.Revision=unknown"
];
}));
ldflags =
let
t = "github.com/grafana/loki/pkg/util/build";
in
[
"-s"
"-w"
"-X ${t}.Version=${version}"
"-X ${t}.BuildUser=nix@nixpkgs"
"-X ${t}.BuildDate=unknown"
"-X ${t}.Branch=unknown"
"-X ${t}.Revision=unknown"
];
}
));
configuration = {
auth_enabled = false;
@ -439,7 +471,7 @@ in
proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
proxyWebsockets = true;
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header Host $host;
'';
};
};
@ -448,61 +480,75 @@ in
services.prometheus.scrapeConfigs = [
{
job_name = "node";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.node.port}"];
labels = commonLabels;
}];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ];
labels = commonLabels;
}
];
}
{
job_name = "netdata";
metrics_path = "/api/v1/allmetrics";
params.format = [ "prometheus" ];
honor_labels = true;
static_configs = [{
targets = [ "127.0.0.1:19999" ];
labels = commonLabels;
}];
static_configs = [
{
targets = [ "127.0.0.1:19999" ];
labels = commonLabels;
}
];
}
{
job_name = "smartctl";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}"];
labels = commonLabels;
}];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}" ];
labels = commonLabels;
}
];
}
{
job_name = "prometheus_internal";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.port}"];
labels = commonLabels;
}];
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.port}" ];
labels = commonLabels;
}
];
}
] ++ (lib.lists.optional config.services.nginx.enable {
job_name = "nginx";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}"];
]
++ (lib.lists.optional config.services.nginx.enable {
job_name = "nginx";
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}" ];
labels = commonLabels;
}];
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) {
# job_name = "redis";
# static_configs = [
# {
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.redis.port}"];
# }
# ];
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.openvpn.servers) > 0) {
# job_name = "openvpn";
# static_configs = [
# {
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"];
# }
# ];
}) ++ (lib.optional config.services.dnsmasq.enable {
job_name = "dnsmasq";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}"];
}
];
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) {
# job_name = "redis";
# static_configs = [
# {
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.redis.port}"];
# }
# ];
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.openvpn.servers) > 0) {
# job_name = "openvpn";
# static_configs = [
# {
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"];
# }
# ];
})
++ (lib.optional config.services.dnsmasq.enable {
job_name = "dnsmasq";
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}" ];
labels = commonLabels;
}];
}
];
});
services.prometheus.exporters.nginx = lib.mkIf config.services.nginx.enable {
enable = true;
@ -513,7 +559,7 @@ in
services.prometheus.exporters.node = {
enable = true;
# https://github.com/prometheus/node_exporter#collectors
enabledCollectors = ["ethtool"];
enabledCollectors = [ "ethtool" ];
port = 9112;
listenAddress = "127.0.0.1";
};

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.nginx;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = c: "${c.subdomain}.${c.domain}";
@ -42,12 +47,13 @@ let
autheliaRules = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
default = [];
default = [ ];
description = "Authelia rule configuration";
example = lib.literalExpression ''[{
policy = "two_factor";
subject = ["group:service_user"];
}]'';
example = lib.literalExpression ''
[{
policy = "two_factor";
subject = ["group:service_user"];
}]'';
};
extraConfig = lib.mkOption {
@ -77,41 +83,44 @@ in
vhosts = lib.mkOption {
description = "Endpoints to be protected by authelia.";
type = lib.types.listOf vhostConfig;
default = [];
default = [ ];
};
};
config = {
networking.firewall.allowedTCPPorts = [ 80 443 ];
networking.firewall.allowedTCPPorts = [
80
443
];
services.nginx.enable = true;
services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn";
services.nginx.appendHttpConfig = lib.mkIf cfg.accessLog ''
log_format apm
'{'
'"remote_addr":"$remote_addr",'
'"remote_user":"$remote_user",'
'"time_local":"$time_local",'
'"request":"$request",'
'"request_length":"$request_length",'
'"server_name":"$server_name",'
'"status":"$status",'
'"bytes_sent":"$bytes_sent",'
'"body_bytes_sent":"$body_bytes_sent",'
'"referrer":"$http_referrer",'
'"user_agent":"$http_user_agent",'
'"gzip_ration":"$gzip_ratio",'
'"post":"$request_body",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"request_time":"$request_time",'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_connect_time":"$upstream_connect_time",'
'"upstream_header_time":"$upstream_header_time"'
'}';
log_format apm
'{'
'"remote_addr":"$remote_addr",'
'"remote_user":"$remote_user",'
'"time_local":"$time_local",'
'"request":"$request",'
'"request_length":"$request_length",'
'"server_name":"$server_name",'
'"status":"$status",'
'"bytes_sent":"$bytes_sent",'
'"body_bytes_sent":"$body_bytes_sent",'
'"referrer":"$http_referrer",'
'"user_agent":"$http_user_agent",'
'"gzip_ration":"$gzip_ratio",'
'"post":"$request_body",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"request_time":"$request_time",'
'"upstream_response_time":"$upstream_response_time",'
'"upstream_connect_time":"$upstream_connect_time",'
'"upstream_header_time":"$upstream_header_time"'
'}';
access_log syslog:server=unix:/dev/log apm;
'';
access_log syslog:server=unix:/dev/log apm;
'';
services.nginx.virtualHosts =
let
@ -189,13 +198,13 @@ in
};
};
in
lib.mkMerge (map vhostCfg cfg.vhosts);
lib.mkMerge (map vhostCfg cfg.vhosts);
shb.authelia.rules =
let
authConfig = c: map (r: r // { domain = fqdn c; }) c.autheliaRules;
in
lib.flatten (map authConfig cfg.vhosts);
lib.flatten (map authConfig cfg.vhosts);
security.acme.defaults.reloadServices = [
"nginx.service"

View file

@ -1,9 +1,15 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.postgresql;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
upgrade-script = old: new:
upgrade-script =
old: new:
let
oldStr = builtins.toString old;
newStr = builtins.toString new;
@ -11,37 +17,37 @@ let
oldPkg = pkgs.${"postgresql_${oldStr}"};
newPkg = pkgs.${"postgresql_${newStr}"};
in
pkgs.writeScriptBin "upgrade-pg-cluster-${oldStr}-${newStr}" ''
set -eux
# XXX it's perhaps advisable to stop all services that depend on postgresql
systemctl stop postgresql
pkgs.writeScriptBin "upgrade-pg-cluster-${oldStr}-${newStr}" ''
set -eux
# XXX it's perhaps advisable to stop all services that depend on postgresql
systemctl stop postgresql
export NEWDATA="/var/lib/postgresql/${newPkg.psqlSchema}"
export NEWBIN="${newPkg}/bin"
export NEWDATA="/var/lib/postgresql/${newPkg.psqlSchema}"
export NEWBIN="${newPkg}/bin"
export OLDDATA="/var/lib/postgresql/${oldPkg.psqlSchema}"
export OLDBIN="${oldPkg}/bin"
export OLDDATA="/var/lib/postgresql/${oldPkg.psqlSchema}"
export OLDBIN="${oldPkg}/bin"
install -d -m 0700 -o postgres -g postgres "$NEWDATA"
cd "$NEWDATA"
sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
install -d -m 0700 -o postgres -g postgres "$NEWDATA"
cd "$NEWDATA"
sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
sudo -u postgres $NEWBIN/pg_upgrade \
--old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
--old-bindir $OLDBIN --new-bindir $NEWBIN \
"$@"
'';
sudo -u postgres $NEWBIN/pg_upgrade \
--old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
--old-bindir $OLDBIN --new-bindir $NEWBIN \
"$@"
'';
in
{
options.shb.postgresql = {
debug = lib.mkOption {
type = lib.types.bool;
description = ''
Enable debugging options.
Enable debugging options.
Currently enables shared_preload_libraries = "auto_explain, pg_stat_statements"
Currently enables shared_preload_libraries = "auto_explain, pg_stat_statements"
See https://www.postgresql.org/docs/current/pgstatstatements.html'';
See https://www.postgresql.org/docs/current/pgstatstatements.html'';
default = false;
};
enableTCPIP = lib.mkOption {
@ -55,7 +61,7 @@ in
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.databasebackup.mkRequester {
user = "postgres";
@ -75,27 +81,29 @@ in
ensures = lib.mkOption {
description = "List of username, database and/or passwords that should be created.";
type = lib.types.listOf (lib.types.submodule {
options = {
username = lib.mkOption {
type = lib.types.str;
description = "Postgres user name.";
};
type = lib.types.listOf (
lib.types.submodule {
options = {
username = lib.mkOption {
type = lib.types.str;
description = "Postgres user name.";
};
database = lib.mkOption {
type = lib.types.str;
description = "Postgres database.";
};
database = lib.mkOption {
type = lib.types.str;
description = "Postgres database.";
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Optional password file for the postgres user. If not given, only peer auth is accepted for this user, otherwise password auth is allowed.";
default = null;
example = "/run/secrets/postgresql/password";
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Optional password file for the postgres user. If not given, only peer auth is accepted for this user, otherwise password auth is allowed.";
default = null;
example = "/run/secrets/postgresql/password";
};
};
};
});
default = [];
}
);
default = [ ];
};
};
@ -123,51 +131,61 @@ in
dbConfig = ensureCfgs: {
services.postgresql.enable = lib.mkDefault ((builtins.length ensureCfgs) > 0);
services.postgresql.ensureDatabases = map ({ database, ... }: database) ensureCfgs;
services.postgresql.ensureUsers = map ({ username, database, ... }: {
name = username;
ensureDBOwnership = true;
ensureClauses.login = true;
}) ensureCfgs;
services.postgresql.ensureUsers = map (
{ username, database, ... }:
{
name = username;
ensureDBOwnership = true;
ensureClauses.login = true;
}
) ensureCfgs;
};
pwdConfig = ensureCfgs: {
systemd.services.postgresql-setup.script = lib.mkAfter
(let
systemd.services.postgresql-setup.script = lib.mkAfter (
let
prefix = ''
psql -tA <<'EOF'
DO $$
DECLARE password TEXT;
BEGIN
psql -tA <<'EOF'
DO $$
DECLARE password TEXT;
BEGIN
'';
suffix = ''
END $$;
EOF
'';
exec = { username, passwordFile, ... }: ''
password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', '''));
EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password);
END $$;
EOF
'';
exec =
{ username, passwordFile, ... }:
''
password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', '''));
EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password);
'';
cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs;
in
if (builtins.length cfgsWithPasswords) == 0 then "" else
prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix);
if (builtins.length cfgsWithPasswords) == 0 then
""
else
prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix
);
};
debugConfig = enableDebug: lib.mkIf enableDebug {
services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements";
};
debugConfig =
enableDebug:
lib.mkIf enableDebug {
services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements";
};
in
lib.mkMerge ([
commonConfig
(dbConfig cfg.ensures)
(pwdConfig cfg.ensures)
(lib.mkIf cfg.enableTCPIP tcpConfig)
(debugConfig cfg.debug)
{
environment.systemPackages = lib.mkIf config.services.postgresql.enable [
(upgrade-script 15 16)
(upgrade-script 16 17)
];
}
]);
lib.mkMerge ([
commonConfig
(dbConfig cfg.ensures)
(pwdConfig cfg.ensures)
(lib.mkIf cfg.enableTCPIP tcpConfig)
(debugConfig cfg.debug)
{
environment.systemPackages = lib.mkIf config.services.postgresql.enable [
(upgrade-script 15 16)
(upgrade-script 16 17)
];
}
]);
}

View file

@ -1,167 +1,226 @@
{ config, pkgs, lib, utils, ... }:
{
config,
pkgs,
lib,
utils,
...
}:
let
cfg = config.shb.restic;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
inherit (lib) concatStringsSep filterAttrs flatten literalExpression optionals listToAttrs mapAttrsToList mkEnableOption mkOption mkMerge;
inherit (lib) hasPrefix mkIf nameValuePair optionalAttrs removePrefix;
inherit (lib.types) attrsOf enum int ints oneOf nonEmptyStr nullOr str submodule;
inherit (lib)
concatStringsSep
filterAttrs
flatten
literalExpression
optionals
listToAttrs
mapAttrsToList
mkEnableOption
mkOption
mkMerge
;
inherit (lib)
hasPrefix
mkIf
nameValuePair
optionalAttrs
removePrefix
;
inherit (lib.types)
attrsOf
enum
int
ints
oneOf
nonEmptyStr
nullOr
str
submodule
;
commonOptions = { name, prefix, config, ... }: {
enable = mkEnableOption ''
this backup intance.
commonOptions =
{
name,
prefix,
config,
...
}:
{
enable = mkEnableOption ''
this backup intance.
A disabled instance will not backup data anymore
but still provides the helper tool to restore snapshots
'';
A disabled instance will not backup data anymore
but still provides the helper tool to restore snapshots
'';
passphrase = lib.mkOption {
description = "Encryption key for the backup repository.";
type = lib.types.submodule {
options = contracts.secret.mkRequester {
mode = "0400";
owner = config.request.user;
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
restartUnits = [ (fullName name config.settings.repository) ];
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
passphrase = lib.mkOption {
description = "Encryption key for the backup repository.";
type = lib.types.submodule {
options = contracts.secret.mkRequester {
mode = "0400";
owner = config.request.user;
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
restartUnits = [ (fullName name config.settings.repository) ];
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
};
};
};
};
repository = mkOption {
description = "Repositories to back this instance to.";
type = submodule {
options = {
path = mkOption {
type = str;
description = "Repository location";
};
secrets = mkOption {
type = attrsOf lib.shb.secretFileType;
default = {};
description = ''
Secrets needed to access the repository where the backups will be stored.
See [s3 config](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3) for an example
and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets.
'';
example = literalExpression ''
{
AWS_ACCESS_KEY_ID.source = <path/to/secret>;
AWS_SECRET_ACCESS_KEY.source = <path/to/secret>;
}
'';
};
timerConfig = mkOption {
type = attrsOf utils.systemdUtils.unitOptions.unitOption;
default = {
OnCalendar = "daily";
Persistent = true;
repository = mkOption {
description = "Repositories to back this instance to.";
type = submodule {
options = {
path = mkOption {
type = str;
description = "Repository location";
};
description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.'';
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
secrets = mkOption {
type = attrsOf lib.shb.secretFileType;
default = { };
description = ''
Secrets needed to access the repository where the backups will be stored.
See [s3 config](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3) for an example
and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets.
'';
example = literalExpression ''
{
AWS_ACCESS_KEY_ID.source = <path/to/secret>;
AWS_SECRET_ACCESS_KEY.source = <path/to/secret>;
}
'';
};
timerConfig = mkOption {
type = attrsOf utils.systemdUtils.unitOptions.unitOption;
default = {
OnCalendar = "daily";
Persistent = true;
};
description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.'';
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
Persistent = true;
};
};
};
};
};
};
retention = mkOption {
description = "For how long to keep backup files.";
type = attrsOf (oneOf [ int nonEmptyStr ]);
default = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
retention = mkOption {
description = "For how long to keep backup files.";
type = attrsOf (oneOf [
int
nonEmptyStr
]);
default = {
keep_within = "1d";
keep_hourly = 24;
keep_daily = 7;
keep_weekly = 4;
keep_monthly = 6;
};
};
limitUploadKiBs = mkOption {
type = nullOr int;
description = "Limit upload bandwidth to the given KiB/s amount.";
default = null;
example = 8000;
};
limitDownloadKiBs = mkOption {
type = nullOr int;
description = "Limit download bandwidth to the given KiB/s amount.";
default = null;
example = 8000;
};
};
limitUploadKiBs = mkOption {
type = nullOr int;
description = "Limit upload bandwidth to the given KiB/s amount.";
default = null;
example = 8000;
};
limitDownloadKiBs = mkOption {
type = nullOr int;
description = "Limit download bandwidth to the given KiB/s amount.";
default = null;
example = 8000;
};
};
repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (removePrefix "/" name);
repoSlugName = name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (removePrefix "/" name);
fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}";
in
{
options.shb.restic = {
instances = mkOption {
description = "Files to backup following the [backup contract](./contracts-backup.html).";
default = {};
type = attrsOf (submodule ({ name, config, ... }: {
options = contracts.backup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Restic provider.
'';
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = contracts.backup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Restic provider.
'';
type = submodule {
options = commonOptions { inherit name config; prefix = "instances"; };
type = submodule {
options = commonOptions {
inherit name config;
prefix = "instances";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}));
}
)
);
};
databases = mkOption {
description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html).";
default = {};
type = attrsOf (submodule ({ name, config, ... }: {
options = contracts.databasebackup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Restic provider.
'';
default = { };
type = attrsOf (
submodule (
{ name, config, ... }:
{
options = contracts.databasebackup.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Restic provider.
'';
type = submodule {
options = commonOptions { inherit name config; prefix = "databases"; };
type = submodule {
options = commonOptions {
inherit name config;
prefix = "databases";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
};
resultCfg = {
restoreScript = fullName name config.settings.repository;
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupService = "${fullName name config.settings.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
performance = mkOption {
description = "Reduce performance impact of backup jobs.";
default = {};
default = { };
type = submodule {
options = {
niceness = mkOption {
@ -170,7 +229,11 @@ in
default = 15;
};
ioSchedulingClass = mkOption {
type = enum [ "idle" "best-effort" "realtime" ];
type = enum [
"idle"
"best-effort"
"realtime"
];
description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
default = "best-effort";
};
@ -184,23 +247,28 @@ in
};
};
config = mkIf (cfg.instances != {} || cfg.databases != {}) (
config = mkIf (cfg.instances != { } || cfg.databases != { }) (
let
enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances;
enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases;
in mkMerge [
in
mkMerge [
{
environment.systemPackages = optionals (enabledInstances != {} || enabledDatabases != {}) [ pkgs.restic ];
environment.systemPackages = optionals (enabledInstances != { } || enabledDatabases != { }) [
pkgs.restic
];
}
{
# Create repository if it is a local path.
systemd.tmpfiles.rules =
let
mkSettings = name: instance: optionals (hasPrefix "/" instance.settings.repository.path) [
"d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -"
];
mkSettings =
name: instance:
optionals (hasPrefix "/" instance.settings.repository.path) [
"d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -"
];
in
flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases));
flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases));
}
{
services.restic.backups =
@ -219,8 +287,8 @@ in
inherit (instance.settings.repository) timerConfig;
pruneOpts = mapAttrsToList (name: value:
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
pruneOpts = mapAttrsToList (
name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}"
) instance.settings.retention;
backupPrepareCommand = concatStringsSep "\n" instance.request.hooks.beforeBackup;
@ -234,12 +302,13 @@ in
++ (optionals (instance.settings.limitDownloadKiBs != null) [
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
]);
} // optionalAttrs (builtins.length instance.request.excludePatterns > 0) {
}
// optionalAttrs (builtins.length instance.request.excludePatterns > 0) {
exclude = instance.request.excludePatterns;
};
};
in
mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
}
{
services.restic.backups =
@ -258,8 +327,8 @@ in
inherit (instance.settings.repository) timerConfig;
pruneOpts = mapAttrsToList (name: value:
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
pruneOpts = mapAttrsToList (
name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}"
) instance.settings.retention;
extraBackupArgs =
@ -269,120 +338,131 @@ in
++ (optionals (instance.settings.limitDownloadKiBs != null) [
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
])
++
(let
cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd;
in
++ (
let
cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd;
in
[
"--stdin-filename ${instance.request.backupName} --stdin-from-command -- ${cmd}/bin/dump.sh"
]);
]
);
};
};
in
mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases));
mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases));
}
{
systemd.services =
let
mkSettings = name: instance:
mkSettings =
name: instance:
let
serviceName = fullName name instance.settings.repository;
in
{
${serviceName} = mkMerge [
{
serviceConfig = {
Nice = cfg.performance.niceness;
IOSchedulingClass = cfg.performance.ioSchedulingClass;
IOSchedulingPriority = cfg.performance.ioPriority;
# BindReadOnlyPaths = instance.sourceDirectories;
};
}
(optionalAttrs (instance.settings.repository.secrets != {})
{
serviceConfig.EnvironmentFile = [
"/run/secrets_restic/${serviceName}"
];
after = [ "${serviceName}-pre.service" ];
requires = [ "${serviceName}-pre.service" ];
})
];
{
${serviceName} = mkMerge [
{
serviceConfig = {
Nice = cfg.performance.niceness;
IOSchedulingClass = cfg.performance.ioSchedulingClass;
IOSchedulingPriority = cfg.performance.ioPriority;
# BindReadOnlyPaths = instance.sourceDirectories;
};
}
(optionalAttrs (instance.settings.repository.secrets != { }) {
serviceConfig.EnvironmentFile = [
"/run/secrets_restic/${serviceName}"
];
after = [ "${serviceName}-pre.service" ];
requires = [ "${serviceName}-pre.service" ];
})
];
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != {})
(let
script = lib.shb.genConfigOutOfBandSystemd {
config = instance.settings.repository.secrets;
configLocation = "/run/secrets_restic/${serviceName}";
generator = lib.shb.toEnvVar;
user = instance.request.user;
};
in
{
script = script.preStart;
serviceConfig.Type = "oneshot";
serviceConfig.LoadCredential = script.loadCredentials;
});
};
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != { }) (
let
script = lib.shb.genConfigOutOfBandSystemd {
config = instance.settings.repository.secrets;
configLocation = "/run/secrets_restic/${serviceName}";
generator = lib.shb.toEnvVar;
user = instance.request.user;
};
in
{
script = script.preStart;
serviceConfig.Type = "oneshot";
serviceConfig.LoadCredential = script.loadCredentials;
}
);
};
in
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
}
{
systemd.services = let
mkEnv = name: instance:
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
enable = true;
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = (lib.shb.replaceSecrets {
userConfig = instance.settings.repository.secrets // {
RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path;
RESTIC_REPOSITORY = instance.settings.repository.path;
};
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
generator = lib.shb.toEnvVar;
user = instance.request.user;
});
};
in
systemd.services =
let
mkEnv =
name: instance:
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
enable = true;
wantedBy = [ "multi-user.target" ];
serviceConfig.Type = "oneshot";
script = (
lib.shb.replaceSecrets {
userConfig = instance.settings.repository.secrets // {
RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path;
RESTIC_REPOSITORY = instance.settings.repository.path;
};
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
generator = lib.shb.toEnvVar;
user = instance.request.user;
}
);
};
in
listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
}
{
environment.systemPackages = let
mkResticBinary = name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
environment.systemPackages =
let
mkResticBinary =
name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
| xargs -d '\n')
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
| xargs -d '\n')
if ! [ "$1" = "restore" ]; then
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
else
shift
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic restore $@ --target /"
fi
if ! [ "$1" = "restore" ]; then
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
else
shift
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic restore $@ --target /"
fi
'';
in
in
flatten (mapAttrsToList mkResticBinary cfg.instances);
}
{
environment.systemPackages = let
mkResticBinary = name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
environment.systemPackages =
let
mkResticBinary =
name: instance:
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
set -euo pipefail
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
| xargs -d '\n')
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
| xargs -d '\n')
if ! [ "$1" = "restore" ]; then
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
else
shift
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupName} | ${instance.request.restoreCmd}"
fi
if ! [ "$1" = "restore" ]; then
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
else
shift
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupName} | ${instance.request.restoreCmd}"
fi
'';
in
in
flatten (mapAttrsToList mkResticBinary cfg.databases);
}
]);
]
);
}

View file

@ -1,9 +1,14 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mapAttrs mkOption;
inherit (lib.types) attrsOf anything submodule;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
cfg = config.shb.sops;
in
@ -11,35 +16,42 @@ in
options.shb.sops = {
secret = mkOption {
description = "Secret following the [secret contract](./contracts-secret.html).";
default = {};
type = attrsOf (submodule ({ name, options, ... }: {
options = contracts.secret.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Sops provider.
default = { };
type = attrsOf (
submodule (
{ name, options, ... }:
{
options = contracts.secret.mkProvider {
settings = mkOption {
description = ''
Settings specific to the Sops provider.
This is a passthrough option to set [sops-nix options](https://github.com/Mic92/sops-nix/blob/master/modules/sops/default.nix).
This is a passthrough option to set [sops-nix options](https://github.com/Mic92/sops-nix/blob/master/modules/sops/default.nix).
Note though that the `mode`, `owner`, `group`, and `restartUnits`
are managed by the [shb.sops.secret.<name>.request](#blocks-sops-options-shb.sops.secret._name_.request) option.
'';
Note though that the `mode`, `owner`, `group`, and `restartUnits`
are managed by the [shb.sops.secret.<name>.request](#blocks-sops-options-shb.sops.secret._name_.request) option.
'';
type = attrsOf anything;
default = {};
};
type = attrsOf anything;
default = { };
};
resultCfg = {
path = "/run/secrets/${name}";
pathText = "/run/secrets/<name>";
};
};
}));
resultCfg = {
path = "/run/secrets/${name}";
pathText = "/run/secrets/<name>";
};
};
}
)
);
};
};
config = {
sops.secrets = let
mkSecret = n: secretCfg: secretCfg.request // secretCfg.settings;
in mapAttrs mkSecret cfg.secret;
sops.secrets =
let
mkSecret = n: secretCfg: secretCfg.request // secretCfg.settings;
in
mapAttrs mkSecret cfg.secret;
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,41 @@
# Inspired from https://github.com/NixOS/nixpkgs/pull/231152 but made it so we can have multiple instances.
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.shb.tinyproxy;
mkValueStringTinyproxy = with lib; v:
if true == v then "yes"
else if false == v then "no"
else generators.mkValueStringDefault {} v;
mkValueStringTinyproxy =
with lib;
v:
if true == v then
"yes"
else if false == v then
"no"
else
generators.mkValueStringDefault { } v;
mkKeyValueTinyproxy = {
mkValueString ? mkValueStringDefault {}
}: sep: k: v:
if null == v then ""
else "${lib.strings.escape [sep] k}${sep}${mkValueString v}";
mkKeyValueTinyproxy =
{
mkValueString ? mkValueStringDefault { },
}:
sep: k: v:
if null == v then "" else "${lib.strings.escape [ sep ] k}${sep}${mkValueString v}";
settingsFormat = (pkgs.formats.keyValue {
settingsFormat = (
pkgs.formats.keyValue {
mkKeyValue = mkKeyValueTinyproxy {
mkValueString = mkValueStringTinyproxy;
} " ";
listsAsDuplicateKeys= true;
});
listsAsDuplicateKeys = true;
}
);
configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings;
in
@ -33,115 +46,124 @@ in
options = {
enable = mkEnableOption "Tinyproxy daemon";
package = mkPackageOption pkgs "tinyproxy" {};
package = mkPackageOption pkgs "tinyproxy" { };
dynamicBindFile = mkOption {
description = ''
File holding the IP to bind to.
File holding the IP to bind to.
'';
default = "";
};
settings = mkOption {
description = ''
Configuration for [tinyproxy](https://tinyproxy.github.io/).
Configuration for [tinyproxy](https://tinyproxy.github.io/).
'';
default = { };
example = literalExpression ''{
Port 8888;
Listen 127.0.0.1;
Timeout 600;
Allow 127.0.0.1;
Anonymous = ['"Host"' '"Authorization"'];
ReversePath = '"/example/" "http://www.example.com/"';
}'';
type = types.submodule ({name, ...}: {
freeformType = settingsFormat.type;
options = {
Listen = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Specify which address to listen to.
'';
example = literalExpression ''
{
Port 8888;
Listen 127.0.0.1;
Timeout 600;
Allow 127.0.0.1;
Anonymous = ['"Host"' '"Authorization"'];
ReversePath = '"/example/" "http://www.example.com/"';
}'';
type = types.submodule (
{ name, ... }:
{
freeformType = settingsFormat.type;
options = {
Listen = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Specify which address to listen to.
'';
};
Port = mkOption {
type = types.int;
default = 8888;
description = ''
Specify which port to listen to.
'';
};
Anonymous = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
If an `Anonymous` keyword is present, then anonymous proxying is enabled. The
headers listed with `Anonymous` are allowed through, while all others are denied.
If no Anonymous keyword is present, then all headers are allowed through. You must
include quotes around the headers.
'';
};
Filter = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Tinyproxy supports filtering of web sites based on URLs or domains. This option
specifies the location of the file containing the filter rules, one rule per line.
'';
};
};
Port = mkOption {
type = types.int;
default = 8888;
description = ''
Specify which port to listen to.
'';
};
Anonymous = mkOption {
type = types.listOf types.str;
default = [];
description = ''
If an `Anonymous` keyword is present, then anonymous proxying is enabled. The
headers listed with `Anonymous` are allowed through, while all others are denied.
If no Anonymous keyword is present, then all headers are allowed through. You must
include quotes around the headers.
'';
};
Filter = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Tinyproxy supports filtering of web sites based on URLs or domains. This option
specifies the location of the file containing the filter rules, one rule per line.
'';
};
};
});
}
);
};
};
};
in
{
shb.tinyproxy = mkOption {
description = "Tinyproxy instances.";
default = {};
type = types.attrsOf instanceOption;
};
{
shb.tinyproxy = mkOption {
description = "Tinyproxy instances.";
default = { };
type = types.attrsOf instanceOption;
};
};
config = {
systemd.services =
let
instanceConfig = name: c: mkIf c.enable {
"tinyproxy-${name}" = {
description = "TinyProxy daemon - instance ${name}";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "tinyproxy";
Group = "tinyproxy";
Type = "simple";
ExecStart = "${getExe c.package} -d -c /etc/tinyproxy/${name}.conf";
ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
KillSignal = "SIGINT";
TimeoutStopSec = "30s";
Restart = "on-failure";
RestartSec = "1s";
RestartSteps = "3";
RestartMaxDelaySec = "10s";
ConfigurationDirectory = "tinyproxy";
instanceConfig =
name: c:
mkIf c.enable {
"tinyproxy-${name}" = {
description = "TinyProxy daemon - instance ${name}";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "tinyproxy";
Group = "tinyproxy";
Type = "simple";
ExecStart = "${getExe c.package} -d -c /etc/tinyproxy/${name}.conf";
ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
KillSignal = "SIGINT";
TimeoutStopSec = "30s";
Restart = "on-failure";
RestartSec = "1s";
RestartSteps = "3";
RestartMaxDelaySec = "10s";
ConfigurationDirectory = "tinyproxy";
};
preStart = concatStringsSep "\n" (
[
"cat ${configFile name c} > /etc/tinyproxy/${name}.conf"
]
++ optionals (c.dynamicBindFile != "") [
"echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf"
"cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf"
]
);
};
preStart = concatStringsSep "\n" ([
"cat ${configFile name c} > /etc/tinyproxy/${name}.conf"
] ++ optionals (c.dynamicBindFile != "") [
"echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf"
"cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf"
]);
};
};
in
mkMerge (mapAttrsToList instanceConfig cfg);
mkMerge (mapAttrsToList instanceConfig cfg);
users.users.tinyproxy = {
group = "tinyproxy";
isSystemUser = true;
};
users.groups.tinyproxy = {};
users.groups.tinyproxy = { };
};
meta.maintainers = with maintainers; [ tcheronneau ];

View file

@ -1,4 +1,9 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.vpn;
@ -6,197 +11,212 @@ let
quoteEach = lib.concatMapStrings (x: ''"${x}"'');
nordvpnConfig =
{ name
, dev
, authFile
, remoteServerIP
, dependentServices ? []
}: ''
client
dev ${dev}
proto tcp
remote ${remoteServerIP} 443
resolv-retry infinite
remote-random
nobind
tun-mtu 1500
tun-mtu-extra 32
mssfix 1450
persist-key
persist-tun
ping 15
ping-restart 0
ping-timer-rem
reneg-sec 0
comp-lzo no
{
name,
dev,
authFile,
remoteServerIP,
dependentServices ? [ ],
}:
''
client
dev ${dev}
proto tcp
remote ${remoteServerIP} 443
resolv-retry infinite
remote-random
nobind
tun-mtu 1500
tun-mtu-extra 32
mssfix 1450
persist-key
persist-tun
ping 15
ping-restart 0
ping-timer-rem
reneg-sec 0
comp-lzo no
status /tmp/openvpn/${name}.status
status /tmp/openvpn/${name}.status
remote-cert-tls server
remote-cert-tls server
auth-user-pass ${authFile}
verb 3
pull
fast-io
cipher AES-256-CBC
auth SHA512
auth-user-pass ${authFile}
verb 3
pull
fast-io
cipher AES-256-CBC
auth SHA512
script-security 2
route-noexec
route-up ${routeUp name dependentServices}/bin/routeUp.sh
down ${routeDown name dependentServices}/bin/routeDown.sh
script-security 2
route-noexec
route-up ${routeUp name dependentServices}/bin/routeUp.sh
down ${routeDown name dependentServices}/bin/routeDown.sh
<ca>
-----BEGIN CERTIFICATE-----
MIIFCjCCAvKgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA5MQswCQYDVQQGEwJQQTEQ
MA4GA1UEChMHTm9yZFZQTjEYMBYGA1UEAxMPTm9yZFZQTiBSb290IENBMB4XDTE2
MDEwMTAwMDAwMFoXDTM1MTIzMTIzNTk1OVowOTELMAkGA1UEBhMCUEExEDAOBgNV
BAoTB05vcmRWUE4xGDAWBgNVBAMTD05vcmRWUE4gUm9vdCBDQTCCAiIwDQYJKoZI
hvcNAQEBBQADggIPADCCAgoCggIBAMkr/BYhyo0F2upsIMXwC6QvkZps3NN2/eQF
kfQIS1gql0aejsKsEnmY0Kaon8uZCTXPsRH1gQNgg5D2gixdd1mJUvV3dE3y9FJr
XMoDkXdCGBodvKJyU6lcfEVF6/UxHcbBguZK9UtRHS9eJYm3rpL/5huQMCppX7kU
eQ8dpCwd3iKITqwd1ZudDqsWaU0vqzC2H55IyaZ/5/TnCk31Q1UP6BksbbuRcwOV
skEDsm6YoWDnn/IIzGOYnFJRzQH5jTz3j1QBvRIuQuBuvUkfhx1FEwhwZigrcxXu
MP+QgM54kezgziJUaZcOM2zF3lvrwMvXDMfNeIoJABv9ljw969xQ8czQCU5lMVmA
37ltv5Ec9U5hZuwk/9QO1Z+d/r6Jx0mlurS8gnCAKJgwa3kyZw6e4FZ8mYL4vpRR
hPdvRTWCMJkeB4yBHyhxUmTRgJHm6YR3D6hcFAc9cQcTEl/I60tMdz33G6m0O42s
Qt/+AR3YCY/RusWVBJB/qNS94EtNtj8iaebCQW1jHAhvGmFILVR9lzD0EzWKHkvy
WEjmUVRgCDd6Ne3eFRNS73gdv/C3l5boYySeu4exkEYVxVRn8DhCxs0MnkMHWFK6
MyzXCCn+JnWFDYPfDKHvpff/kLDobtPBf+Lbch5wQy9quY27xaj0XwLyjOltpiST
LWae/Q4vAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG
SIb3DQEBDQUAA4ICAQC9fUL2sZPxIN2mD32VeNySTgZlCEdVmlq471o/bDMP4B8g
nQesFRtXY2ZCjs50Jm73B2LViL9qlREmI6vE5IC8IsRBJSV4ce1WYxyXro5rmVg/
k6a10rlsbK/eg//GHoJxDdXDOokLUSnxt7gk3QKpX6eCdh67p0PuWm/7WUJQxH2S
DxsT9vB/iZriTIEe/ILoOQF0Aqp7AgNCcLcLAmbxXQkXYCCSB35Vp06u+eTWjG0/
pyS5V14stGtw+fA0DJp5ZJV4eqJ5LqxMlYvEZ/qKTEdoCeaXv2QEmN6dVqjDoTAo
k0t5u4YRXzEVCfXAC3ocplNdtCA72wjFJcSbfif4BSC8bDACTXtnPC7nD0VndZLp
+RiNLeiENhk0oTC+UVdSc+n2nJOzkCK0vYu0Ads4JGIB7g8IB3z2t9ICmsWrgnhd
NdcOe15BincrGA8avQ1cWXsfIKEjbrnEuEk9b5jel6NfHtPKoHc9mDpRdNPISeVa
wDBM1mJChneHt59Nh8Gah74+TM1jBsw4fhJPvoc7Atcg740JErb904mZfkIEmojC
VPhBHVQ9LHBAdM8qFI2kRK0IynOmAZhexlP/aT/kpEsEPyaZQlnBn3An1CRz8h0S
PApL8PytggYKeQmRhl499+6jLxcZ2IegLfqq41dzIjwHwTMplg+1pKIOVojpWA==
-----END CERTIFICATE-----
</ca>
key-direction 1
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
e685bdaf659a25a200e2b9e39e51ff03
0fc72cf1ce07232bd8b2be5e6c670143
f51e937e670eee09d4f2ea5a6e4e6996
5db852c275351b86fc4ca892d78ae002
d6f70d029bd79c4d1c26cf14e9588033
cf639f8a74809f29f72b9d58f9b8f5fe
fc7938eade40e9fed6cb92184abb2cc1
0eb1a296df243b251df0643d53724cdb
5a92a1d6cb817804c4a9319b57d53be5
80815bcfcb2df55018cc83fc43bc7ff8
2d51f9b88364776ee9d12fc85cc7ea5b
9741c4f598c485316db066d52db4540e
212e1518a9bd4828219e24b20d88f598
a196c9de96012090e333519ae18d3509
9427e7b372d348d352dc4c85e18cd4b9
3f8a56ddb2e64eb67adfc9b337157ff4
-----END OpenVPN Static key V1-----
</tls-auth>
<ca>
-----BEGIN CERTIFICATE-----
MIIFCjCCAvKgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA5MQswCQYDVQQGEwJQQTEQ
MA4GA1UEChMHTm9yZFZQTjEYMBYGA1UEAxMPTm9yZFZQTiBSb290IENBMB4XDTE2
MDEwMTAwMDAwMFoXDTM1MTIzMTIzNTk1OVowOTELMAkGA1UEBhMCUEExEDAOBgNV
BAoTB05vcmRWUE4xGDAWBgNVBAMTD05vcmRWUE4gUm9vdCBDQTCCAiIwDQYJKoZI
hvcNAQEBBQADggIPADCCAgoCggIBAMkr/BYhyo0F2upsIMXwC6QvkZps3NN2/eQF
kfQIS1gql0aejsKsEnmY0Kaon8uZCTXPsRH1gQNgg5D2gixdd1mJUvV3dE3y9FJr
XMoDkXdCGBodvKJyU6lcfEVF6/UxHcbBguZK9UtRHS9eJYm3rpL/5huQMCppX7kU
eQ8dpCwd3iKITqwd1ZudDqsWaU0vqzC2H55IyaZ/5/TnCk31Q1UP6BksbbuRcwOV
skEDsm6YoWDnn/IIzGOYnFJRzQH5jTz3j1QBvRIuQuBuvUkfhx1FEwhwZigrcxXu
MP+QgM54kezgziJUaZcOM2zF3lvrwMvXDMfNeIoJABv9ljw969xQ8czQCU5lMVmA
37ltv5Ec9U5hZuwk/9QO1Z+d/r6Jx0mlurS8gnCAKJgwa3kyZw6e4FZ8mYL4vpRR
hPdvRTWCMJkeB4yBHyhxUmTRgJHm6YR3D6hcFAc9cQcTEl/I60tMdz33G6m0O42s
Qt/+AR3YCY/RusWVBJB/qNS94EtNtj8iaebCQW1jHAhvGmFILVR9lzD0EzWKHkvy
WEjmUVRgCDd6Ne3eFRNS73gdv/C3l5boYySeu4exkEYVxVRn8DhCxs0MnkMHWFK6
MyzXCCn+JnWFDYPfDKHvpff/kLDobtPBf+Lbch5wQy9quY27xaj0XwLyjOltpiST
LWae/Q4vAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG
SIb3DQEBDQUAA4ICAQC9fUL2sZPxIN2mD32VeNySTgZlCEdVmlq471o/bDMP4B8g
nQesFRtXY2ZCjs50Jm73B2LViL9qlREmI6vE5IC8IsRBJSV4ce1WYxyXro5rmVg/
k6a10rlsbK/eg//GHoJxDdXDOokLUSnxt7gk3QKpX6eCdh67p0PuWm/7WUJQxH2S
DxsT9vB/iZriTIEe/ILoOQF0Aqp7AgNCcLcLAmbxXQkXYCCSB35Vp06u+eTWjG0/
pyS5V14stGtw+fA0DJp5ZJV4eqJ5LqxMlYvEZ/qKTEdoCeaXv2QEmN6dVqjDoTAo
k0t5u4YRXzEVCfXAC3ocplNdtCA72wjFJcSbfif4BSC8bDACTXtnPC7nD0VndZLp
+RiNLeiENhk0oTC+UVdSc+n2nJOzkCK0vYu0Ads4JGIB7g8IB3z2t9ICmsWrgnhd
NdcOe15BincrGA8avQ1cWXsfIKEjbrnEuEk9b5jel6NfHtPKoHc9mDpRdNPISeVa
wDBM1mJChneHt59Nh8Gah74+TM1jBsw4fhJPvoc7Atcg740JErb904mZfkIEmojC
VPhBHVQ9LHBAdM8qFI2kRK0IynOmAZhexlP/aT/kpEsEPyaZQlnBn3An1CRz8h0S
PApL8PytggYKeQmRhl499+6jLxcZ2IegLfqq41dzIjwHwTMplg+1pKIOVojpWA==
-----END CERTIFICATE-----
</ca>
key-direction 1
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
e685bdaf659a25a200e2b9e39e51ff03
0fc72cf1ce07232bd8b2be5e6c670143
f51e937e670eee09d4f2ea5a6e4e6996
5db852c275351b86fc4ca892d78ae002
d6f70d029bd79c4d1c26cf14e9588033
cf639f8a74809f29f72b9d58f9b8f5fe
fc7938eade40e9fed6cb92184abb2cc1
0eb1a296df243b251df0643d53724cdb
5a92a1d6cb817804c4a9319b57d53be5
80815bcfcb2df55018cc83fc43bc7ff8
2d51f9b88364776ee9d12fc85cc7ea5b
9741c4f598c485316db066d52db4540e
212e1518a9bd4828219e24b20d88f598
a196c9de96012090e333519ae18d3509
9427e7b372d348d352dc4c85e18cd4b9
3f8a56ddb2e64eb67adfc9b337157ff4
-----END OpenVPN Static key V1-----
</tls-auth>
'';
routeUp = name: dependentServices: pkgs.writeShellApplication {
name = "routeUp.sh";
routeUp =
name: dependentServices:
pkgs.writeShellApplication {
name = "routeUp.sh";
runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools ];
runtimeInputs = [
pkgs.iproute2
pkgs.systemd
pkgs.nettools
];
text = ''
echo "Running route-up..."
text = ''
echo "Running route-up..."
echo "dev=''${dev:?}"
echo "ifconfig_local=''${ifconfig_local:?}"
echo "route_vpn_gateway=''${route_vpn_gateway:?}"
echo "dev=''${dev:?}"
echo "ifconfig_local=''${ifconfig_local:?}"
echo "route_vpn_gateway=''${route_vpn_gateway:?}"
set -x
set -x
ip rule
ip rule add from "''${ifconfig_local:?}/32" table ${name}
ip rule add to "''${route_vpn_gateway:?}/32" table ${name}
ip rule
ip rule
ip rule add from "''${ifconfig_local:?}/32" table ${name}
ip rule add to "''${route_vpn_gateway:?}/32" table ${name}
ip rule
ip route list table ${name} || :
retVal=$?
if [ $retVal -eq 2 ]; then
echo "table is empty"
elif [ $retVal -ne 0 ]; then
exit 1
fi
ip route add default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name}
ip route flush cache
ip route list table ${name} || :
retVal=$?
if [ $retVal -eq 2 ]; then
echo "table is empty"
elif [ $retVal -ne 0 ]; then
exit 1
fi
ip route list table ${name} || :
retVal=$?
if [ $retVal -eq 2 ]; then
echo "table is empty"
elif [ $retVal -ne 0 ]; then
exit 1
fi
ip route add default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name}
ip route flush cache
ip route list table ${name} || :
retVal=$?
if [ $retVal -eq 2 ]; then
echo "table is empty"
elif [ $retVal -ne 0 ]; then
exit 1
fi
echo "''${ifconfig_local:?}" > /run/openvpn/${name}/ifconfig_local
echo "''${ifconfig_local:?}" > /run/openvpn/${name}/ifconfig_local
dependencies=(${quoteEach dependentServices})
for i in "''${dependencies[@]}"; do
systemctl restart "$i" || :
done
dependencies=(${quoteEach dependentServices})
for i in "''${dependencies[@]}"; do
systemctl restart "$i" || :
done
echo "Running route-up DONE"
'';
};
echo "Running route-up DONE"
'';
};
routeDown = name: dependentServices: pkgs.writeShellApplication {
name = "routeDown.sh";
routeDown =
name: dependentServices:
pkgs.writeShellApplication {
name = "routeDown.sh";
runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools pkgs.coreutils ];
runtimeInputs = [
pkgs.iproute2
pkgs.systemd
pkgs.nettools
pkgs.coreutils
];
text = ''
echo "Running route-down..."
text = ''
echo "Running route-down..."
echo "dev=''${dev:?}"
echo "ifconfig_local=''${ifconfig_local:?}"
echo "route_vpn_gateway=''${route_vpn_gateway:?}"
echo "dev=''${dev:?}"
echo "ifconfig_local=''${ifconfig_local:?}"
echo "route_vpn_gateway=''${route_vpn_gateway:?}"
set -x
set -x
ip rule
ip rule del from "''${ifconfig_local:?}/32" table ${name}
ip rule del to "''${route_vpn_gateway:?}/32" table ${name}
ip rule
ip rule
ip rule del from "''${ifconfig_local:?}/32" table ${name}
ip rule del to "''${route_vpn_gateway:?}/32" table ${name}
ip rule
# This will probably fail because the dev is already gone.
ip route list table ${name} || :
retVal=$?
if [ $retVal -eq 2 ]; then
echo "table is empty"
elif [ $retVal -ne 0 ]; then
exit 1
fi
ip route del default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} || :
ip route flush cache
ip route list table ${name} || :
retVal=$?
if [ $retVal -eq 2 ]; then
echo "table is empty"
elif [ $retVal -ne 0 ]; then
exit 1
fi
# This will probably fail because the dev is already gone.
ip route list table ${name} || :
retVal=$?
if [ $retVal -eq 2 ]; then
echo "table is empty"
elif [ $retVal -ne 0 ]; then
exit 1
fi
ip route del default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} || :
ip route flush cache
ip route list table ${name} || :
retVal=$?
if [ $retVal -eq 2 ]; then
echo "table is empty"
elif [ $retVal -ne 0 ]; then
exit 1
fi
rm /run/openvpn/${name}/ifconfig_local
rm /run/openvpn/${name}/ifconfig_local
dependencies=(${quoteEach dependentServices})
for i in "''${dependencies[@]}"; do
systemctl stop "$i" || :
done
dependencies=(${quoteEach dependentServices})
for i in "''${dependencies[@]}"; do
systemctl stop "$i" || :
done
echo "Running route-down DONE"
'';
};
echo "Running route-down DONE"
'';
};
in
{
options =
@ -205,7 +225,7 @@ in
options = {
enable = lib.mkEnableOption "OpenVPN config";
package = lib.mkPackageOption pkgs "openvpn" {};
package = lib.mkPackageOption pkgs "openvpn" { };
provider = lib.mkOption {
description = "VPN provider, if given uses ready-made configuration.";
@ -243,68 +263,76 @@ in
};
};
in
{
shb.vpn = lib.mkOption {
description = "OpenVPN instances.";
default = {};
type = lib.types.attrsOf instanceOption;
};
{
shb.vpn = lib.mkOption {
description = "OpenVPN instances.";
default = { };
type = lib.types.attrsOf instanceOption;
};
};
config = {
services.openvpn.servers =
let
instanceConfig = name: c: lib.mkIf c.enable {
${name} = {
autoStart = true;
instanceConfig =
name: c:
lib.mkIf c.enable {
${name} = {
autoStart = true;
up = "mkdir -p /run/openvpn/${name}";
up = "mkdir -p /run/openvpn/${name}";
config = nordvpnConfig {
inherit name;
inherit (c) dev remoteServerIP authFile;
dependentServices = lib.optional (c.proxyPort != null) "tinyproxy-${name}.service";
config = nordvpnConfig {
inherit name;
inherit (c) dev remoteServerIP authFile;
dependentServices = lib.optional (c.proxyPort != null) "tinyproxy-${name}.service";
};
};
};
};
in
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
systemd.tmpfiles.rules = map (name:
"d /tmp/openvpn/${name}.status 0700 root root"
) (lib.attrNames cfg);
systemd.tmpfiles.rules = map (name: "d /tmp/openvpn/${name}.status 0700 root root") (
lib.attrNames cfg
);
networking.iproute2.enable = true;
networking.iproute2.rttablesExtraConfig =
lib.concatStringsSep "\n" (lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg);
networking.iproute2.rttablesExtraConfig = lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg
);
shb.tinyproxy =
let
instanceConfig = name: c: lib.mkIf (c.enable && c.proxyPort != null) {
${name} = {
enable = true;
# package = pkgs.tinyproxy.overrideAttrs (old: {
# withDebug = false;
# patches = old.patches ++ [
# (pkgs.fetchpatch {
# name = "";
# url = "https://github.com/tinyproxy/tinyproxy/pull/494/commits/2532ba09896352b31f3538d7819daa1fc3f829f1.patch";
# sha256 = "sha256-Q0MkHnttW8tH3+hoCt9ACjHjmmZQgF6pC/menIrU0Co=";
# })
# ];
# });
dynamicBindFile = "/run/openvpn/${name}/ifconfig_local";
settings = {
Port = c.proxyPort;
Listen = "127.0.0.1";
Syslog = "On";
LogLevel = "Info";
Allow = [ "127.0.0.1" "::1" ];
ViaProxyName = ''"tinyproxy"'';
instanceConfig =
name: c:
lib.mkIf (c.enable && c.proxyPort != null) {
${name} = {
enable = true;
# package = pkgs.tinyproxy.overrideAttrs (old: {
# withDebug = false;
# patches = old.patches ++ [
# (pkgs.fetchpatch {
# name = "";
# url = "https://github.com/tinyproxy/tinyproxy/pull/494/commits/2532ba09896352b31f3538d7819daa1fc3f829f1.patch";
# sha256 = "sha256-Q0MkHnttW8tH3+hoCt9ACjHjmmZQgF6pC/menIrU0Co=";
# })
# ];
# });
dynamicBindFile = "/run/openvpn/${name}/ifconfig_local";
settings = {
Port = c.proxyPort;
Listen = "127.0.0.1";
Syslog = "On";
LogLevel = "Info";
Allow = [
"127.0.0.1"
"::1"
];
ViaProxyName = ''"tinyproxy"'';
};
};
};
};
in
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
};
}

View file

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

View file

@ -1,22 +1,35 @@
{ lib, ... }:
let
inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString;
inherit (lib.types) listOf nonEmptyListOf submodule str;
inherit (lib)
concatStringsSep
literalMD
mkOption
optionalAttrs
optionalString
;
inherit (lib.types)
listOf
nonEmptyListOf
submodule
str
;
inherit (lib.shb) anyNotNull;
in
{
mkRequest =
{ user ? "",
{
user ? "",
userText ? null,
sourceDirectories ? [ "/var/lib/example" ],
sourceDirectoriesText ? null,
excludePatterns ? [],
excludePatterns ? [ ],
excludePatternsText ? null,
beforeBackup ? [],
beforeBackup ? [ ],
beforeBackupText ? null,
afterBackup ? [],
afterBackup ? [ ],
afterBackupText ? null,
}: mkOption {
}:
mkOption {
description = ''
Request part of the backup contract.
@ -31,72 +44,102 @@ in
};
};
defaultText = optionalString (anyNotNull [
userText
sourceDirectoriesText
excludePatternsText
beforeBackupText
afterBackupText
]) (literalMD ''
{
user = ${if userText != null then userText else user};
sourceDirectories = ${if sourceDirectoriesText != null then sourceDirectoriesText else "[ " + concatStringsSep " " sourceDirectories + " ]"};
excludePatterns = ${if excludePatternsText != null then excludePatternsText else "[ " + concatStringsSep " " excludePatterns + " ]"};
hooks.beforeBackup = ${if beforeBackupText != null then beforeBackupText else "[ " + concatStringsSep " " beforeBackup + " ]"};
hooks.afterBackup = ${if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]"};
};
'');
defaultText =
optionalString
(anyNotNull [
userText
sourceDirectoriesText
excludePatternsText
beforeBackupText
afterBackupText
])
(literalMD ''
{
user = ${if userText != null then userText else user};
sourceDirectories = ${
if sourceDirectoriesText != null then
sourceDirectoriesText
else
"[ " + concatStringsSep " " sourceDirectories + " ]"
};
excludePatterns = ${
if excludePatternsText != null then
excludePatternsText
else
"[ " + concatStringsSep " " excludePatterns + " ]"
};
hooks.beforeBackup = ${
if beforeBackupText != null then
beforeBackupText
else
"[ " + concatStringsSep " " beforeBackup + " ]"
};
hooks.afterBackup = ${
if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]"
};
};
'');
type = submodule {
options = {
user = mkOption {
description = ''
Unix user doing the backups.
'';
type = str;
example = "vaultwarden";
default = user;
} // optionalAttrs (userText != null) {
defaultText = literalMD userText;
};
user =
mkOption {
description = ''
Unix user doing the backups.
'';
type = str;
example = "vaultwarden";
default = user;
}
// optionalAttrs (userText != null) {
defaultText = literalMD userText;
};
sourceDirectories = mkOption {
description = "Directories to backup.";
type = nonEmptyListOf str;
example = "/var/lib/vaultwarden";
default = sourceDirectories;
} // optionalAttrs (sourceDirectoriesText != null) {
defaultText = literalMD sourceDirectoriesText;
};
sourceDirectories =
mkOption {
description = "Directories to backup.";
type = nonEmptyListOf str;
example = "/var/lib/vaultwarden";
default = sourceDirectories;
}
// optionalAttrs (sourceDirectoriesText != null) {
defaultText = literalMD sourceDirectoriesText;
};
excludePatterns = mkOption {
description = "File patterns to exclude.";
type = listOf str;
default = excludePatterns;
} // optionalAttrs (excludePatternsText != null) {
defaultText = literalMD excludePatternsText;
};
excludePatterns =
mkOption {
description = "File patterns to exclude.";
type = listOf str;
default = excludePatterns;
}
// optionalAttrs (excludePatternsText != null) {
defaultText = literalMD excludePatternsText;
};
hooks = mkOption {
description = "Hooks to run around the backup.";
default = {};
default = { };
type = submodule {
options = {
beforeBackup = mkOption {
description = "Hooks to run before backup.";
type = listOf str;
default = beforeBackup;
} // optionalAttrs (beforeBackupText != null) {
defaultText = literalMD beforeBackupText;
};
beforeBackup =
mkOption {
description = "Hooks to run before backup.";
type = listOf str;
default = beforeBackup;
}
// optionalAttrs (beforeBackupText != null) {
defaultText = literalMD beforeBackupText;
};
afterBackup = mkOption {
description = "Hooks to run after backup.";
type = listOf str;
default = afterBackup;
} // optionalAttrs (afterBackupText != null) {
defaultText = literalMD afterBackupText;
};
afterBackup =
mkOption {
description = "Hooks to run after backup.";
type = listOf str;
default = afterBackup;
}
// optionalAttrs (afterBackupText != null) {
defaultText = literalMD afterBackupText;
};
};
};
};
@ -104,70 +147,79 @@ in
};
};
mkResult = {
restoreScript ? "restore",
restoreScriptText ? null,
backupService ? "backup.service",
backupServiceText ? null,
}: mkOption {
description = ''
Result part of the backup contract.
Options set by the provider module that indicates the name of the backup and restor scripts.
'';
default = {
inherit restoreScript backupService;
};
defaultText = optionalString (anyNotNull [
restoreScriptText
backupServiceText
]) (literalMD ''
mkResult =
{
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
backupService = ${if backupServiceText != null then backupServiceText else backupService};
}
'');
restoreScript ? "restore",
restoreScriptText ? null,
backupService ? "backup.service",
backupServiceText ? null,
}:
mkOption {
description = ''
Result part of the backup contract.
type = submodule {
options = {
restoreScript = mkOption {
description = ''
Name of script that can restore the database.
One can then list snapshots with:
Options set by the provider module that indicates the name of the backup and restor scripts.
'';
default = {
inherit restoreScript backupService;
};
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
```
defaultText =
optionalString
(anyNotNull [
restoreScriptText
backupServiceText
])
(literalMD ''
{
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
backupService = ${if backupServiceText != null then backupServiceText else backupService};
}
'');
And restore the database with:
type = submodule {
options = {
restoreScript =
mkOption {
description = ''
Name of script that can restore the database.
One can then list snapshots with:
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
```
'';
type = str;
default = restoreScript;
} // optionalAttrs (restoreScriptText != null) {
defaultText = literalMD restoreScriptText;
};
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
```
backupService = mkOption {
description = ''
Name of service backing up the database.
And restore the database with:
This script can be ran manually to backup the database:
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
```
'';
type = str;
default = restoreScript;
}
// optionalAttrs (restoreScriptText != null) {
defaultText = literalMD restoreScriptText;
};
```bash
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
```
'';
type = str;
default = backupService;
} // optionalAttrs (backupServiceText != null) {
defaultText = literalMD backupServiceText;
backupService =
mkOption {
description = ''
Name of service backing up the database.
This script can be ran manually to backup the database:
```bash
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
```
'';
type = str;
default = backupService;
}
// optionalAttrs (backupServiceText != null) {
defaultText = literalMD backupServiceText;
};
};
};
};
};
}

View file

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

View file

@ -1,10 +1,17 @@
{ pkgs, lib }:
let
inherit (lib) concatMapStringsSep getAttrFromPath mkIf optionalAttrs setAttrByPath;
inherit (lib)
concatMapStringsSep
getAttrFromPath
mkIf
optionalAttrs
setAttrByPath
;
in
{ name,
{
name,
providerRoot,
modules ? [],
modules ? [ ],
username ? "me",
sourceDirectories ? [
"/opt/files/A"
@ -12,107 +19,115 @@ in
],
settings, # { repository, config } -> attrset
extraConfig ? null, # { username, config } -> attrset
}: lib.shb.runNixOSTest {
}:
lib.shb.runNixOSTest {
inherit name;
nodes.machine = { config, ... }: {
imports = [ lib.shb.baseImports ] ++ modules;
nodes.machine =
{ config, ... }:
{
imports = [ lib.shb.baseImports ] ++ modules;
config = lib.mkMerge [
(setAttrByPath providerRoot {
request = {
inherit sourceDirectories;
user = username;
};
settings = settings {
inherit config;
repository = "/opt/repos/${name}";
};
})
(mkIf (username != "root") {
users.users.${username} = {
isSystemUser = true;
extraGroups = [ "sudoers" ];
group = "root";
};
})
(optionalAttrs (extraConfig != null) (extraConfig { inherit username config; }))
];
};
config = lib.mkMerge [
(setAttrByPath providerRoot {
request = {
inherit sourceDirectories;
user = username;
};
settings = settings {
inherit config;
repository = "/opt/repos/${name}";
};
})
(mkIf (username != "root") {
users.users.${username} = {
isSystemUser = true;
extraGroups = [ "sudoers" ];
group = "root";
};
})
(optionalAttrs (extraConfig != null) (extraConfig {
inherit username config;
}))
];
};
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
testScript = { nodes, ... }: let
provider = (getAttrFromPath providerRoot nodes.machine).result;
in ''
from dictdiffer import diff
testScript =
{ nodes, ... }:
let
provider = (getAttrFromPath providerRoot nodes.machine).result;
in
''
from dictdiffer import diff
username = "${username}"
sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ]
username = "${username}"
sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ]
def list_files(dir):
files_and_content = {}
def list_files(dir):
files_and_content = {}
files = machine.succeed(f"""find {dir} -type f""").split("\n")[:-1]
files = machine.succeed(f"""find {dir} -type f""").split("\n")[:-1]
for f in files:
content = machine.succeed(f"""cat {f}""").strip()
files_and_content[f] = content
for f in files:
content = machine.succeed(f"""cat {f}""").strip()
files_and_content[f] = content
return files_and_content
return files_and_content
def assert_files(dir, files):
result = list(diff(list_files(dir), files))
if len(result) > 0:
raise Exception("Unexpected files:", result)
def assert_files(dir, files):
result = list(diff(list_files(dir), files))
if len(result) > 0:
raise Exception("Unexpected files:", result)
with subtest("Create initial content"):
for path in sourceDirectories:
machine.succeed(f"""
mkdir -p {path}
echo repo_fileA_1 > {path}/fileA
echo repo_fileB_1 > {path}/fileB
with subtest("Create initial content"):
for path in sourceDirectories:
machine.succeed(f"""
mkdir -p {path}
echo repo_fileA_1 > {path}/fileA
echo repo_fileB_1 > {path}/fileB
chown {username}: -R {path}
chmod go-rwx -R {path}
""")
for path in sourceDirectories:
assert_files(path, {
f'{path}/fileA': 'repo_fileA_1',
f'{path}/fileB': 'repo_fileB_1',
})
with subtest("First backup in repo"):
print(machine.succeed("systemctl cat ${provider.backupService}"))
machine.succeed("systemctl start ${provider.backupService}")
with subtest("New content"):
for path in sourceDirectories:
machine.succeed(f"""
echo repo_fileA_2 > {path}/fileA
echo repo_fileB_2 > {path}/fileB
chown {username}: -R {path}
chmod go-rwx -R {path}
""")
assert_files(path, {
f'{path}/fileA': 'repo_fileA_2',
f'{path}/fileB': 'repo_fileB_2',
})
for path in sourceDirectories:
assert_files(path, {
f'{path}/fileA': 'repo_fileA_1',
f'{path}/fileB': 'repo_fileB_1',
})
with subtest("Delete content"):
for path in sourceDirectories:
machine.succeed(f"""rm -r {path}/*""")
with subtest("First backup in repo"):
print(machine.succeed("systemctl cat ${provider.backupService}"))
machine.succeed("systemctl start ${provider.backupService}")
assert_files(path, {})
with subtest("New content"):
for path in sourceDirectories:
machine.succeed(f"""
echo repo_fileA_2 > {path}/fileA
echo repo_fileB_2 > {path}/fileB
""")
with subtest("Restore initial content from repo"):
machine.succeed("""${provider.restoreScript} restore latest""")
assert_files(path, {
f'{path}/fileA': 'repo_fileA_2',
f'{path}/fileB': 'repo_fileB_2',
})
for path in sourceDirectories:
assert_files(path, {
f'{path}/fileA': 'repo_fileA_1',
f'{path}/fileB': 'repo_fileB_1',
})
with subtest("Delete content"):
for path in sourceDirectories:
machine.succeed(f"""rm -r {path}/*""")
assert_files(path, {})
with subtest("Restore initial content from repo"):
machine.succeed("""${provider.restoreScript} restore latest""")
for path in sourceDirectories:
assert_files(path, {
f'{path}/fileA': 'repo_fileA_1',
f'{path}/fileB': 'repo_fileB_1',
})
'';
}

View file

@ -1,12 +1,19 @@
{ lib, ... }:
let
inherit (lib) mkOption literalExpression literalMD optionalAttrs optionalString;
inherit (lib)
mkOption
literalExpression
literalMD
optionalAttrs
optionalString
;
inherit (lib.types) submodule str;
inherit (lib.shb) anyNotNull;
in
{
mkRequest =
{ user ? "root",
{
user ? "root",
userText ? null,
backupName ? "dump",
backupNameText ? null,
@ -14,7 +21,8 @@ in
backupCmdText ? null,
restoreCmd ? "",
restoreCmdText ? null,
}: mkOption {
}:
mkOption {
description = ''
Request part of the backup contract.
@ -23,137 +31,161 @@ in
'';
default = {
inherit user backupName backupCmd restoreCmd;
inherit
user
backupName
backupCmd
restoreCmd
;
};
defaultText = optionalString (anyNotNull [
userText
backupNameText
backupCmdText
restoreCmdText
]) (literalMD ''
{
user = ${if userText != null then userText else user};
backupName = ${if backupNameText != null then backupNameText else backupName};
backupCmd = ${if backupCmdText != null then backupCmdText else backupCmd};
restoreCmd = ${if restoreCmdText != null then restoreCmdText else restoreCmd};
}
'');
defaultText =
optionalString
(anyNotNull [
userText
backupNameText
backupCmdText
restoreCmdText
])
(literalMD ''
{
user = ${if userText != null then userText else user};
backupName = ${if backupNameText != null then backupNameText else backupName};
backupCmd = ${if backupCmdText != null then backupCmdText else backupCmd};
restoreCmd = ${if restoreCmdText != null then restoreCmdText else restoreCmd};
}
'');
type = submodule {
options = {
user = mkOption {
description = ''
Unix user doing the backups.
user =
mkOption {
description = ''
Unix user doing the backups.
This should be an admin user having access to all databases.
'';
type = str;
example = "postgres";
default = user;
} // optionalAttrs (userText != null) {
defaultText = literalMD userText;
};
This should be an admin user having access to all databases.
'';
type = str;
example = "postgres";
default = user;
}
// optionalAttrs (userText != null) {
defaultText = literalMD userText;
};
backupName = mkOption {
description = "Name of the backup in the repository.";
type = str;
example = "postgresql.sql";
default = backupName;
} // optionalAttrs (backupNameText != null) {
defaultText = literalMD backupNameText;
};
backupName =
mkOption {
description = "Name of the backup in the repository.";
type = str;
example = "postgresql.sql";
default = backupName;
}
// optionalAttrs (backupNameText != null) {
defaultText = literalMD backupNameText;
};
backupCmd = mkOption {
description = "Command that produces the database dump on stdout.";
type = str;
example = literalExpression ''
''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable
'';
default = backupCmd;
} // optionalAttrs (backupCmdText != null) {
defaultText = literalMD backupCmdText;
};
backupCmd =
mkOption {
description = "Command that produces the database dump on stdout.";
type = str;
example = literalExpression ''
''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable
'';
default = backupCmd;
}
// optionalAttrs (backupCmdText != null) {
defaultText = literalMD backupCmdText;
};
restoreCmd = mkOption {
description = "Command that reads the database dump on stdin and restores the database.";
type = str;
example = literalExpression ''
''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres
'';
default = restoreCmd;
} // optionalAttrs (restoreCmdText != null) {
defaultText = literalMD restoreCmdText;
};
restoreCmd =
mkOption {
description = "Command that reads the database dump on stdin and restores the database.";
type = str;
example = literalExpression ''
''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres
'';
default = restoreCmd;
}
// optionalAttrs (restoreCmdText != null) {
defaultText = literalMD restoreCmdText;
};
};
};
};
mkResult = {
restoreScript ? "restore",
restoreScriptText ? null,
backupService ? "backup.service",
backupServiceText ? null,
}: mkOption {
description = ''
Result part of the backup contract.
Options set by the provider module that indicates the name of the backup and restor scripts.
'';
default = {
inherit restoreScript backupService;
};
defaultText = optionalString (anyNotNull [
restoreScriptText
backupServiceText
]) (literalMD ''
mkResult =
{
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
backupService = ${if backupServiceText != null then backupServiceText else backupService};
}
'');
restoreScript ? "restore",
restoreScriptText ? null,
backupService ? "backup.service",
backupServiceText ? null,
}:
mkOption {
description = ''
Result part of the backup contract.
type = submodule {
options = {
restoreScript = mkOption {
description = ''
Name of script that can restore the database.
One can then list snapshots with:
Options set by the provider module that indicates the name of the backup and restor scripts.
'';
default = {
inherit restoreScript backupService;
};
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
```
defaultText =
optionalString
(anyNotNull [
restoreScriptText
backupServiceText
])
(literalMD ''
{
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
backupService = ${if backupServiceText != null then backupServiceText else backupService};
}
'');
And restore the database with:
type = submodule {
options = {
restoreScript =
mkOption {
description = ''
Name of script that can restore the database.
One can then list snapshots with:
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
```
'';
type = str;
default = restoreScript;
} // optionalAttrs (restoreScriptText != null) {
defaultText = literalMD restoreScriptText;
};
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
```
backupService = mkOption {
description = ''
Name of service backing up the database.
And restore the database with:
This script can be ran manually to backup the database:
```bash
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
```
'';
type = str;
default = restoreScript;
}
// optionalAttrs (restoreScriptText != null) {
defaultText = literalMD restoreScriptText;
};
```bash
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
```
'';
type = str;
default = backupService;
} // optionalAttrs (backupServiceText != null) {
defaultText = literalMD backupServiceText;
backupService =
mkOption {
description = ''
Name of service backing up the database.
This script can be ran manually to backup the database:
```bash
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
```
'';
type = str;
default = backupService;
}
// optionalAttrs (backupServiceText != null) {
defaultText = literalMD backupServiceText;
};
};
};
};
};
}

View file

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

View file

@ -1,84 +1,98 @@
{ pkgs, lib }:
let
inherit (lib) getAttrFromPath mkIf optionalAttrs setAttrByPath;
inherit (lib)
getAttrFromPath
mkIf
optionalAttrs
setAttrByPath
;
in
{ name,
{
name,
requesterRoot,
providerRoot,
extraConfig ? null, # { config, database } -> attrset
modules ? [],
modules ? [ ],
database ? "me",
settings, # { repository, config } -> attrset
}: lib.shb.runNixOSTest {
}:
lib.shb.runNixOSTest {
inherit name;
nodes.machine = { config, ... }: {
imports = [ lib.shb.baseImports ] ++ modules;
config = lib.mkMerge [
(setAttrByPath providerRoot {
request = (getAttrFromPath requesterRoot config).request;
settings = settings {
inherit config;
repository = "/opt/repos/database";
};
})
(mkIf (database != "root") {
users.users.${database} = {
isSystemUser = true;
extraGroups = [ "sudoers" ];
group = "root";
};
})
(optionalAttrs (extraConfig != null) (extraConfig { inherit config database; }))
];
};
nodes.machine =
{ config, ... }:
{
imports = [ lib.shb.baseImports ] ++ modules;
config = lib.mkMerge [
(setAttrByPath providerRoot {
request = (getAttrFromPath requesterRoot config).request;
settings = settings {
inherit config;
repository = "/opt/repos/database";
};
})
(mkIf (database != "root") {
users.users.${database} = {
isSystemUser = true;
extraGroups = [ "sudoers" ];
group = "root";
};
})
(optionalAttrs (extraConfig != null) (extraConfig {
inherit config database;
}))
];
};
testScript = { nodes, ... }: let
provider = getAttrFromPath providerRoot nodes.machine;
in ''
import csv
testScript =
{ nodes, ... }:
let
provider = getAttrFromPath providerRoot nodes.machine;
in
''
import csv
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
def peer_cmd(cmd, db="me"):
return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db)
def peer_cmd(cmd, db="me"):
return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db)
def query(query):
res = machine.succeed(peer_cmd(query))
return list(dict(l) for l in csv.DictReader(res.splitlines()))
def query(query):
res = machine.succeed(peer_cmd(query))
return list(dict(l) for l in csv.DictReader(res.splitlines()))
def cmp_tables(a, b):
for i in range(max(len(a), len(b))):
diff = set(a[i]) ^ set(b[i])
if len(diff) > 0:
raise Exception(i, diff)
def cmp_tables(a, b):
for i in range(max(len(a), len(b))):
diff = set(a[i]) ^ set(b[i])
if len(diff) > 0:
raise Exception(i, diff)
table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}]
table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}]
with subtest("create fixture"):
machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)"))
machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)"))
with subtest("create fixture"):
machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)"))
machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)"))
res = query("SELECT * FROM test")
cmp_tables(res, table)
res = query("SELECT * FROM test")
cmp_tables(res, table)
with subtest("backup"):
print(machine.succeed("systemctl cat ${provider.result.backupService}"))
print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase"))
machine.succeed("systemctl start ${provider.result.backupService}")
with subtest("backup"):
print(machine.succeed("systemctl cat ${provider.result.backupService}"))
print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase"))
machine.succeed("systemctl start ${provider.result.backupService}")
with subtest("drop database"):
machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres"))
machine.fail(peer_cmd("SELECT * FROM test"))
with subtest("drop database"):
machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres"))
machine.fail(peer_cmd("SELECT * FROM test"))
with subtest("restore"):
print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})"))
machine.succeed("${provider.result.restoreScript} restore latest ")
with subtest("restore"):
print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})"))
machine.succeed("${provider.result.restoreScript} restore latest ")
with subtest("check restoration"):
res = query("SELECT * FROM test")
cmp_tables(res, table)
'';
with subtest("check restoration"):
res = query("SELECT * FROM test")
cmp_tables(res, table)
'';
}

View file

@ -4,55 +4,61 @@ let
inherit (lib.types) anything;
mkContractFunctions =
{ mkRequest,
{
mkRequest,
mkResult,
}: {
}:
{
mkRequester = requestCfg: {
request = mkRequest requestCfg;
result = mkResult {};
result = mkResult { };
};
mkProvider =
{ resultCfg,
settings ? {},
}: {
request = mkRequest {};
{
resultCfg,
settings ? { },
}:
{
request = mkRequest { };
result = mkResult resultCfg;
} // optionalAttrs (settings != {}) { inherit settings; };
}
// optionalAttrs (settings != { }) { inherit settings; };
contract = {
request = mkRequest {};
request = mkRequest { };
result = mkResult {};
result = mkResult { };
settings = mkOption {
description = ''
Optional attribute set with options specific to the provider.
Optional attribute set with options specific to the provider.
'';
type = anything;
};
};
};
importContract = module:
importContract =
module:
let
importedModule = pkgs.callPackage module {};
importedModule = pkgs.callPackage module { };
in
mkContractFunctions {
inherit (importedModule) mkRequest mkResult;
};
mkContractFunctions {
inherit (importedModule) mkRequest mkResult;
};
in
{
databasebackup = importContract ./databasebackup.nix;
backup = importContract ./backup.nix;
mount = pkgs.callPackage ./mount.nix {};
mount = pkgs.callPackage ./mount.nix { };
secret = importContract ./secret.nix;
ssl = pkgs.callPackage ./ssl.nix {};
ssl = pkgs.callPackage ./ssl.nix { };
test = {
secret = pkgs.callPackage ./secret/test.nix {};
databasebackup = pkgs.callPackage ./databasebackup/test.nix {};
backup = pkgs.callPackage ./backup/test.nix {};
secret = pkgs.callPackage ./secret/test.nix { };
databasebackup = pkgs.callPackage ./databasebackup/test.nix { };
backup = pkgs.callPackage ./backup/test.nix { };
};
}

View file

@ -1,20 +1,28 @@
{ lib, ... }:
let
inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString;
inherit (lib)
concatStringsSep
literalMD
mkOption
optionalAttrs
optionalString
;
inherit (lib.types) listOf submodule str;
inherit (lib.shb) anyNotNull;
in
{
mkRequest =
{ mode ? "0400",
{
mode ? "0400",
modeText ? null,
owner ? "root",
ownerText ? null,
group ? "root",
groupText ? null,
restartUnits ? [],
restartUnits ? [ ],
restartUnitsText ? null,
}: mkOption {
}:
mkOption {
description = ''
Request part of the secret contract.
@ -23,64 +31,87 @@ in
'';
default = {
inherit mode owner group restartUnits;
inherit
mode
owner
group
restartUnits
;
};
defaultText = optionalString (anyNotNull [
modeText
ownerText
groupText
restartUnitsText
]) (literalMD ''
{
mode = ${if modeText != null then modeText else mode};
owner = ${if ownerText != null then ownerText else owner};
group = ${if groupText != null then groupText else group};
restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + concatStringsSep " " restartUnits + " ]"};
}
'');
defaultText =
optionalString
(anyNotNull [
modeText
ownerText
groupText
restartUnitsText
])
(literalMD ''
{
mode = ${if modeText != null then modeText else mode};
owner = ${if ownerText != null then ownerText else owner};
group = ${if groupText != null then groupText else group};
restartUnits = ${
if restartUnitsText != null then
restartUnitsText
else
"[ " + concatStringsSep " " restartUnits + " ]"
};
}
'');
type = submodule {
options = {
mode = mkOption {
description = ''
Mode of the secret file.
'';
type = str;
default = mode;
} // optionalAttrs (modeText != null) {
defaultText = literalMD modeText;
};
mode =
mkOption {
description = ''
Mode of the secret file.
'';
type = str;
default = mode;
}
// optionalAttrs (modeText != null) {
defaultText = literalMD modeText;
};
owner = mkOption ({
description = ''
Linux user owning the secret file.
'';
type = str;
default = owner;
} // optionalAttrs (ownerText != null) {
defaultText = literalMD ownerText;
});
owner = mkOption (
{
description = ''
Linux user owning the secret file.
'';
type = str;
default = owner;
}
// optionalAttrs (ownerText != null) {
defaultText = literalMD ownerText;
}
);
group = mkOption {
description = ''
Linux group owning the secret file.
'';
type = str;
default = group;
} // optionalAttrs (groupText != null) {
defaultText = literalMD groupText;
};
group =
mkOption {
description = ''
Linux group owning the secret file.
'';
type = str;
default = group;
}
// optionalAttrs (groupText != null) {
defaultText = literalMD groupText;
};
restartUnits = mkOption ({
description = ''
Systemd units to restart after the secret is updated.
'';
type = listOf str;
default = restartUnits;
} // optionalAttrs (restartUnitsText != null) {
defaultText = literalMD restartUnitsText;
});
restartUnits = mkOption (
{
description = ''
Systemd units to restart after the secret is updated.
'';
type = listOf str;
default = restartUnits;
}
// optionalAttrs (restartUnitsText != null) {
defaultText = literalMD restartUnitsText;
}
);
};
};
};
@ -90,34 +121,39 @@ in
path ? "/run/secrets/secret",
pathText ? null,
}:
mkOption ({
description = ''
Result part of the secret contract.
mkOption (
{
description = ''
Result part of the secret contract.
Options set by the provider module that indicates where the secret can be found.
'';
default = {
inherit path;
};
type = submodule {
options = {
path = mkOption {
type = lib.types.path;
description = ''
Path to the file containing the secret generated out of band.
Options set by the provider module that indicates where the secret can be found.
'';
default = {
inherit path;
};
type = submodule {
options = {
path =
mkOption {
type = lib.types.path;
description = ''
Path to the file containing the secret generated out of band.
This path will exist after deploying to a target host,
it is not available through the nix store.
'';
default = path;
} // optionalAttrs (pathText != null) {
defaultText = pathText;
This path will exist after deploying to a target host,
it is not available through the nix store.
'';
default = path;
}
// optionalAttrs (pathText != null) {
defaultText = pathText;
};
};
};
};
} // optionalAttrs (pathText != null) {
defaultText = {
path = pathText;
};
});
}
// optionalAttrs (pathText != null) {
defaultText = {
path = pathText;
};
}
);
}

View file

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

View file

@ -3,24 +3,33 @@ let
inherit (lib) getAttrFromPath setAttrByPath;
inherit (lib) mkIf;
in
{ name,
configRoot,
settingsCfg, # str -> attrset
modules ? [],
owner ? "root",
group ? "root",
mode ? "0400",
restartUnits ? [ "myunit.service" ],
}: lib.shb.runNixOSTest {
name = "secret_${name}_${owner}_${group}_${mode}";
{
name,
configRoot,
settingsCfg, # str -> attrset
modules ? [ ],
owner ? "root",
group ? "root",
mode ? "0400",
restartUnits ? [ "myunit.service" ],
}:
lib.shb.runNixOSTest {
name = "secret_${name}_${owner}_${group}_${mode}";
nodes.machine = { config, ... }: {
nodes.machine =
{ config, ... }:
{
imports = [ lib.shb.baseImports ] ++ modules;
config = lib.mkMerge [
(setAttrByPath configRoot {
A = {
request = {
inherit owner group mode restartUnits;
inherit
owner
group
mode
restartUnits
;
};
settings = settingsCfg "secretA";
};
@ -29,35 +38,36 @@ in
users.users.${owner}.isNormalUser = true;
})
(mkIf (group != "root") {
users.groups.${group} = {};
users.groups.${group} = { };
})
];
};
testScript = { nodes, ... }:
let
result = (getAttrFromPath configRoot nodes.machine)."A".result;
in
''
owner = machine.succeed("stat -c '%U' ${result.path}").strip()
print(f"Got owner {owner}")
if owner != "${owner}":
raise Exception(f"Owner should be '${owner}' but got '{owner}'")
testScript =
{ nodes, ... }:
let
result = (getAttrFromPath configRoot nodes.machine)."A".result;
in
''
owner = machine.succeed("stat -c '%U' ${result.path}").strip()
print(f"Got owner {owner}")
if owner != "${owner}":
raise Exception(f"Owner should be '${owner}' but got '{owner}'")
group = machine.succeed("stat -c '%G' ${result.path}").strip()
print(f"Got group {group}")
if group != "${group}":
raise Exception(f"Group should be '${group}' but got '{group}'")
group = machine.succeed("stat -c '%G' ${result.path}").strip()
print(f"Got group {group}")
if group != "${group}":
raise Exception(f"Group should be '${group}' but got '{group}'")
mode = str(int(machine.succeed("stat -c '%a' ${result.path}").strip()))
print(f"Got mode {mode}")
wantedMode = str(int("${mode}"))
if mode != wantedMode:
raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'")
mode = str(int(machine.succeed("stat -c '%a' ${result.path}").strip()))
print(f"Got mode {mode}")
wantedMode = str(int("${mode}"))
if mode != wantedMode:
raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'")
content = machine.succeed("cat ${result.path}").strip()
print(f"Got content {content}")
if content != "secretA":
raise Exception(f"Content should be 'secretA' but got '{content}'")
'';
content = machine.succeed("cat ${result.path}").strip()
print(f"Got content {content}")
if content != "secretA":
raise Exception(f"Content should be 'secretA' but got '{content}'")
'';
}

View file

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

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.arr;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
apps = {
radarr = {
@ -11,7 +16,7 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for radarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.radarr.settingsFormat.type;
options = {
@ -20,7 +25,10 @@ let
description = "Path to api key secret file.";
};
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -69,7 +77,7 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for sonarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.sonarr.settingsFormat.type;
options = {
@ -78,7 +86,10 @@ let
description = "Path to api key secret file.";
};
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -122,12 +133,15 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for bazarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.bazarr.settingsFormat.type;
options = {
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -147,12 +161,15 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for readarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.readarr.settingsFormat.type;
options = {
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -171,12 +188,15 @@ let
moreOptions = {
settings = lib.mkOption {
description = "Specific options for lidarr.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.lidarr.settingsFormat.type;
options = {
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
type = lib.types.enum [
"debug"
"info"
];
description = "Log level.";
default = "info";
};
@ -191,11 +211,11 @@ let
};
};
jackett = {
settingsFormat = pkgs.formats.json {};
settingsFormat = pkgs.formats.json { };
moreOptions = {
settings = lib.mkOption {
description = "Specific options for jackett.";
default = {};
default = { };
type = lib.types.submodule {
freeformType = apps.jackett.settingsFormat.type;
options = {
@ -214,13 +234,18 @@ let
default = null;
};
ProxyType = lib.mkOption {
type = lib.types.enum [ "-1" "0" "1" "2" ];
type = lib.types.enum [
"-1"
"0"
"1"
"2"
];
default = "-1";
description = ''
-1 = disabled
0 = HTTP
1 = SOCKS4
2 = SOCKS5
-1 = disabled
0 = HTTP
1 = SOCKS4
2 = SOCKS5
'';
};
ProxyUrl = lib.mkOption {
@ -256,83 +281,101 @@ let
};
};
vhosts = { extraBypassResources ? [] }: c: {
inherit (c) subdomain domain authEndpoint ssl;
vhosts =
{
extraBypassResources ? [ ],
}:
c: {
inherit (c)
subdomain
domain
authEndpoint
ssl
;
upstream = "http://127.0.0.1:${toString c.settings.Port}";
autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [
{
domain = "${c.subdomain}.${c.domain}";
policy = "bypass";
resources = extraBypassResources ++ [
"^/api.*"
"^/feed.*"
];
}
{
domain = "${c.subdomain}.${c.domain}";
policy = "two_factor";
subject = ["group:arr_user"];
}
];
};
appOption = name: c: lib.nameValuePair name (lib.mkOption {
description = "Configuration for ${name}";
default = {};
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption name;
subdomain = lib.mkOption {
type = lib.types.str;
description = "Subdomain under which ${name} will be served.";
example = name;
};
domain = lib.mkOption {
type = lib.types.str;
description = "Domain under which ${name} will be served.";
example = "example.com";
};
dataDir = lib.mkOption {
type = lib.types.str;
description = "Directory where ${name} stores data.";
default = "/var/lib/${name}";
};
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
authEndpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Endpoint to the SSO provider. Leave null to not have SSO configured.";
example = "https://authelia.example.com";
};
backup = lib.mkOption {
description = ''
Backup configuration.
'';
default = {};
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = name;
sourceDirectories = [
cfg.${name}.dataDir
];
excludePatterns = [".db-shm" ".db-wal" ".mono"];
};
};
};
} // (c.moreOptions or {});
upstream = "http://127.0.0.1:${toString c.settings.Port}";
autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [
{
domain = "${c.subdomain}.${c.domain}";
policy = "bypass";
resources = extraBypassResources ++ [
"^/api.*"
"^/feed.*"
];
}
{
domain = "${c.subdomain}.${c.domain}";
policy = "two_factor";
subject = [ "group:arr_user" ];
}
];
};
});
appOption =
name: c:
lib.nameValuePair name (
lib.mkOption {
description = "Configuration for ${name}";
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption name;
subdomain = lib.mkOption {
type = lib.types.str;
description = "Subdomain under which ${name} will be served.";
example = name;
};
domain = lib.mkOption {
type = lib.types.str;
description = "Domain under which ${name} will be served.";
example = "example.com";
};
dataDir = lib.mkOption {
type = lib.types.str;
description = "Directory where ${name} stores data.";
default = "/var/lib/${name}";
};
ssl = lib.mkOption {
description = "Path to SSL files";
type = lib.types.nullOr contracts.ssl.certs;
default = null;
};
authEndpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Endpoint to the SSO provider. Leave null to not have SSO configured.";
example = "https://authelia.example.com";
};
backup = lib.mkOption {
description = ''
Backup configuration.
'';
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = name;
sourceDirectories = [
cfg.${name}.dataDir
];
excludePatterns = [
".db-shm"
".db-wal"
".mono"
];
};
};
};
}
// (c.moreOptions or { });
};
}
);
in
{
imports = [
@ -343,155 +386,167 @@ in
config = lib.mkMerge [
(lib.mkIf cfg.radarr.enable (
let
cfg' = cfg.radarr;
isSSOEnabled = !(isNull cfg'.authEndpoint);
in
{
services.nginx.enable = true;
let
cfg' = cfg.radarr;
isSSOEnabled = !(isNull cfg'.authEndpoint);
in
{
services.nginx.enable = true;
services.radarr = {
enable = true;
dataDir = "/var/lib/radarr";
};
services.radarr = {
enable = true;
dataDir = "/var/lib/radarr";
};
systemd.services.radarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
});
resultPath = "${config.services.radarr.dataDir}/config.xml";
generator = lib.shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat;
};
systemd.services.radarr.preStart = lib.shb.replaceSecrets {
userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
});
resultPath = "${config.services.radarr.dataDir}/config.xml";
generator = lib.shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.sonarr.enable (
let
cfg' = cfg.sonarr;
isSSOEnabled = !(isNull cfg'.authEndpoint);
in
{
services.nginx.enable = true;
let
cfg' = cfg.sonarr;
isSSOEnabled = !(isNull cfg'.authEndpoint);
in
{
services.nginx.enable = true;
services.sonarr = {
enable = true;
dataDir = "/var/lib/sonarr";
};
users.users.sonarr = {
extraGroups = [ "media" ];
};
services.sonarr = {
enable = true;
dataDir = "/var/lib/sonarr";
};
users.users.sonarr = {
extraGroups = [ "media" ];
};
systemd.services.sonarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
});
resultPath = "${config.services.sonarr.dataDir}/config.xml";
generator = apps.sonarr.settingsFormat.generate;
};
systemd.services.sonarr.preStart = lib.shb.replaceSecrets {
userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
});
resultPath = "${config.services.sonarr.dataDir}/config.xml";
generator = apps.sonarr.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.bazarr.enable (
let
cfg' = cfg.bazarr;
isSSOEnabled = !(isNull cfg'.authEndpoint);
in
{
services.bazarr = {
enable = true;
listenPort = cfg'.settings.Port;
};
users.users.bazarr = {
extraGroups = [ "media" ];
};
systemd.services.bazarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
});
resultPath = "/var/lib/bazarr/config.xml";
generator = apps.bazarr.settingsFormat.generate;
};
let
cfg' = cfg.bazarr;
isSSOEnabled = !(isNull cfg'.authEndpoint);
in
{
services.bazarr = {
enable = true;
listenPort = cfg'.settings.Port;
};
users.users.bazarr = {
extraGroups = [ "media" ];
};
systemd.services.bazarr.preStart = lib.shb.replaceSecrets {
userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
});
resultPath = "/var/lib/bazarr/config.xml";
generator = apps.bazarr.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.readarr.enable (
let
cfg' = cfg.readarr;
in
{
services.readarr = {
enable = true;
dataDir = "/var/lib/readarr";
};
users.users.readarr = {
extraGroups = [ "media" ];
};
systemd.services.readarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings;
resultPath = "${config.services.readarr.dataDir}/config.xml";
generator = apps.readarr.settingsFormat.generate;
};
let
cfg' = cfg.readarr;
in
{
services.readarr = {
enable = true;
dataDir = "/var/lib/readarr";
};
users.users.readarr = {
extraGroups = [ "media" ];
};
systemd.services.readarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings;
resultPath = "${config.services.readarr.dataDir}/config.xml";
generator = apps.readarr.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.lidarr.enable (
let
cfg' = cfg.lidarr;
isSSOEnabled = !(isNull cfg'.authEndpoint);
in
{
services.lidarr = {
enable = true;
dataDir = "/var/lib/lidarr";
};
users.users.lidarr = {
extraGroups = [ "media" ];
};
systemd.services.lidarr.preStart = lib.shb.replaceSecrets {
userConfig = cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
});
resultPath = "${config.services.lidarr.dataDir}/config.xml";
generator = apps.lidarr.settingsFormat.generate;
};
let
cfg' = cfg.lidarr;
isSSOEnabled = !(isNull cfg'.authEndpoint);
in
{
services.lidarr = {
enable = true;
dataDir = "/var/lib/lidarr";
};
users.users.lidarr = {
extraGroups = [ "media" ];
};
systemd.services.lidarr.preStart = lib.shb.replaceSecrets {
userConfig =
cfg'.settings
// (lib.optionalAttrs isSSOEnabled {
AuthenticationRequired = "DisabledForLocalAddresses";
AuthenticationMethod = "External";
});
resultPath = "${config.services.lidarr.dataDir}/config.xml";
generator = apps.lidarr.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {} cfg') ];
}))
shb.nginx.vhosts = [ (vhosts { } cfg') ];
}
))
(lib.mkIf cfg.jackett.enable (
let
cfg' = cfg.jackett;
in
{
services.jackett = {
enable = true;
dataDir = "/var/lib/jackett";
};
# TODO: avoid implicitly relying on the media group
users.users.jackett = {
extraGroups = [ "media" ];
};
systemd.services.jackett.preStart = lib.shb.replaceSecrets {
userConfig = lib.shb.renameAttrName cfg'.settings "ApiKey" "APIKey";
resultPath = "${config.services.jackett.dataDir}/ServerConfig.json";
generator = apps.jackett.settingsFormat.generate;
};
let
cfg' = cfg.jackett;
in
{
services.jackett = {
enable = true;
dataDir = "/var/lib/jackett";
};
# TODO: avoid implicitly relying on the media group
users.users.jackett = {
extraGroups = [ "media" ];
};
systemd.services.jackett.preStart = lib.shb.replaceSecrets {
userConfig = lib.shb.renameAttrName cfg'.settings "ApiKey" "APIKey";
resultPath = "${config.services.jackett.dataDir}/ServerConfig.json";
generator = apps.jackett.settingsFormat.generate;
};
shb.nginx.vhosts = [ (vhosts {
extraBypassResources = [ "^/dl.*" ];
} cfg') ];
}))
shb.nginx.vhosts = [
(vhosts {
extraBypassResources = [ "^/dl.*" ];
} cfg')
];
}
))
];
}

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.audiobookshelf;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -40,18 +45,18 @@ in
extraServiceConfig = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra configuration given to the systemd service file.";
default = {};
default = { };
example = lib.literalExpression ''
{
MemoryHigh = "512M";
MemoryMax = "900M";
}
{
MemoryHigh = "512M";
MemoryMax = "900M";
}
'';
};
sso = lib.mkOption {
description = "SSO configuration.";
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO";
@ -87,7 +92,10 @@ in
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -133,84 +141,102 @@ in
};
logLevel = lib.mkOption {
type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
type = lib.types.nullOr (
lib.types.enum [
"critical"
"error"
"warning"
"info"
"debug"
]
);
description = "Enable logging.";
default = false;
example = true;
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [{
services.audiobookshelf = {
enable = true;
openFirewall = true;
dataDir = "audiobookshelf";
host = "127.0.0.1";
port = cfg.webPort;
};
services.nginx.enable = true;
services.nginx.virtualHosts."${fqdn}" = {
http2 = true;
forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
# https://github.com/advplyr/audiobookshelf#nginx-reverse-proxy
extraConfig = ''
set $audiobookshelf 127.0.0.1;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://$audiobookshelf:${builtins.toString cfg.webPort};
proxy_redirect http:// https://;
}
'';
};
shb.authelia.extraDefinitions = {
user_attributes.${roleClaim}.expression =
''"${cfg.sso.adminUserGroup}" in groups ? ["admin"] : ("${cfg.sso.userGroup}" in groups ? ["user"] : [""])'';
};
shb.authelia.extraOidcClaimsPolicies.${roleClaim} = {
custom_claims = {
"${roleClaim}" = {};
};
};
shb.authelia.extraOidcScopes."${roleClaim}" = {
claims = [ "${roleClaim}" ];
};
shb.authelia.oidcClients = lib.lists.optionals cfg.sso.enable [
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
client_id = cfg.sso.clientID;
client_name = "Audiobookshelf";
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
claims_policy = "${roleClaim}";
public = false;
authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback"
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect"
services.audiobookshelf = {
enable = true;
openFirewall = true;
dataDir = "audiobookshelf";
host = "127.0.0.1";
port = cfg.webPort;
};
services.nginx.enable = true;
services.nginx.virtualHosts."${fqdn}" = {
http2 = true;
forceSSL = !(isNull cfg.ssl);
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
# https://github.com/advplyr/audiobookshelf#nginx-reverse-proxy
extraConfig = ''
set $audiobookshelf 127.0.0.1;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://$audiobookshelf:${builtins.toString cfg.webPort};
proxy_redirect http:// https://;
}
'';
};
shb.authelia.extraDefinitions = {
user_attributes.${roleClaim}.expression =
''"${cfg.sso.adminUserGroup}" in groups ? ["admin"] : ("${cfg.sso.userGroup}" in groups ? ["user"] : [""])'';
};
shb.authelia.extraOidcClaimsPolicies.${roleClaim} = {
custom_claims = {
"${roleClaim}" = { };
};
};
shb.authelia.extraOidcScopes."${roleClaim}" = {
claims = [ "${roleClaim}" ];
};
shb.authelia.oidcClients = lib.lists.optionals cfg.sso.enable [
{
client_id = cfg.sso.clientID;
client_name = "Audiobookshelf";
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
claims_policy = "${roleClaim}";
public = false;
authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback"
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect"
];
scopes = [
"openid"
"profile"
"email"
"groups"
"${roleClaim}"
];
require_pkce = true;
pkce_challenge_method = "S256";
userinfo_signed_response_alg = "none";
token_endpoint_auth_method = "client_secret_basic";
}
];
scopes = [ "openid" "profile" "email" "groups" "${roleClaim}" ];
require_pkce = true;
pkce_challenge_method = "S256";
userinfo_signed_response_alg = "none";
token_endpoint_auth_method = "client_secret_basic";
}
];
} {
systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig;
}]);
{
systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig;
}
]
);
}

View file

@ -1,20 +1,31 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.deluge;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
authGenerator = users:
authGenerator =
users:
let
genLine = name: { password, priority ? 10 }:
genLine =
name:
{
password,
priority ? 10,
}:
"${name}:${password}:${toString priority}";
lines = lib.mapAttrsToList genLine users;
in
lib.concatStringsSep "\n" lines;
lib.concatStringsSep "\n" lines;
in
{
imports = [
@ -57,7 +68,10 @@ in
daemonListenPorts = lib.mkOption {
type = lib.types.listOf lib.types.int;
description = "Deluge daemon listen ports";
default = [ 6881 6889 ];
default = [
6881
6889
];
};
webPort = lib.mkOption {
@ -158,12 +172,12 @@ in
extraServiceConfig = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra configuration given to the systemd service file.";
default = {};
default = { };
example = lib.literalExpression ''
{
MemoryHigh = "512M";
MemoryMax = "900M";
}
{
MemoryHigh = "512M";
MemoryMax = "900M";
}
'';
};
@ -176,14 +190,16 @@ in
extraUsers = lib.mkOption {
description = "Users having access to this deluge instance. Attrset of username to user options.";
type = lib.types.attrsOf (lib.types.submodule {
options = {
password = lib.mkOption {
type = lib.shb.secretFileType;
description = "File containing the user password.";
type = lib.types.attrsOf (
lib.types.submodule {
options = {
password = lib.mkOption {
type = lib.shb.secretFileType;
description = "File containing the user password.";
};
};
};
});
}
);
};
localclientPassword = lib.mkOption {
@ -198,12 +214,17 @@ in
prometheusScraperPassword = lib.mkOption {
description = "Password for prometheus scraper. Setting this option will activate the prometheus deluge exporter.";
type = lib.types.nullOr (lib.types.submodule {
options = contracts.secret.mkRequester {
owner = "deluge";
restartUnits = [ "deluged.service" "prometheus.service" ];
};
});
type = lib.types.nullOr (
lib.types.submodule {
options = contracts.secret.mkRequester {
owner = "deluge";
restartUnits = [
"deluged.service"
"prometheus.service"
];
};
}
);
default = null;
};
@ -214,35 +235,35 @@ in
Label is automatically enabled if any of the `shb.arr.*` service is enabled.
'';
example = ["Label"];
default = [];
example = [ "Label" ];
default = [ ];
};
additionalPlugins = lib.mkOption {
type = lib.types.listOf lib.types.path;
description = "Location of additional plugins. Each item in the list must be the path to the directory containing the plugin .egg file.";
default = [];
default = [ ];
example = lib.literalExpression ''
additionalPlugins = [
(pkgs.callPackage ({ python3, fetchFromGitHub }: python3.pkgs.buildPythonPackage {
name = "deluge-autotracker";
version = "1.0.0";
src = fetchFromGitHub {
owner = "ibizaman";
repo = "deluge-autotracker";
rev = "cc40d816a497bbf1c2ebeb3d8b1176210548a3e6";
sha256 = "sha256-0LpVdv1fak2a5eX4unjhUcN7nMAl9fgpr3X+7XnQE6c=";
} + "/autotracker";
doCheck = false;
format = "other";
nativeBuildInputs = [ python3.pkgs.setuptools ];
buildPhase = '''
mkdir "$out"
python3 setup.py install --install-lib "$out"
''';
doInstallPhase = false;
}) {})
];
additionalPlugins = [
(pkgs.callPackage ({ python3, fetchFromGitHub }: python3.pkgs.buildPythonPackage {
name = "deluge-autotracker";
version = "1.0.0";
src = fetchFromGitHub {
owner = "ibizaman";
repo = "deluge-autotracker";
rev = "cc40d816a497bbf1c2ebeb3d8b1176210548a3e6";
sha256 = "sha256-0LpVdv1fak2a5eX4unjhUcN7nMAl9fgpr3X+7XnQE6c=";
} + "/autotracker";
doCheck = false;
format = "other";
nativeBuildInputs = [ python3.pkgs.setuptools ];
buildPhase = '''
mkdir "$out"
python3 setup.py install --install-lib "$out"
''';
doInstallPhase = false;
}) {})
];
'';
};
@ -250,7 +271,7 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "deluge";
@ -262,146 +283,175 @@ in
};
logLevel = lib.mkOption {
type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
type = lib.types.nullOr (
lib.types.enum [
"critical"
"error"
"warning"
"info"
"debug"
]
);
description = "Enable logging.";
default = null;
example = "info";
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [{
services.deluge = {
enable = true;
declarative = true;
openFirewall = true;
inherit (cfg) dataDir;
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.deluge = {
enable = true;
declarative = true;
openFirewall = true;
inherit (cfg) dataDir;
config = {
download_location = cfg.settings.downloadLocation;
allow_remote = true;
daemon_port = cfg.daemonPort;
listen_ports = cfg.daemonListenPorts;
proxy = lib.optionalAttrs (cfg.proxyPort != null) {
force_proxy = true;
hostname = "127.0.0.1";
port = cfg.proxyPort;
proxy_hostnames = true;
proxy_peer_connections = true;
proxy_tracker_connections = true;
type = 4; # HTTP
config = {
download_location = cfg.settings.downloadLocation;
allow_remote = true;
daemon_port = cfg.daemonPort;
listen_ports = cfg.daemonListenPorts;
proxy = lib.optionalAttrs (cfg.proxyPort != null) {
force_proxy = true;
hostname = "127.0.0.1";
port = cfg.proxyPort;
proxy_hostnames = true;
proxy_peer_connections = true;
proxy_tracker_connections = true;
type = 4; # HTTP
};
outgoing_interface = cfg.outgoingInterface;
enabled_plugins =
cfg.enabledPlugins
++ lib.optional (lib.any (x: x.enable) [
config.services.radarr
config.services.sonarr
config.services.bazarr
config.services.readarr
config.services.lidarr
]) "Label";
inherit (cfg.settings)
max_active_limit
max_active_downloading
max_active_seeding
max_connections_global
max_connections_per_torrent
max_download_speed
max_download_speed_per_torrent
max_upload_slots_global
max_upload_slots_per_torrent
max_upload_speed
max_upload_speed_per_torrent
dont_count_slow_torrents
;
new_release_check = false;
};
authFile = "${cfg.dataDir}/.config/deluge/authTemplate";
web.enable = true;
web.port = cfg.webPort;
};
outgoing_interface = cfg.outgoingInterface;
enabled_plugins = cfg.enabledPlugins
++ lib.optional (lib.any (x: x.enable) [
config.services.radarr
config.services.sonarr
config.services.bazarr
config.services.readarr
config.services.lidarr
]) "Label";
systemd.services.deluged.preStart = lib.mkBefore (
lib.shb.replaceSecrets {
userConfig =
cfg.extraUsers
// {
localclient.password.source = config.shb.deluge.localclientPassword.result.path;
}
// (lib.optionalAttrs (config.shb.deluge.prometheusScraperPassword != null) {
prometheus_scraper.password.source = config.shb.deluge.prometheusScraperPassword.result.path;
});
resultPath = "${cfg.dataDir}/.config/deluge/authTemplate";
generator = name: value: pkgs.writeText "delugeAuth" (authGenerator value);
}
);
inherit (cfg.settings)
max_active_limit
max_active_downloading
max_active_seeding
max_connections_global
max_connections_per_torrent
systemd.services.deluged.serviceConfig.ExecStart = lib.mkForce (
lib.concatStringsSep " \\\n " (
[
"${config.services.deluge.package}/bin/deluged"
"--do-not-daemonize"
"--config ${cfg.dataDir}/.config/deluge"
]
++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}")
)
);
max_download_speed
max_download_speed_per_torrent
systemd.tmpfiles.rules =
let
plugins = pkgs.symlinkJoin {
name = "deluge-plugins";
paths = cfg.additionalPlugins;
};
in
[
"L+ ${cfg.dataDir}/.config/deluge/plugins - - - - ${plugins}"
];
max_upload_slots_global
max_upload_slots_per_torrent
max_upload_speed
max_upload_speed_per_torrent
dont_count_slow_torrents;
new_release_check = false;
};
authFile = "${cfg.dataDir}/.config/deluge/authTemplate";
web.enable = true;
web.port = cfg.webPort;
};
systemd.services.deluged.preStart = lib.mkBefore (lib.shb.replaceSecrets {
userConfig = cfg.extraUsers // {
localclient.password.source = config.shb.deluge.localclientPassword.result.path;
} // (lib.optionalAttrs (config.shb.deluge.prometheusScraperPassword != null) {
prometheus_scraper.password.source = config.shb.deluge.prometheusScraperPassword.result.path;
});
resultPath = "${cfg.dataDir}/.config/deluge/authTemplate";
generator = name: value: pkgs.writeText "delugeAuth" (authGenerator value);
});
systemd.services.deluged.serviceConfig.ExecStart = lib.mkForce (lib.concatStringsSep " \\\n " ([
"${config.services.deluge.package}/bin/deluged"
"--do-not-daemonize"
"--config ${cfg.dataDir}/.config/deluge"
] ++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}")
));
systemd.tmpfiles.rules =
let
plugins = pkgs.symlinkJoin {
name = "deluge-plugins";
paths = cfg.additionalPlugins;
};
in
[
"L+ ${cfg.dataDir}/.config/deluge/plugins - - - - ${plugins}"
shb.nginx.vhosts = [
(
{
inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
{
domain = fqdn;
policy = "bypass";
resources = [
"^/json"
];
}
{
domain = fqdn;
policy = "two_factor";
subject = [ "group:deluge_user" ];
}
];
}
// (lib.optionalAttrs (cfg.authEndpoint != null) {
inherit (cfg) authEndpoint;
})
)
];
}
{
systemd.services.deluged.serviceConfig = cfg.extraServiceConfig;
}
(lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) {
services.prometheus.exporters.deluge = {
enable = true;
shb.nginx.vhosts = [
({
inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
delugeHost = "127.0.0.1";
delugePort = config.services.deluge.config.daemon_port;
delugeUser = "prometheus_scraper";
delugePasswordFile = config.shb.deluge.prometheusScraperPassword.result.path;
exportPerTorrentMetrics = true;
};
services.prometheus.scrapeConfigs = [
{
domain = fqdn;
policy = "bypass";
resources = [
"^/json"
job_name = "deluge";
static_configs = [
{
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.deluge.port}" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
}
];
}
{
domain = fqdn;
policy = "two_factor";
subject = ["group:deluge_user"];
}
];
} // (lib.optionalAttrs (cfg.authEndpoint != null) {
inherit (cfg) authEndpoint;
}))
];
} {
systemd.services.deluged.serviceConfig = cfg.extraServiceConfig;
} (lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) {
services.prometheus.exporters.deluge = {
enable = true;
delugeHost = "127.0.0.1";
delugePort = config.services.deluge.config.daemon_port;
delugeUser = "prometheus_scraper";
delugePasswordFile = config.shb.deluge.prometheusScraperPassword.result.path;
exportPerTorrentMetrics = true;
};
services.prometheus.scrapeConfigs = [
{
job_name = "deluge";
static_configs = [{
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.deluge.port}"];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
}];
}
];
})
]);
})
]
);
}

View file

@ -1,25 +1,60 @@
{ config, options, pkgs, lib, ... }:
{
config,
options,
pkgs,
lib,
...
}:
let
cfg = config.shb.forgejo;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
inherit (lib) all attrNames concatMapStringsSep getExe lists literalExpression mapAttrsToList mkBefore mkEnableOption mkForce mkIf mkMerge mkOption mkOverride nameValuePair optionalString optionals;
inherit (lib.types) attrsOf bool enum listOf nullOr package port submodule str;
inherit (lib)
all
attrNames
concatMapStringsSep
getExe
lists
literalExpression
mapAttrsToList
mkBefore
mkEnableOption
mkForce
mkIf
mkMerge
mkOption
mkOverride
nameValuePair
optionalString
optionals
;
inherit (lib.types)
attrsOf
bool
enum
listOf
nullOr
package
port
submodule
str
;
in
{
imports = [
../blocks/nginx.nix
(lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''Instead, define an admin user in shb.forgejo.users and give it the same password, like so:
shb.forgejo.users = {
"forgejoadmin" = {
isAdmin = true;
email = "forgejoadmin@example.com";
password.result = <path/to/password>;
};
};
(lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''
Instead, define an admin user in shb.forgejo.users and give it the same password, like so:
shb.forgejo.users = {
"forgejoadmin" = {
isAdmin = true;
email = "forgejoadmin@example.com";
password.result = <path/to/password>;
};
};
'')
];
@ -60,7 +95,7 @@ in
description = ''
LDAP Integration.
'';
default = {};
default = { };
type = nullOr (submodule {
options = {
enable = mkEnableOption "LDAP integration.";
@ -125,7 +160,7 @@ in
waitForSystemdServices = mkOption {
type = listOf str;
default = [];
default = [ ];
description = ''
List of systemd services to wait on before starting.
This is needed because forgejo will try a lookup on the LDAP instance
@ -140,7 +175,7 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = submodule {
options = {
enable = mkEnableOption "SSO integration.";
@ -164,7 +199,10 @@ in
};
authorization_policy = mkOption {
type = enum [ "one_factor" "two_factor" ];
type = enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -205,9 +243,10 @@ in
};
email = mkOption {
description = ''Email of user.
description = ''
Email of user.
This is only set when the user is created, changing this later on will have no effect.
This is only set when the user is created, changing this later on will have no effect.
'';
type = str;
};
@ -254,7 +293,6 @@ in
'';
};
hostPackages = mkOption {
type = listOf package;
default = with pkgs; [
@ -289,13 +327,14 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = options.services.forgejo.user.value;
sourceDirectories = [
options.services.forgejo.dump.backupDir.value
] ++ optionals (cfg.repositoryRoot != null) [
]
++ optionals (cfg.repositoryRoot != null) [
cfg.repositoryRoot
];
};
@ -317,7 +356,9 @@ in
```
'';
readOnly = true;
default = { path = config.services.forgejo.stateDir; };
default = {
path = config.services.forgejo.stateDir;
};
};
smtp = mkOption {
@ -390,10 +431,12 @@ in
# https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113
systemd.services.forgejo.serviceConfig.Type = mkForce "exec";
shb.nginx.vhosts = [{
inherit (cfg) domain subdomain ssl;
upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}";
}];
shb.nginx.vhosts = [
{
inherit (cfg) domain subdomain ssl;
upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}";
}
];
})
(mkIf cfg.enable {
@ -420,58 +463,60 @@ in
systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices;
systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices;
# The delimiter in the `cut` command is a TAB!
systemd.services.forgejo.preStart = let
provider = "SHB-${cfg.ldap.provider}";
in ''
auth="${getExe config.services.forgejo.package} admin auth"
systemd.services.forgejo.preStart =
let
provider = "SHB-${cfg.ldap.provider}";
in
''
auth="${getExe config.services.forgejo.package} admin auth"
echo "Trying to find existing ldap configuration for ${provider}"...
set +e -o pipefail
id="$($auth list | grep "${provider}.*LDAP" | cut -d' ' -f1)"
found=$?
set -e +o pipefail
echo "Trying to find existing ldap configuration for ${provider}"...
set +e -o pipefail
id="$($auth list | grep "${provider}.*LDAP" | cut -d' ' -f1)"
found=$?
set -e +o pipefail
if [[ $found = 0 ]]; then
echo Found ldap configuration at id=$id, updating it if needed.
$auth update-ldap \
--id $id \
--name ${provider} \
--host ${cfg.ldap.host} \
--port ${toString cfg.ldap.port} \
--bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
--security-protocol Unencrypted \
--user-search-base ou=people,${cfg.ldap.dcdomain} \
--user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
--username-attribute uid \
--firstname-attribute givenName \
--surname-attribute sn \
--email-attribute mail \
--avatar-attribute jpegPhoto \
--synchronize-users
echo "Done updating LDAP configuration."
else
echo Did not find any ldap configuration, creating one with name ${provider}.
$auth add-ldap \
--name ${provider} \
--host ${cfg.ldap.host} \
--port ${toString cfg.ldap.port} \
--bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
--security-protocol Unencrypted \
--user-search-base ou=people,${cfg.ldap.dcdomain} \
--user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
--username-attribute uid \
--firstname-attribute givenName \
--surname-attribute sn \
--email-attribute mail \
--avatar-attribute jpegPhoto \
--synchronize-users
echo "Done adding LDAP configuration."
fi
'';
if [[ $found = 0 ]]; then
echo Found ldap configuration at id=$id, updating it if needed.
$auth update-ldap \
--id $id \
--name ${provider} \
--host ${cfg.ldap.host} \
--port ${toString cfg.ldap.port} \
--bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
--security-protocol Unencrypted \
--user-search-base ou=people,${cfg.ldap.dcdomain} \
--user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
--username-attribute uid \
--firstname-attribute givenName \
--surname-attribute sn \
--email-attribute mail \
--avatar-attribute jpegPhoto \
--synchronize-users
echo "Done updating LDAP configuration."
else
echo Did not find any ldap configuration, creating one with name ${provider}.
$auth add-ldap \
--name ${provider} \
--host ${cfg.ldap.host} \
--port ${toString cfg.ldap.port} \
--bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
--security-protocol Unencrypted \
--user-search-base ou=people,${cfg.ldap.dcdomain} \
--user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
--username-attribute uid \
--firstname-attribute givenName \
--surname-attribute sn \
--email-attribute mail \
--avatar-attribute jpegPhoto \
--synchronize-users
echo "Done adding LDAP configuration."
fi
'';
})
# For Authelia to Forgejo integration: https://www.authelia.com/integration/openid-connect/gitea/
@ -488,7 +533,7 @@ in
ENABLE_OPENID_SIGNUP = true;
WHITELISTED_URIS = cfg.sso.endpoint;
};
service = {
# DISABLE_REGISTRATION = mkForce false;
# ALLOW_ONLY_EXTERNAL_REGISTRATION = false;
@ -497,48 +542,53 @@ in
};
# The delimiter in the `cut` command is a TAB!
systemd.services.forgejo.preStart = let
provider = "SHB-${cfg.sso.provider}";
in ''
auth="${getExe config.services.forgejo.package} admin auth"
systemd.services.forgejo.preStart =
let
provider = "SHB-${cfg.sso.provider}";
in
''
auth="${getExe config.services.forgejo.package} admin auth"
echo "Trying to find existing sso configuration for ${provider}"...
set +e -o pipefail
id="$($auth list | grep "${provider}.*OAuth2" | cut -d' ' -f1)"
found=$?
set -e +o pipefail
echo "Trying to find existing sso configuration for ${provider}"...
set +e -o pipefail
id="$($auth list | grep "${provider}.*OAuth2" | cut -d' ' -f1)"
found=$?
set -e +o pipefail
if [[ $found = 0 ]]; then
echo Found sso configuration at id=$id, updating it if needed.
$auth update-oauth \
--id $id \
--name ${provider} \
--provider openidConnect \
--key forgejo \
--secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \
--auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration
else
echo Did not find any sso configuration, creating one with name ${provider}.
$auth add-oauth \
--name ${provider} \
--provider openidConnect \
--key forgejo \
--secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \
--auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration
fi
'';
if [[ $found = 0 ]]; then
echo Found sso configuration at id=$id, updating it if needed.
$auth update-oauth \
--id $id \
--name ${provider} \
--provider openidConnect \
--key forgejo \
--secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \
--auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration
else
echo Did not find any sso configuration, creating one with name ${provider}.
$auth add-oauth \
--name ${provider} \
--provider openidConnect \
--key forgejo \
--secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \
--auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration
fi
'';
shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [
(let
provider = "SHB-${cfg.sso.provider}";
in {
client_id = cfg.sso.clientID;
client_name = "Forgejo";
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
public = false;
authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ];
})
(
let
provider = "SHB-${cfg.sso.provider}";
in
{
client_id = cfg.sso.clientID;
client_name = "Forgejo";
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
public = false;
authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ];
}
)
];
})
@ -552,13 +602,14 @@ in
systemd.services.forgejo.preStart = ''
admin="${getExe config.services.forgejo.package} admin user"
'' + concatMapStringsSep "\n" (u: ''
''
+ concatMapStringsSep "\n" (u: ''
if ! $admin list | grep "${u.name}"; then
$admin create ${optionalString u.value.isAdmin "--admin"} --email "${u.value.email}" --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})"
else
$admin change-password --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})"
fi
'') (mapAttrsToList nameValuePair cfg.users);
'') (mapAttrsToList nameValuePair cfg.users);
})
(mkIf (cfg.enable && cfg.smtp != null) {
@ -584,15 +635,17 @@ in
instances.local = {
enable = true;
name = "local";
url = let
protocol = if cfg.ssl != null then "https" else "http";
in "${protocol}://${cfg.subdomain}.${cfg.domain}";
url =
let
protocol = if cfg.ssl != null then "https" else "http";
in
"${protocol}://${cfg.subdomain}.${cfg.domain}";
tokenFile = ""; # Empty variable to satisfy an assertion.
labels = [
# "ubuntu-latest:docker://node:16-bullseye"
# "ubuntu-22.04:docker://node:16-bullseye"
# "ubuntu-20.04:docker://node:16-bullseye"
# "ubuntu-18.04:docker://node:16-buster"
# "ubuntu-18.04:docker://node:16-buster"
"native:host"
];
inherit (cfg) hostPackages;

View file

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

View file

@ -1,9 +1,14 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.hledger;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
in
@ -63,7 +68,7 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "hledger";
@ -76,7 +81,7 @@ in
extraArguments = lib.mkOption {
description = "Extra arguments append to the hledger command.";
default = ["--forecast"];
default = [ "--forecast" ];
type = lib.types.listOf lib.types.str;
};
};
@ -88,7 +93,7 @@ in
baseUrl = "";
stateDir = cfg.dataDir;
journalFiles = ["hledger.journal"];
journalFiles = [ "hledger.journal" ];
host = "127.0.0.1";
port = cfg.port;
@ -101,20 +106,27 @@ in
# If the hledger.journal file does not exist, hledger-web refuses to start, so we create an
# empty one if it does not exist yet..
preStart = ''
test -f /var/lib/hledger/hledger.journal || touch /var/lib/hledger/hledger.journal
test -f /var/lib/hledger/hledger.journal || touch /var/lib/hledger/hledger.journal
'';
serviceConfig.StateDirectory = "hledger";
};
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain authEndpoint ssl;
inherit (cfg)
subdomain
domain
authEndpoint
ssl
;
upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}";
autheliaRules = [{
domain = fqdn;
policy = "two_factor";
subject = ["group:hledger_user"];
}];
autheliaRules = [
{
domain = fqdn;
policy = "two_factor";
subject = [ "group:hledger_user" ];
}
];
}
];
};

View file

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

View file

@ -1,32 +1,45 @@
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.shb.immich;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
protocol = if !(isNull cfg.ssl) then "https" else "http";
roleClaim = "immich_user";
# 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";
scopes = [ "openid" "email" "profile" "groups" "immich_scope"];
scopes = [
"openid"
"email"
"profile"
"groups"
"immich_scope"
];
dataFolder = cfg.mediaLocation;
ssoFqdnWithPort = if isNull cfg.sso.port
then cfg.sso.endpoint
else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
ssoFqdnWithPort =
if isNull cfg.sso.port then cfg.sso.endpoint else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
# Generate Immich configuration file only for SHB-managed settings
shbManagedSettings = lib.optionalAttrs (cfg.settings != {}) cfg.settings
shbManagedSettings =
lib.optionalAttrs (cfg.settings != { }) cfg.settings
// lib.optionalAttrs (cfg.sso.enable) {
oauth = {
enabled = true;
issuerUrl = "${ssoFqdnWithPort}";
clientId = cfg.sso.clientID;
roleClaim = roleClaim;
clientSecret = { source = cfg.sso.sharedSecret.result.path; };
clientSecret = {
source = cfg.sso.sharedSecret.result.path;
};
scope = builtins.concatStringsSep " " scopes;
storageLabelClaim = cfg.sso.storageLabelClaim;
#storageQuotaClaim = quotaClaim; # TODO (commented out, otherwise defaults to 0 bytes!)
@ -49,7 +62,9 @@ let
host = cfg.smtp.host;
port = cfg.smtp.port;
username = cfg.smtp.username;
password = { source = cfg.smtp.password.result.path; };
password = {
source = cfg.smtp.password.result.path;
};
ignoreTLS = cfg.smtp.ignoreTLS;
secure = cfg.smtp.secure;
};
@ -58,19 +73,36 @@ let
};
configFile = "/var/lib/immich/config.json";
# Use SHB's replaceSecrets function for loading secrets at runtime
configSetupScript = lib.optionalString (cfg.sso.enable || cfg.smtp != null) (
lib.shb.replaceSecrets {
userConfig = shbManagedSettings;
resultPath = configFile;
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json {});
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json { });
user = "immich";
permissions = "u=r,g=,o=";
}
);
inherit (lib) mkEnableOption mkIf lists mkOption optionals;
inherit (lib.types) attrs attrsOf bool enum listOf nullOr port submodule str path;
inherit (lib)
mkEnableOption
mkIf
lists
mkOption
optionals
;
inherit (lib.types)
attrs
attrsOf
bool
enum
listOf
nullOr
port
submodule
str
path
;
in
{
imports = [
@ -154,14 +186,16 @@ in
```
'';
readOnly = true;
default = { path = dataFolder; };
default = {
path = dataFolder;
};
};
backup = mkOption {
description = ''
Backup configuration for Immich media files and database.
'';
default = {};
default = { };
type = submodule {
options = contracts.backup.mkRequester {
user = "immich";
@ -190,7 +224,7 @@ in
machineLearning = mkOption {
description = "Machine learning configuration.";
default = {};
default = { };
type = submodule {
options = {
enable = mkOption {
@ -202,7 +236,7 @@ in
environment = mkOption {
description = "Extra environment variables for machine learning service.";
type = attrsOf str;
default = {};
default = { };
example = {
MACHINE_LEARNING_WORKERS = "2";
MACHINE_LEARNING_WORKER_TIMEOUT = "180";
@ -216,13 +250,17 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = submodule {
options = {
enable = mkEnableOption "SSO integration.";
provider = mkOption {
type = enum [ "Authelia" "Keycloak" "Generic" ];
type = enum [
"Authelia"
"Keycloak"
"Generic"
];
description = "OIDC provider name, used for display.";
default = "Authelia";
};
@ -311,7 +349,10 @@ in
};
authorization_policy = mkOption {
type = enum [ "one_factor" "two_factor" ];
type = enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -325,10 +366,10 @@ in
Immich configuration settings.
Only specify settings that you want SHB to manage declaratively.
Other settings can be configured through Immich's admin UI.
See https://immich.app/docs/install/config-file/ for available options.
'';
default = {};
default = { };
example = {
ffmpeg.crf = 23;
job.backgroundTask.concurrency = 5;
@ -351,31 +392,31 @@ in
description = "SMTP address from which the emails originate.";
example = "noreply@example.com";
};
replyTo = mkOption {
type = str;
description = "Reply-to address for emails.";
example = "support@example.com";
};
host = mkOption {
type = str;
description = "SMTP host to send the emails to.";
example = "smtp.example.com";
};
port = mkOption {
type = port;
description = "SMTP port to send the emails to.";
default = 587;
};
username = mkOption {
type = str;
description = "Username to connect to the SMTP host.";
example = "smtp-user";
};
password = mkOption {
description = "File containing the password to connect to the SMTP host.";
type = submodule {
@ -386,13 +427,13 @@ in
};
};
};
ignoreTLS = mkOption {
type = bool;
description = "Ignore TLS certificate errors.";
default = false;
};
secure = mkOption {
type = bool;
description = "Use secure connection (SSL/TLS).";
@ -428,7 +469,7 @@ in
host = "127.0.0.1";
port = cfg.port;
mediaLocation = cfg.mediaLocation;
# Hardware acceleration configuration
accelerationDevices = cfg.accelerationDevices;
@ -436,7 +477,7 @@ in
# Database configuration
database = {
# Disable pgvecto.rs, as it was deprecated before SHB integration
# Disable pgvecto.rs, as it was deprecated before SHB integration
enableVectors = false;
};
@ -452,9 +493,11 @@ in
REDIS_HOSTNAME = "127.0.0.1";
REDIS_PORT = "6379";
REDIS_DBINDEX = "0";
} // lib.optionalAttrs (cfg.jwtSecretFile != null) {
}
// lib.optionalAttrs (cfg.jwtSecretFile != null) {
JWT_SECRET_FILE = cfg.jwtSecretFile.result.path;
} // lib.optionalAttrs (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) {
}
// lib.optionalAttrs (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) {
IMMICH_CONFIG_FILE = configFile;
};
};
@ -464,27 +507,32 @@ in
"d /var/lib/immich 0700 immich immich"
];
# 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)) {
description = "Setup Immich configuration for SHB-managed settings";
wantedBy = [ "multi-user.target" ];
before = [ "immich-server.service" ];
after = [ "network.target" ];
serviceConfig = {
Type = "oneshot";
User = "immich";
Group = "immich";
};
script = ''
mkdir -p ${dataFolder}
# Generate config file with only SHB-managed settings
${configSetupScript}
'';
};
# 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))
{
description = "Setup Immich configuration for SHB-managed settings";
wantedBy = [ "multi-user.target" ];
before = [ "immich-server.service" ];
after = [ "network.target" ];
serviceConfig = {
Type = "oneshot";
User = "immich";
Group = "immich";
};
script = ''
mkdir -p ${dataFolder}
# Generate config file with only SHB-managed settings
${configSetupScript}
'';
};
# Add immich user to video and render groups for hardware acceleration
users.users.immich.extraGroups = optionals (cfg.accelerationDevices != []) [ "video" "render" ];
users.users.immich.extraGroups = optionals (cfg.accelerationDevices != [ ]) [
"video"
"render"
];
# PostgreSQL extensions are automatically handled by the Immich service
@ -499,7 +547,10 @@ in
{
domain = fqdn;
policy = cfg.sso.authorization_policy;
subject = ["group:immich_user" "group:immich_admin"];
subject = [
"group:immich_user"
"group:immich_admin"
];
}
];
authEndpoint = lib.mkIf (cfg.sso.enable) cfg.sso.endpoint;
@ -515,10 +566,20 @@ in
# Ensure services start in correct order
systemd.services.immich-server = {
after = [ "postgresql.service" "redis-immich.service" ]
++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ];
requires = [ "postgresql.service" "redis-immich.service" ]
++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ];
after = [
"postgresql.service"
"redis-immich.service"
]
++ optionals (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) [
"immich-setup-config.service"
];
requires = [
"postgresql.service"
"redis-immich.service"
]
++ optionals (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) [
"immich-setup-config.service"
];
};
systemd.services.immich-machine-learning = mkIf cfg.machineLearning.enable {
@ -527,21 +588,21 @@ in
# Authelia integration for SSO
shb.authelia.extraDefinitions = {
# 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
# Immich expects all users that get a token to be granted access. So users can either be part of the
# "admin" group or the "user" group. Users that are not part of either should be blocked by
# the ID provider (Authelia).
user_attributes.${roleClaim}.expression = ''"${cfg.sso.adminUserGroup}" in groups ? "admin" : "user"'';
user_attributes.${roleClaim}.expression =
''"${cfg.sso.adminUserGroup}" in groups ? "admin" : "user"'';
};
shb.authelia.extraOidcClaimsPolicies.immich_policy = {
custom_claims = {
${roleClaim} = {};
${roleClaim} = { };
};
};
shb.authelia.extraOidcScopes.immich_scope = {
claims = [ roleClaim ];
};
shb.authelia.oidcClients = lists.optionals (cfg.sso.enable && cfg.sso.provider == "Authelia") [
{
client_id = cfg.sso.clientID;

View file

@ -1,11 +1,16 @@
{ config, lib, pkgs, ...}:
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
cfg = config.shb.jellyfin;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -91,31 +96,33 @@ in
admin = lib.mkOption {
description = "Default admin user info. Only needed if LDAP or SSO is not configured.";
default = null;
type = types.nullOr (types.submodule {
options = {
username = lib.mkOption {
description = "Username of the default admin user.";
type = types.str;
default = "jellyfin";
};
password = lib.mkOption {
description = "Password of the default admin user.";
type = types.submodule {
options = contracts.secret.mkRequester {
mode = "0440";
owner = "jellyfin";
group = "jellyfin";
restartUnits = [ "jellyfin.service" ];
type = types.nullOr (
types.submodule {
options = {
username = lib.mkOption {
description = "Username of the default admin user.";
type = types.str;
default = "jellyfin";
};
password = lib.mkOption {
description = "Password of the default admin user.";
type = types.submodule {
options = contracts.secret.mkRequester {
mode = "0440";
owner = "jellyfin";
group = "jellyfin";
restartUnits = [ "jellyfin.service" ];
};
};
};
};
};
});
}
);
};
ldap = lib.mkOption {
description = "LDAP configuration.";
default = {};
default = { };
type = types.submodule {
options = {
enable = lib.mkEnableOption "LDAP";
@ -167,7 +174,7 @@ in
sso = lib.mkOption {
description = "SSO configuration.";
default = {};
default = { };
type = types.submodule {
options = {
enable = lib.mkEnableOption "SSO";
@ -203,7 +210,10 @@ in
};
authorization_policy = lib.mkOption {
type = types.enum [ "one_factor" "two_factor" ];
type = types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -238,23 +248,27 @@ in
description = ''
Backup configuration.
'';
default = {};
default = { };
type = types.submodule {
options = contracts.backup.mkRequester {
user = "jellyfin";
sourceDirectories = [
config.services.jellyfin.dataDir
];
sourceDirectoriesText = ''[
"services.jellyfin.dataDir"
]'';
sourceDirectoriesText = ''
[
"services.jellyfin.dataDir"
]'';
};
};
};
};
imports = [
(lib.mkRenamedOptionModule [ "shb" "jellyfin" "adminPassword" ] [ "shb" "jellyfin" "admin" "password" ])
(lib.mkRenamedOptionModule
[ "shb" "jellyfin" "adminPassword" ]
[ "shb" "jellyfin" "admin" "password" ]
)
];
config = lib.mkIf cfg.enable {
@ -269,7 +283,10 @@ in
networking.firewall = {
# from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery
allowedUDPPorts = [ 1900 7359 ];
allowedUDPPorts = [
1900
7359
];
};
services.nginx.enable = true;
@ -380,21 +397,23 @@ in
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
}
'';
'';
};
services.prometheus.scrapeConfigs = [{
job_name = "jellyfin";
static_configs = [
{
targets = ["127.0.0.1:${toString cfg.port}"];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
}
];
}];
services.prometheus.scrapeConfigs = [
{
job_name = "jellyfin";
static_configs = [
{
targets = [ "127.0.0.1:${toString cfg.port}" ];
labels = {
"hostname" = config.networking.hostName;
"domain" = cfg.domain;
};
}
];
}
];
# LDAP config but you need to install the plugin by hand
@ -427,7 +446,7 @@ in
<EnabledFolders />
<PasswordResetUrl />
</PluginConfiguration>
'';
'';
# SchemeOverride is needed because of
# https://github.com/9p4/jellyfin-plugin-sso/issues/264
@ -560,23 +579,23 @@ in
</NetworkConfiguration>
'';
in
lib.strings.optionalString cfg.debug
''
if [ -f "${config.services.jellyfin.configDir}/logging.json" ] && [ ! -L "${config.services.jellyfin.configDir}/logging.json" ]; then
echo "A ${config.services.jellyfin.configDir}/logging.json file exists already, this indicates probably an existing installation. Please remove it before continuing."
exit 1
fi
ln -fs "${debugLogging}" "${config.services.jellyfin.configDir}/logging.json"
''
+ (lib.shb.replaceSecretsScript {
file = networkConfig;
# Write permissions are needed otherwise the jellyfin-cli tool will not work correctly.
permissions = "u=rw,g=rw,o=";
resultPath = "${config.services.jellyfin.dataDir}/config/network.xml";
replacements = [
];
})
+ lib.strings.optionalString cfg.ldap.enable (lib.shb.replaceSecretsScript {
lib.strings.optionalString cfg.debug ''
if [ -f "${config.services.jellyfin.configDir}/logging.json" ] && [ ! -L "${config.services.jellyfin.configDir}/logging.json" ]; then
echo "A ${config.services.jellyfin.configDir}/logging.json file exists already, this indicates probably an existing installation. Please remove it before continuing."
exit 1
fi
ln -fs "${debugLogging}" "${config.services.jellyfin.configDir}/logging.json"
''
+ (lib.shb.replaceSecretsScript {
file = networkConfig;
# Write permissions are needed otherwise the jellyfin-cli tool will not work correctly.
permissions = "u=rw,g=rw,o=";
resultPath = "${config.services.jellyfin.dataDir}/config/network.xml";
replacements = [
];
})
+ lib.strings.optionalString cfg.ldap.enable (
lib.shb.replaceSecretsScript {
file = ldapConfig;
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml";
replacements = [
@ -585,8 +604,10 @@ in
source = cfg.ldap.adminPassword.result.path;
}
];
})
+ lib.strings.optionalString cfg.sso.enable (lib.shb.replaceSecretsScript {
}
)
+ lib.strings.optionalString cfg.sso.enable (
lib.shb.replaceSecretsScript {
file = ssoConfig;
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/SSO-Auth.xml";
replacements = [
@ -595,92 +616,96 @@ in
source = cfg.sso.sharedSecret.result.path;
}
];
})
+ lib.strings.optionalString cfg.sso.enable (lib.shb.replaceSecretsScript {
}
)
+ lib.strings.optionalString cfg.sso.enable (
lib.shb.replaceSecretsScript {
file = brandingConfig;
resultPath = "${config.services.jellyfin.dataDir}/config/branding.xml";
replacements = [
];
});
}
);
systemd.services.jellyfin.serviceConfig.ExecStartPost = let
# We must always wait for the service to be fully initialized,
# even if we're planning on changing the config and restarting.
waitForCurl = pkgs.writeShellApplication {
name = "waitForCurl";
runtimeInputs = [ pkgs.curl ];
text = ''
URL="http://127.0.0.1:${toString cfg.port}/System/Info/Public"
SLEEP_INTERVAL_SEC=2
TIMEOUT=60
systemd.services.jellyfin.serviceConfig.ExecStartPost =
let
# We must always wait for the service to be fully initialized,
# even if we're planning on changing the config and restarting.
waitForCurl = pkgs.writeShellApplication {
name = "waitForCurl";
runtimeInputs = [ pkgs.curl ];
text = ''
URL="http://127.0.0.1:${toString cfg.port}/System/Info/Public"
SLEEP_INTERVAL_SEC=2
TIMEOUT=60
start_time=$(date +%s)
start_time=$(date +%s)
echo "Waiting for $URL to return HTTP 200..."
echo "Waiting for $URL to return HTTP 200..."
while true; do
status_code=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || true)
if [ "$status_code" = "200" ]; then
echo "Service is up (HTTP 200 received)."
exit 0
fi
while true; do
status_code=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || true)
if [ "$status_code" = "200" ]; then
echo "Service is up (HTTP 200 received)."
exit 0
fi
now=$(date +%s)
elapsed=$(( now - start_time ))
now=$(date +%s)
elapsed=$(( now - start_time ))
if [ $elapsed -ge $TIMEOUT ]; then
echo "Timeout reached ($TIMEOUT seconds). Exiting with failure."
exit 1
fi
if [ $elapsed -ge $TIMEOUT ]; then
echo "Timeout reached ($TIMEOUT seconds). Exiting with failure."
exit 1
fi
echo "Waiting for service... (status: $status_code), elapsed: ''${elapsed}s"
sleep "$SLEEP_INTERVAL_SEC"
done
echo "Waiting for service... (status: $status_code), elapsed: ''${elapsed}s"
sleep "$SLEEP_INTERVAL_SEC"
done
echo "Finished waiting, curl returned a 200."
'';
};
echo "Finished waiting, curl returned a 200."
'';
};
# This file is used to know if the jellyfin service has been restarted
# because a new config just got written to.
#
# If the file does not exist, write the config, create the file then restart.
# If the file exists, do nothing and remove the file, resetting the state for the next time.
restartedFile="${config.services.jellyfin.dataDir}/.jellyfin-restarted";
# This file is used to know if the jellyfin service has been restarted
# because a new config just got written to.
#
# If the file does not exist, write the config, create the file then restart.
# If the file exists, do nothing and remove the file, resetting the state for the next time.
restartedFile = "${config.services.jellyfin.dataDir}/.jellyfin-restarted";
writeConfig = pkgs.writeShellApplication {
name = "writeConfig";
runtimeInputs = [ pkgs.systemd ];
text = ''
if ! [ -f "${restartedFile}" ]; then
${lib.getExe jellyfin-cli} wizard \
--datadir='${config.services.jellyfin.dataDir}' \
--configdir='${config.services.jellyfin.configDir}' \
--cachedir='${config.services.jellyfin.cacheDir}' \
--logdir='${config.services.jellyfin.logDir}' \
--username=${cfg.admin.username} \
--password-file=${cfg.admin.password.result.path} \
--enable-remote-access=true \
--write
fi
'';
};
writeConfig = pkgs.writeShellApplication {
name = "writeConfig";
runtimeInputs = [ pkgs.systemd ];
text = ''
if ! [ -f "${restartedFile}" ]; then
${lib.getExe jellyfin-cli} wizard \
--datadir='${config.services.jellyfin.dataDir}' \
--configdir='${config.services.jellyfin.configDir}' \
--cachedir='${config.services.jellyfin.cacheDir}' \
--logdir='${config.services.jellyfin.logDir}' \
--username=${cfg.admin.username} \
--password-file=${cfg.admin.password.result.path} \
--enable-remote-access=true \
--write
fi
'';
};
restartJellyfinOnce = pkgs.writeShellApplication {
name = "restartJellyfin";
runtimeInputs = [ pkgs.systemd ];
text = ''
if [ -f "${restartedFile}" ]; then
echo "jellyfin.service has been restarted"
rm "${restartedFile}"
else
echo "Restarting jellyfin.service"
touch "${restartedFile}"
systemctl reload-or-restart jellyfin.service
fi
'';
};
in
restartJellyfinOnce = pkgs.writeShellApplication {
name = "restartJellyfin";
runtimeInputs = [ pkgs.systemd ];
text = ''
if [ -f "${restartedFile}" ]; then
echo "jellyfin.service has been restarted"
rm "${restartedFile}"
else
echo "Restarting jellyfin.service"
touch "${restartedFile}"
systemctl reload-or-restart jellyfin.service
fi
'';
};
in
lib.optionals (cfg.admin != null) [
(lib.getExe waitForCurl)

View file

@ -1,8 +1,13 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.karakeep;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
in
{
imports = [
@ -37,18 +42,18 @@ in
};
environment = lib.mkOption {
default = {};
default = { };
type = lib.types.attrsOf lib.types.str;
description = "Extra environment variables. See https://docs.karakeep.app/configuration/";
example = ''
{
OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}";
INFERENCE_TEXT_MODEL = "deepseek-r1:1.5b";
INFERENCE_IMAGE_MODEL = "llava";
EMBEDDING_TEXT_MODEL = "nomic-embed-text:v1.5";
INFERENCE_ENABLE_AUTO_SUMMARIZATION = "true";
INFERENCE_JOB_TIMEOUT_SEC = "200";
}
{
OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}";
INFERENCE_TEXT_MODEL = "deepseek-r1:1.5b";
INFERENCE_IMAGE_MODEL = "llava";
EMBEDDING_TEXT_MODEL = "nomic-embed-text:v1.5";
INFERENCE_ENABLE_AUTO_SUMMARIZATION = "true";
INFERENCE_JOB_TIMEOUT_SEC = "200";
}
'';
};
@ -56,7 +61,7 @@ in
description = ''
Setup LDAP integration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
userGroup = lib.mkOption {
@ -72,7 +77,7 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO integration.";
@ -91,7 +96,10 @@ in
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -102,7 +110,11 @@ in
options = contracts.secret.mkRequester {
owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets.
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
restartUnits = [
"karakeep-init.service"
"karakeep-workers.service"
"karakeep-workers.service"
];
};
};
};
@ -125,7 +137,7 @@ in
description = ''
Backup state directory.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "karakeep";
@ -142,7 +154,11 @@ in
options = contracts.secret.mkRequester {
owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets.
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
restartUnits = [
"karakeep-init.service"
"karakeep-workers.service"
"karakeep-workers.service"
];
};
};
};
@ -153,93 +169,108 @@ in
options = contracts.secret.mkRequester {
owner = "karakeep";
# These services are the ones relying on the environment file containing the secrets.
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
restartUnits = [
"karakeep-init.service"
"karakeep-workers.service"
"karakeep-workers.service"
];
};
};
};
};
config = (lib.mkMerge [
(lib.mkIf cfg.enable {
services.karakeep = {
enable = true;
meilisearch.enable = true;
config = (
lib.mkMerge [
(lib.mkIf cfg.enable {
services.karakeep = {
enable = true;
meilisearch.enable = true;
extraEnvironment = {
PORT = toString cfg.port;
DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS
} // cfg.environment;
};
extraEnvironment = {
PORT = toString cfg.port;
DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS
}
// cfg.environment;
};
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString cfg.port}/";
}
];
# Piggybacking onto the upstream karakeep-init and replacing its script by ours.
# This is needed otherwise the MEILI_MASTER_KEY is generated randomly on first start
# instead of using the value from the cfg.meilisearchMasterKey option.
systemd.services.karakeep-init = {
script = lib.mkForce ((lib.shb.replaceSecrets {
userConfig = {
MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path;
NEXTAUTH_SECRET.source = cfg.nextauthSecret.result.path;
} // lib.optionalAttrs cfg.sso.enable {
OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path;
};
resultPath = "/var/lib/karakeep/settings.env";
generator = lib.shb.toEnvVar;
}) + ''
export DATA_DIR="$STATE_DIRECTORY"
exec ${config.services.karakeep.package}/lib/karakeep/migrate
'');
};
})
(lib.mkIf cfg.enable {
services.meilisearch = {
dumplessUpgrade = true;
environment = "production";
masterKeyFile = cfg.meilisearchMasterKey.result.path;
};
})
(lib.mkIf (cfg.enable && cfg.sso.enable) {
shb.lldap.ensureGroups = {
${cfg.ldap.userGroup} = {};
};
shb.authelia.extraOidcAuthorizationPolicies.karakeep = {
default_policy = "deny";
rules = [
shb.nginx.vhosts = [
{
subject = [ "group:${cfg.ldap.userGroup}" ];
policy = cfg.sso.authorization_policy;
inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString cfg.port}/";
}
];
};
shb.authelia.oidcClients = [
{
client_id = cfg.sso.clientID;
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
scopes = [ "openid" "email" "profile" ];
authorization_policy = "karakeep";
redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/api/auth/callback/custom"
];
}
];
services.karakeep = {
extraEnvironment = {
DISABLE_SIGNUPS = "false";
DISABLE_PASSWORD_AUTH = "true";
NEXTAUTH_URL = "https://${cfg.subdomain}.${cfg.domain}";
OAUTH_WELLKNOWN_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration";
OAUTH_PROVIDER_NAME = "Single Sign-On";
OAUTH_CLIENT_ID = cfg.sso.clientID;
OAUTH_SCOPE = "openid email profile";
# Piggybacking onto the upstream karakeep-init and replacing its script by ours.
# This is needed otherwise the MEILI_MASTER_KEY is generated randomly on first start
# instead of using the value from the cfg.meilisearchMasterKey option.
systemd.services.karakeep-init = {
script = lib.mkForce (
(lib.shb.replaceSecrets {
userConfig = {
MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path;
NEXTAUTH_SECRET.source = cfg.nextauthSecret.result.path;
}
// lib.optionalAttrs cfg.sso.enable {
OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path;
};
resultPath = "/var/lib/karakeep/settings.env";
generator = lib.shb.toEnvVar;
})
+ ''
export DATA_DIR="$STATE_DIRECTORY"
exec ${config.services.karakeep.package}/lib/karakeep/migrate
''
);
};
};
})
]);
})
(lib.mkIf cfg.enable {
services.meilisearch = {
dumplessUpgrade = true;
environment = "production";
masterKeyFile = cfg.meilisearchMasterKey.result.path;
};
})
(lib.mkIf (cfg.enable && cfg.sso.enable) {
shb.lldap.ensureGroups = {
${cfg.ldap.userGroup} = { };
};
shb.authelia.extraOidcAuthorizationPolicies.karakeep = {
default_policy = "deny";
rules = [
{
subject = [ "group:${cfg.ldap.userGroup}" ];
policy = cfg.sso.authorization_policy;
}
];
};
shb.authelia.oidcClients = [
{
client_id = cfg.sso.clientID;
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
scopes = [
"openid"
"email"
"profile"
];
authorization_policy = "karakeep";
redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/api/auth/callback/custom"
];
}
];
services.karakeep = {
extraEnvironment = {
DISABLE_SIGNUPS = "false";
DISABLE_PASSWORD_AUTH = "true";
NEXTAUTH_URL = "https://${cfg.subdomain}.${cfg.domain}";
OAUTH_WELLKNOWN_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration";
OAUTH_PROVIDER_NAME = "Single Sign-On";
OAUTH_CLIENT_ID = cfg.sso.clientID;
OAUTH_SCOPE = "openid email profile";
};
};
})
]
);
}

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,22 @@
{ config, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
let
cfg = config.shb.open-webui;
contracts = pkgs.callPackage ../contracts {};
contracts = pkgs.callPackage ../contracts { };
roleClaim = "openwebui_groups";
oauthScopes = [ "openid" "email" "profile" "groups" "${roleClaim}" ];
oauthScopes = [
"openid"
"email"
"profile"
"groups"
"${roleClaim}"
];
in
{
imports = [
@ -42,19 +53,19 @@ in
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra environment variables. See https://docs.openwebui.com/getting-started/env-configuration";
default = {};
default = { };
example = ''
{
WEBUI_NAME = "SelfHostBlocks";
{
WEBUI_NAME = "SelfHostBlocks";
OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}";
RAG_EMBEDDING_MODEL = "nomic-embed-text:v1.5";
OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}";
RAG_EMBEDDING_MODEL = "nomic-embed-text:v1.5";
ENABLE_OPENAI_API = "True";
OPENAI_API_BASE_URL = "http://127.0.0.1:''${toString config.services.llama-cpp.port}";
ENABLE_WEB_SEARCH = "True";
RAG_EMBEDDING_ENGINE = "openai";
}
ENABLE_OPENAI_API = "True";
OPENAI_API_BASE_URL = "http://127.0.0.1:''${toString config.services.llama-cpp.port}";
ENABLE_WEB_SEARCH = "True";
RAG_EMBEDDING_ENGINE = "openai";
}
'';
};
@ -62,7 +73,7 @@ in
description = ''
Setup LDAP integration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
userGroup = lib.mkOption {
@ -84,7 +95,7 @@ in
description = ''
Setup SSO integration.
'';
default = {};
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO integration.";
@ -103,7 +114,10 @@ in
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
type = lib.types.enum [
"one_factor"
"two_factor"
];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
@ -136,7 +150,7 @@ in
description = ''
Backup state directory.
'';
default = {};
default = { };
type = lib.types.submodule {
options = contracts.backup.mkRequester {
user = "open-webui";
@ -149,124 +163,127 @@ in
};
};
config = (lib.mkMerge [
(lib.mkIf cfg.enable {
users.users.open-webui = {
isSystemUser = true;
group = "open-webui";
};
users.groups.open-webui = {};
services.open-webui = {
enable = true;
host = "127.0.0.1";
inherit (cfg) port;
environment = {
WEBUI_URL = "https://${cfg.subdomain}.${cfg.domain}";
ENABLE_PERSISTENT_CONFIG = "False";
ANONYMIZED_TELEMETRY = "False";
DO_NOT_TRACK = "True";
SCARF_NO_ANALYTICS = "True";
ENABLE_VERSION_UPDATE_CHECK = "False";
} // cfg.environment;
};
systemd.services.open-webui.path = [
pkgs.ffmpeg-headless
];
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString cfg.port}/";
extraConfig = ''
proxy_read_timeout 300s;
proxy_send_timeout 300s;
'';
}
];
})
(lib.mkIf (cfg.enable && cfg.sso.enable) {
shb.lldap.ensureGroups = {
${cfg.ldap.userGroup} = {};
${cfg.ldap.adminGroup} = {};
};
services.open-webui = {
package = pkgs.open-webui.overrideAttrs (finalAttrs: {
patches = [
../../patches/0001-selfhostblocks-never-onboard.patch
../../patches/0002-selfhostblocks-do-not-allow-unauthorized-roles.patch
];
});
environment = {
ENABLE_SIGNUP = "False";
WEBUI_AUTH = "True";
ENABLE_FORWARD_USER_INFO_HEADERS = "True";
ENABLE_OAUTH_SIGNUP = "True";
OAUTH_UPDATE_PICTURE_ON_LOGIN = "True";
OAUTH_CLIENT_ID = cfg.sso.clientID;
OPENID_PROVIDER_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration";
OAUTH_PROVIDER_NAME = "Single Sign-On";
OAUTH_USERNAME_CLAIM = "preferred_username";
ENABLE_OAUTH_ROLE_MANAGEMENT = "True";
OAUTH_ALLOWED_ROLES = "user,admin";
OAUTH_ADMIN_ROLES = "admin";
OAUTH_ROLES_CLAIM = roleClaim;
OAUTH_SCOPES = lib.concatStringsSep " " oauthScopes;
config = (
lib.mkMerge [
(lib.mkIf cfg.enable {
users.users.open-webui = {
isSystemUser = true;
group = "open-webui";
};
};
users.groups.open-webui = { };
shb.authelia.extraDefinitions = {
user_attributes.${roleClaim}.expression =
''"${cfg.ldap.adminGroup}" in groups ? ["admin"] : ("${cfg.ldap.userGroup}" in groups ? ["user"] : [""])'';
};
shb.authelia.extraOidcClaimsPolicies.${roleClaim} = {
custom_claims = {
"${roleClaim}" = {};
services.open-webui = {
enable = true;
host = "127.0.0.1";
inherit (cfg) port;
environment = {
WEBUI_URL = "https://${cfg.subdomain}.${cfg.domain}";
ENABLE_PERSISTENT_CONFIG = "False";
ANONYMIZED_TELEMETRY = "False";
DO_NOT_TRACK = "True";
SCARF_NO_ANALYTICS = "True";
ENABLE_VERSION_UPDATE_CHECK = "False";
}
// cfg.environment;
};
};
shb.authelia.extraOidcScopes."${roleClaim}" = {
claims = [ "${roleClaim}" ];
};
shb.authelia.oidcClients = [
{
client_id = cfg.sso.clientID;
client_name = "Open WebUI";
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
claims_policy = "${roleClaim}";
public = false;
authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/oauth/oidc/callback"
];
scopes = oauthScopes;
}
];
systemd.services.open-webui.path = [
pkgs.ffmpeg-headless
];
systemd.services.open-webui.serviceConfig.EnvironmentFile = "/run/open-webui/secrets.env";
systemd.tmpfiles.rules = [
"d '/run/open-webui' 0750 root root - -"
];
systemd.services.open-webui-pre = {
script = lib.shb.replaceSecrets {
userConfig = {
OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path;
shb.nginx.vhosts = [
{
inherit (cfg) subdomain domain ssl;
upstream = "http://127.0.0.1:${toString cfg.port}/";
extraConfig = ''
proxy_read_timeout 300s;
proxy_send_timeout 300s;
'';
}
];
})
(lib.mkIf (cfg.enable && cfg.sso.enable) {
shb.lldap.ensureGroups = {
${cfg.ldap.userGroup} = { };
${cfg.ldap.adminGroup} = { };
};
services.open-webui = {
package = pkgs.open-webui.overrideAttrs (finalAttrs: {
patches = [
../../patches/0001-selfhostblocks-never-onboard.patch
../../patches/0002-selfhostblocks-do-not-allow-unauthorized-roles.patch
];
});
environment = {
ENABLE_SIGNUP = "False";
WEBUI_AUTH = "True";
ENABLE_FORWARD_USER_INFO_HEADERS = "True";
ENABLE_OAUTH_SIGNUP = "True";
OAUTH_UPDATE_PICTURE_ON_LOGIN = "True";
OAUTH_CLIENT_ID = cfg.sso.clientID;
OPENID_PROVIDER_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration";
OAUTH_PROVIDER_NAME = "Single Sign-On";
OAUTH_USERNAME_CLAIM = "preferred_username";
ENABLE_OAUTH_ROLE_MANAGEMENT = "True";
OAUTH_ALLOWED_ROLES = "user,admin";
OAUTH_ADMIN_ROLES = "admin";
OAUTH_ROLES_CLAIM = roleClaim;
OAUTH_SCOPES = lib.concatStringsSep " " oauthScopes;
};
resultPath = "/run/open-webui/secrets.env";
generator = lib.shb.toEnvVar;
};
serviceConfig.Type = "oneshot";
wantedBy = [ "multi-user.target" ];
before = [ "open-webui.service" ];
requiredBy = [ "open-webui.service" ];
};
})
]);
shb.authelia.extraDefinitions = {
user_attributes.${roleClaim}.expression =
''"${cfg.ldap.adminGroup}" in groups ? ["admin"] : ("${cfg.ldap.userGroup}" in groups ? ["user"] : [""])'';
};
shb.authelia.extraOidcClaimsPolicies.${roleClaim} = {
custom_claims = {
"${roleClaim}" = { };
};
};
shb.authelia.extraOidcScopes."${roleClaim}" = {
claims = [ "${roleClaim}" ];
};
shb.authelia.oidcClients = [
{
client_id = cfg.sso.clientID;
client_name = "Open WebUI";
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
claims_policy = "${roleClaim}";
public = false;
authorization_policy = cfg.sso.authorization_policy;
redirect_uris = [
"https://${cfg.subdomain}.${cfg.domain}/oauth/oidc/callback"
];
scopes = oauthScopes;
}
];
systemd.services.open-webui.serviceConfig.EnvironmentFile = "/run/open-webui/secrets.env";
systemd.tmpfiles.rules = [
"d '/run/open-webui' 0750 root root - -"
];
systemd.services.open-webui-pre = {
script = lib.shb.replaceSecrets {
userConfig = {
OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path;
};
resultPath = "/run/open-webui/secrets.env";
generator = lib.shb.toEnvVar;
};
serviceConfig.Type = "oneshot";
wantedBy = [ "multi-user.target" ];
before = [ "open-webui.service" ];
requiredBy = [ "open-webui.service" ];
};
})
]
);
}

View file

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

View file

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

View file

@ -8,178 +8,185 @@ in
basic = lib.shb.runNixOSTest {
name = "authelia-basic";
nodes.machine = { config, pkgs, ... }: {
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/authelia.nix
../../modules/blocks/hardcodedsecret.nix
];
networking.hosts = {
"127.0.0.1" = [
"machine.com"
"client1.machine.com"
"client2.machine.com"
"ldap.machine.com"
"authelia.machine.com"
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/authelia.nix
../../modules/blocks/hardcodedsecret.nix
];
};
shb.lldap = {
enable = true;
dcdomain = "dc=example,dc=com";
subdomain = "ldap";
domain = "machine.com";
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
};
shb.hardcodedsecret.ldapUserPassword = {
request = config.shb.lldap.ldapUserPassword.request;
settings.content = ldapAdminPassword;
};
shb.hardcodedsecret.jwtSecret = {
request = config.shb.lldap.jwtSecret.request;
settings.content = "jwtsecret";
};
shb.authelia = {
enable = true;
subdomain = "authelia";
domain = "machine.com";
ldapHostname = "${config.shb.lldap.subdomain}.${config.shb.lldap.domain}";
ldapPort = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
secrets = {
jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result;
ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
networking.hosts = {
"127.0.0.1" = [
"machine.com"
"client1.machine.com"
"client2.machine.com"
"ldap.machine.com"
"authelia.machine.com"
];
};
oidcClients = [
{
client_id = "client1";
client_name = "My Client 1";
client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$LR2wY11djfLrVQixdlLJew$rPByqFt6JfbIIAITxzAXckwh51QgV8E5YZmA8rXOzkMfBUcMq7cnOKEXF6MAFbjZaGf3J/B1OzLWZTCuZtALVw";
public = false;
authorization_policy = "one_factor";
redirect_uris = [ "http://client1.machine.com/redirect" ];
}
{
client_id = "client2";
client_name = "My Client 2";
client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$76EqVU1N9K.iTOvD4WJ6ww$hqNJU.UHphiCjMChSqk27lUTjDqreuMuyV/u39Esc6HyiRXp5Ecx89ypJ5M0xk3Na97vbgDpwz7il5uwzQ4bfw";
public = false;
authorization_policy = "one_factor";
redirect_uris = [ "http://client2.machine.com/redirect" ];
}
];
};
shb.lldap = {
enable = true;
dcdomain = "dc=example,dc=com";
subdomain = "ldap";
domain = "machine.com";
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
};
shb.hardcodedsecret.autheliaJwtSecret = {
request = config.shb.authelia.secrets.jwtSecret.request;
settings.content = "jwtSecret";
};
shb.hardcodedsecret.ldapAdminPassword = {
request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.content = ldapAdminPassword;
};
shb.hardcodedsecret.sessionSecret = {
request = config.shb.authelia.secrets.sessionSecret.request;
settings.content = "sessionSecret";
};
shb.hardcodedsecret.storageEncryptionKey = {
request = config.shb.authelia.secrets.storageEncryptionKey.request;
settings.content = "storageEncryptionKey";
};
shb.hardcodedsecret.identityProvidersOIDCHMACSecret = {
request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
settings.content = "identityProvidersOIDCHMACSecret";
};
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = {
request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
settings.source = (pkgs.runCommand "gen-private-key" {} ''
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem";
};
shb.hardcodedsecret.ldapUserPassword = {
request = config.shb.lldap.ldapUserPassword.request;
settings.content = ldapAdminPassword;
};
shb.hardcodedsecret.jwtSecret = {
request = config.shb.lldap.jwtSecret.request;
settings.content = "jwtsecret";
};
specialisation = {
withDebug.configuration = {
shb.authelia.debug = true;
shb.authelia = {
enable = true;
subdomain = "authelia";
domain = "machine.com";
ldapHostname = "${config.shb.lldap.subdomain}.${config.shb.lldap.domain}";
ldapPort = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
secrets = {
jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result;
ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
identityProvidersOIDCHMACSecret.result =
config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result =
config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
};
oidcClients = [
{
client_id = "client1";
client_name = "My Client 1";
client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$LR2wY11djfLrVQixdlLJew$rPByqFt6JfbIIAITxzAXckwh51QgV8E5YZmA8rXOzkMfBUcMq7cnOKEXF6MAFbjZaGf3J/B1OzLWZTCuZtALVw";
public = false;
authorization_policy = "one_factor";
redirect_uris = [ "http://client1.machine.com/redirect" ];
}
{
client_id = "client2";
client_name = "My Client 2";
client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$76EqVU1N9K.iTOvD4WJ6ww$hqNJU.UHphiCjMChSqk27lUTjDqreuMuyV/u39Esc6HyiRXp5Ecx89ypJ5M0xk3Na97vbgDpwz7il5uwzQ4bfw";
public = false;
authorization_policy = "one_factor";
redirect_uris = [ "http://client2.machine.com/redirect" ];
}
];
};
shb.hardcodedsecret.autheliaJwtSecret = {
request = config.shb.authelia.secrets.jwtSecret.request;
settings.content = "jwtSecret";
};
shb.hardcodedsecret.ldapAdminPassword = {
request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.content = ldapAdminPassword;
};
shb.hardcodedsecret.sessionSecret = {
request = config.shb.authelia.secrets.sessionSecret.request;
settings.content = "sessionSecret";
};
shb.hardcodedsecret.storageEncryptionKey = {
request = config.shb.authelia.secrets.storageEncryptionKey.request;
settings.content = "storageEncryptionKey";
};
shb.hardcodedsecret.identityProvidersOIDCHMACSecret = {
request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
settings.content = "identityProvidersOIDCHMACSecret";
};
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = {
request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
settings.source =
(pkgs.runCommand "gen-private-key" { } ''
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'')
+ "/private.pem";
};
specialisation = {
withDebug.configuration = {
shb.authelia.debug = true;
};
};
};
};
testScript = { nodes, ... }:
let
specializations = "${nodes.machine.system.build.toplevel}/specialisation";
in
''
import json
testScript =
{ nodes, ... }:
let
specializations = "${nodes.machine.system.build.toplevel}/specialisation";
in
''
import json
start_all()
start_all()
def tests():
machine.wait_for_unit("lldap.service")
machine.wait_for_unit("authelia-authelia.machine.com.target")
machine.wait_for_open_port(9091)
def tests():
machine.wait_for_unit("lldap.service")
machine.wait_for_unit("authelia-authelia.machine.com.target")
machine.wait_for_open_port(9091)
endpoints = json.loads(machine.succeed("curl -s http://machine.com/.well-known/openid-configuration"))
auth_endpoint = endpoints['authorization_endpoint']
print(f"auth_endpoint: {auth_endpoint}")
if auth_endpoint != "http://machine.com/api/oidc/authorization":
raise Exception("Unexpected auth_endpoint")
endpoints = json.loads(machine.succeed("curl -s http://machine.com/.well-known/openid-configuration"))
auth_endpoint = endpoints['authorization_endpoint']
print(f"auth_endpoint: {auth_endpoint}")
if auth_endpoint != "http://machine.com/api/oidc/authorization":
raise Exception("Unexpected auth_endpoint")
resp = machine.succeed(
"curl -f -s '"
+ auth_endpoint
+ "?client_id=other"
+ "&redirect_uri=http://client1.machine.com/redirect"
+ "&scope=openid%20profile%20email"
+ "&response_type=code"
+ "&state=99999999'"
)
print(resp)
if resp != "":
raise Exception("unexpected response")
resp = machine.succeed(
"curl -f -s '"
+ auth_endpoint
+ "?client_id=other"
+ "&redirect_uri=http://client1.machine.com/redirect"
+ "&scope=openid%20profile%20email"
+ "&response_type=code"
+ "&state=99999999'"
)
print(resp)
if resp != "":
raise Exception("unexpected response")
resp = machine.succeed(
"curl -f -s '"
+ auth_endpoint
+ "?client_id=client1"
+ "&redirect_uri=http://client1.machine.com/redirect"
+ "&scope=openid%20profile%20email"
+ "&response_type=code"
+ "&state=11111111'"
)
print(resp)
if "Found" not in resp:
raise Exception("unexpected response")
resp = machine.succeed(
"curl -f -s '"
+ auth_endpoint
+ "?client_id=client1"
+ "&redirect_uri=http://client1.machine.com/redirect"
+ "&scope=openid%20profile%20email"
+ "&response_type=code"
+ "&state=11111111'"
)
print(resp)
if "Found" not in resp:
raise Exception("unexpected response")
resp = machine.succeed(
"curl -f -s '"
+ auth_endpoint
+ "?client_id=client2"
+ "&redirect_uri=http://client2.machine.com/redirect"
+ "&scope=openid%20profile%20email"
+ "&response_type=code"
+ "&state=22222222'"
)
print(resp)
if "Found" not in resp:
raise Exception("unexpected response")
resp = machine.succeed(
"curl -f -s '"
+ auth_endpoint
+ "?client_id=client2"
+ "&redirect_uri=http://client2.machine.com/redirect"
+ "&scope=openid%20profile%20email"
+ "&response_type=code"
+ "&state=22222222'"
)
print(resp)
if "Found" not in resp:
raise Exception("unexpected response")
with subtest("no debug"):
tests()
with subtest("no debug"):
tests()
with subtest("with debug"):
machine.succeed('${specializations}/withDebug/bin/switch-to-configuration test')
tests()
'';
with subtest("with debug"):
machine.succeed('${specializations}/withDebug/bin/switch-to-configuration test')
tests()
'';
};
}

View file

@ -24,7 +24,9 @@ in
configWithTemplates = lib.shb.withReplacements userConfig;
nonSecretConfigFile = pkgs.writeText "config.yaml.template" (lib.generators.toJSON {} configWithTemplates);
nonSecretConfigFile = pkgs.writeText "config.yaml.template" (
lib.generators.toJSON { } configWithTemplates
);
replacements = lib.shb.getReplacements userConfig;
@ -37,105 +39,113 @@ in
replaceInTemplateJSON = lib.shb.replaceSecrets {
inherit userConfig;
resultPath = "/var/lib/config.json";
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json {});
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json { });
};
replaceInTemplateJSONGen = lib.shb.replaceSecrets {
inherit userConfig;
resultPath = "/var/lib/config_gen.json";
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toJSON {});
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toJSON { });
};
replaceInTemplateXML = lib.shb.replaceSecrets {
inherit userConfig;
resultPath = "/var/lib/config.xml";
generator = lib.shb.replaceSecretsFormatAdapter (lib.shb.formatXML {enclosingRoot = "Root";});
generator = lib.shb.replaceSecretsFormatAdapter (lib.shb.formatXML { enclosingRoot = "Root"; });
};
in
lib.shb.runNixOSTest {
name = "lib-template";
nodes.machine = { config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
{
options = {
libtest.config = lib.mkOption {
type = lib.types.attrsOf (lib.types.oneOf [ lib.types.str lib.secretFileType ]);
};
lib.shb.runNixOSTest {
name = "lib-template";
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
{
options = {
libtest.config = lib.mkOption {
type = lib.types.attrsOf (
lib.types.oneOf [
lib.types.str
lib.secretFileType
]
);
};
}
];
};
}
];
system.activationScripts = {
libtest = replaceInTemplate;
libtestJSON = replaceInTemplateJSON;
libtestJSONGen = replaceInTemplateJSONGen;
libtestXML = replaceInTemplateXML;
};
system.activationScripts = {
libtest = replaceInTemplate;
libtestJSON = replaceInTemplateJSON;
libtestJSONGen = replaceInTemplateJSONGen;
libtestXML = replaceInTemplateXML;
};
};
testScript = { nodes, ... }: ''
import json
from collections import ChainMap
from xml.etree import ElementTree
testScript =
{ nodes, ... }:
''
import json
from collections import ChainMap
from xml.etree import ElementTree
start_all()
machine.wait_for_file("/var/lib/config.yaml")
machine.wait_for_file("/var/lib/config.json")
machine.wait_for_file("/var/lib/config_gen.json")
machine.wait_for_file("/var/lib/config.xml")
start_all()
machine.wait_for_file("/var/lib/config.yaml")
machine.wait_for_file("/var/lib/config.json")
machine.wait_for_file("/var/lib/config_gen.json")
machine.wait_for_file("/var/lib/config.xml")
def xml_to_dict_recursive(root):
all_descendants = list(root)
if len(all_descendants) == 0:
return {root.tag: root.text}
else:
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
return {root.tag: dict(merged_dict)}
def xml_to_dict_recursive(root):
all_descendants = list(root)
if len(all_descendants) == 0:
return {root.tag: root.text}
else:
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
return {root.tag: dict(merged_dict)}
wantedConfig = json.loads('${lib.generators.toJSON {} wantedConfig}')
wantedConfig = json.loads('${lib.generators.toJSON { } wantedConfig}')
with subtest("config"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}"))
with subtest("config"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}"))
gotConfig = machine.succeed("cat /var/lib/config.yaml")
print(gotConfig)
gotConfig = json.loads(gotConfig)
gotConfig = machine.succeed("cat /var/lib/config.yaml")
print(gotConfig)
gotConfig = json.loads(gotConfig)
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
with subtest("config JSON Gen"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSONGen" replaceInTemplateJSONGen}"))
with subtest("config JSON Gen"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSONGen" replaceInTemplateJSONGen}"))
gotConfig = machine.succeed("cat /var/lib/config_gen.json")
print(gotConfig)
gotConfig = json.loads(gotConfig)
gotConfig = machine.succeed("cat /var/lib/config_gen.json")
print(gotConfig)
gotConfig = json.loads(gotConfig)
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
with subtest("config JSON"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSON" replaceInTemplateJSON}"))
with subtest("config JSON"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSON" replaceInTemplateJSON}"))
gotConfig = machine.succeed("cat /var/lib/config.json")
print(gotConfig)
gotConfig = json.loads(gotConfig)
gotConfig = machine.succeed("cat /var/lib/config.json")
print(gotConfig)
gotConfig = json.loads(gotConfig)
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
with subtest("config XML"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateXML" replaceInTemplateXML}"))
with subtest("config XML"):
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateXML" replaceInTemplateXML}"))
gotConfig = machine.succeed("cat /var/lib/config.xml")
print(gotConfig)
gotConfig = xml_to_dict_recursive(ElementTree.XML(gotConfig))['Root']
gotConfig = machine.succeed("cat /var/lib/config.xml")
print(gotConfig)
gotConfig = xml_to_dict_recursive(ElementTree.XML(gotConfig))['Root']
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
'';
};
};
}

View file

@ -9,145 +9,148 @@ in
auth = lib.shb.runNixOSTest {
name = "ldap-auth";
nodes.server = { config, pkgs, ... }: {
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
{
options = {
shb.ssl.enable = lib.mkEnableOption "ssl";
nodes.server =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
{
options = {
shb.ssl.enable = lib.mkEnableOption "ssl";
};
}
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/lldap.nix
];
shb.lldap = {
enable = true;
dcdomain = "dc=example,dc=com";
subdomain = "ldap";
domain = "example.com";
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
ensureUsers = {
"charlie" = {
email = "charlie@example.com";
password.result = config.shb.hardcodedsecret."charlie".result;
};
};
}
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/lldap.nix
];
shb.lldap = {
enable = true;
dcdomain = "dc=example,dc=com";
subdomain = "ldap";
domain = "example.com";
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
ensureUsers = {
"charlie" = {
email = "charlie@example.com";
password.result = config.shb.hardcodedsecret."charlie".result;
ensureGroups = {
"family" = { };
};
};
shb.hardcodedsecret.ldapUserPassword = {
request = config.shb.lldap.ldapUserPassword.request;
settings.content = password;
};
shb.hardcodedsecret.jwtSecret = {
request = config.shb.lldap.jwtSecret.request;
settings.content = "jwtSecret";
};
shb.hardcodedsecret."charlie" = {
request = config.shb.lldap.ensureUsers."charlie".password.request;
settings.content = charliePassword;
};
ensureGroups = {
"family" = {};
networking.firewall.allowedTCPPorts = [ 80 ]; # nginx port
environment.systemPackages = [ pkgs.openldap ];
specialisation = {
withDebug.configuration = {
shb.lldap.debug = true;
};
};
};
shb.hardcodedsecret.ldapUserPassword = {
request = config.shb.lldap.ldapUserPassword.request;
settings.content = password;
};
shb.hardcodedsecret.jwtSecret = {
request = config.shb.lldap.jwtSecret.request;
settings.content = "jwtSecret";
};
shb.hardcodedsecret."charlie" = {
request = config.shb.lldap.ensureUsers."charlie".password.request;
settings.content = charliePassword;
};
networking.firewall.allowedTCPPorts = [ 80 ]; # nginx port
environment.systemPackages = [ pkgs.openldap ];
specialisation = {
withDebug.configuration = {
shb.lldap.debug = true;
};
};
};
nodes.client = {};
nodes.client = { };
# Inspired from https://github.com/lldap/lldap/blob/33f50d13a2e2d24a3e6bb05a148246bc98090df0/example_configs/lldap-ha-auth.sh
testScript = { nodes, ... }:
let
specializations = "${nodes.server.system.build.toplevel}/specialisation";
in
''
import json
testScript =
{ nodes, ... }:
let
specializations = "${nodes.server.system.build.toplevel}/specialisation";
in
''
import json
start_all()
start_all()
def tests():
server.wait_for_unit("lldap.service")
server.wait_for_open_port(${toString nodes.server.shb.lldap.webUIListenPort})
server.wait_for_open_port(${toString nodes.server.shb.lldap.ldapPort})
def tests():
server.wait_for_unit("lldap.service")
server.wait_for_open_port(${toString nodes.server.shb.lldap.webUIListenPort})
server.wait_for_open_port(${toString nodes.server.shb.lldap.ldapPort})
with subtest("fail without authenticating"):
client.fail(
"curl -f -s -X GET"
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ " http://server/api/graphql"
)
with subtest("fail without authenticating"):
client.fail(
"curl -f -s -X GET"
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ " http://server/api/graphql"
)
with subtest("fail authenticating with wrong credentials"):
resp = client.fail(
"curl -f -s -X POST"
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ " http://server/auth/simple/login"
+ """ -d '{"username": "admin", "password": "wrong"}'"""
)
with subtest("fail authenticating with wrong credentials"):
resp = client.fail(
"curl -f -s -X POST"
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ " http://server/auth/simple/login"
+ """ -d '{"username": "admin", "password": "wrong"}'"""
)
print(resp)
print(resp)
with subtest("succeed with correct authentication"):
token = json.loads(client.succeed(
"curl -f -s -X POST "
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ " http://server/auth/simple/login "
+ """ -d '{"username": "admin", "password": "${password}"}' """
))['token']
with subtest("succeed with correct authentication"):
token = json.loads(client.succeed(
"curl -f -s -X POST "
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ " http://server/auth/simple/login "
+ """ -d '{"username": "admin", "password": "${password}"}' """
))['token']
data = json.loads(client.succeed(
"curl -f -s -X POST "
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ """ -H "Authorization: Bearer {token}" """.format(token=token)
+ " http://server/api/graphql "
+ """ -d '{"variables": {"id": "admin"}, "query":"query($id:String!){user(userId:$id){displayName groups{displayName}}}"}' """
))['data']
data = json.loads(client.succeed(
"curl -f -s -X POST "
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ """ -H "Authorization: Bearer {token}" """.format(token=token)
+ " http://server/api/graphql "
+ """ -d '{"variables": {"id": "admin"}, "query":"query($id:String!){user(userId:$id){displayName groups{displayName}}}"}' """
))['data']
assert data['user']['displayName'] == "Administrator"
assert data['user']['groups'][0]['displayName'] == "lldap_admin"
assert data['user']['displayName'] == "Administrator"
assert data['user']['groups'][0]['displayName'] == "lldap_admin"
with subtest("succeed charlie"):
resp = client.succeed(
"curl -f -s -X POST "
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ " http://server/auth/simple/login "
+ """ -d '{"username": "charlie", "password": "${charliePassword}"}' """
)
print(resp)
with subtest("succeed charlie"):
resp = client.succeed(
"curl -f -s -X POST "
+ """ -H "Content-type: application/json" """
+ """ -H "Host: ldap.example.com" """
+ " http://server/auth/simple/login "
+ """ -d '{"username": "charlie", "password": "${charliePassword}"}' """
)
print(resp)
with subtest("ldap user search"):
resp = server.succeed('ldapsearch -H ldap://127.0.0.1:${toString nodes.server.shb.lldap.ldapPort} -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w ${password}')
print(resp)
with subtest("ldap user search"):
resp = server.succeed('ldapsearch -H ldap://127.0.0.1:${toString nodes.server.shb.lldap.ldapPort} -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w ${password}')
print(resp)
if "uid=admin" not in resp:
raise Exception("Expected to find admin")
if "uid=admin" not in resp:
raise Exception("Expected to find admin")
if "uid=charlie" not in resp:
raise Exception("Expected to find charlie")
if "uid=charlie" not in resp:
raise Exception("Expected to find charlie")
with subtest("no debug"):
tests()
with subtest("no debug"):
tests()
with subtest("with debug"):
server.succeed('${specializations}/withDebug/bin/switch-to-configuration test')
tests()
'';
with subtest("with debug"):
server.succeed('${specializations}/withDebug/bin/switch-to-configuration test')
tests()
'';
};
}

View file

@ -1,137 +1,148 @@
{ pkgs, lib, ... }:
let
serve = port: text: lib.getExe (pkgs.writers.writePython3Bin "serve"
{
libraries = [ pkgs.python3Packages.systemd ];
}
(let
content = pkgs.writeText "content" text;
in ''
from http.server import BaseHTTPRequestHandler, HTTPServer
from systemd.daemon import notify
serve =
port: text:
lib.getExe (
pkgs.writers.writePython3Bin "serve"
{
libraries = [ pkgs.python3Packages.systemd ];
}
(
let
content = pkgs.writeText "content" text;
in
''
from http.server import BaseHTTPRequestHandler, HTTPServer
from systemd.daemon import notify
with open("${content}", "rb") as f:
content = f.read()
with open("${content}", "rb") as f:
content = f.read()
class HardcodedHandler(BaseHTTPRequestHandler):
def do_GET(self):
reponse = content + self.path.encode('utf-8')
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(reponse)))
self.end_headers()
print("answering to GET request")
self.wfile.write(reponse)
class HardcodedHandler(BaseHTTPRequestHandler):
def do_GET(self):
reponse = content + self.path.encode('utf-8')
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(reponse)))
self.end_headers()
print("answering to GET request")
self.wfile.write(reponse)
def log_message(self, format, *args):
pass # optional: suppress logging
def log_message(self, format, *args):
pass # optional: suppress logging
if __name__ == "__main__":
notify('STATUS=Starting up...')
server_address = ('127.0.0.1', ${toString port})
httpd = HTTPServer(server_address, HardcodedHandler)
print("Serving hardcoded page on http://127.0.0.1:${toString port}")
notify('READY=1')
httpd.serve_forever()
'')
);
if __name__ == "__main__":
notify('STATUS=Starting up...')
server_address = ('127.0.0.1', ${toString port})
httpd = HTTPServer(server_address, HardcodedHandler)
print("Serving hardcoded page on http://127.0.0.1:${toString port}")
notify('READY=1')
httpd.serve_forever()
''
)
);
in
{
default = lib.shb.runNixOSTest {
name = "mitmdump-default";
nodes.machine = { config, pkgs, ... }: {
imports = [
../../modules/blocks/mitmdump.nix
];
systemd.services.test1 = {
serviceConfig.ExecStart = serve 8000 "test1";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
StandardOutput = "journal";
StandardError = "journal";
};
};
systemd.services.test2 = {
serviceConfig.ExecStart = serve 8002 "test2";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
StandardOutput = "journal";
StandardError = "journal";
};
};
shb.mitmdump.instances."test1" = {
listenPort = 8001;
upstreamPort = 8000;
after = [ "test1.service" ];
};
shb.mitmdump.instances."test2" = {
listenPort = 8003;
upstreamPort = 8002;
after = [ "test2.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [
"--set" "verbose_pattern=/verbose"
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
../../modules/blocks/mitmdump.nix
];
systemd.services.test1 = {
serviceConfig.ExecStart = serve 8000 "test1";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
StandardOutput = "journal";
StandardError = "journal";
};
};
systemd.services.test2 = {
serviceConfig.ExecStart = serve 8002 "test2";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
StandardOutput = "journal";
StandardError = "journal";
};
};
shb.mitmdump.instances."test1" = {
listenPort = 8001;
upstreamPort = 8000;
after = [ "test1.service" ];
};
shb.mitmdump.instances."test2" = {
listenPort = 8003;
upstreamPort = 8002;
after = [ "test2.service" ];
enabledAddons = [ config.shb.mitmdump.addons.logger ];
extraArgs = [
"--set"
"verbose_pattern=/verbose"
];
};
};
};
testScript = { nodes, ... }: ''
start_all()
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("test1.service")
machine.wait_for_unit("test2.service")
machine.wait_for_unit("mitmdump-test1.service")
machine.wait_for_unit("mitmdump-test2.service")
machine.wait_for_unit("test1.service")
machine.wait_for_unit("test2.service")
machine.wait_for_unit("mitmdump-test1.service")
machine.wait_for_unit("mitmdump-test2.service")
resp = machine.succeed("curl http://127.0.0.1:8000")
print(resp)
if resp != "test1/":
raise Exception("wanted 'test1'")
resp = machine.succeed("curl http://127.0.0.1:8000")
print(resp)
if resp != "test1/":
raise Exception("wanted 'test1'")
resp = machine.succeed("curl -v http://127.0.0.1:8001")
print(resp)
if resp != "test1/":
raise Exception("wanted 'test1'")
resp = machine.succeed("curl -v http://127.0.0.1:8001")
print(resp)
if resp != "test1/":
raise Exception("wanted 'test1'")
resp = machine.succeed("curl http://127.0.0.1:8002")
print(resp)
if resp != "test2/":
raise Exception("wanted 'test2'")
resp = machine.succeed("curl http://127.0.0.1:8002")
print(resp)
if resp != "test2/":
raise Exception("wanted 'test2'")
resp = machine.succeed("curl http://127.0.0.1:8003/notverbose")
print(resp)
if resp != "test2/notverbose":
raise Exception("wanted 'test2/notverbose'")
resp = machine.succeed("curl http://127.0.0.1:8003/notverbose")
print(resp)
if resp != "test2/notverbose":
raise Exception("wanted 'test2/notverbose'")
resp = machine.succeed("curl http://127.0.0.1:8003/verbose")
print(resp)
if resp != "test2/verbose":
raise Exception("wanted 'test2/verbose'")
resp = machine.succeed("curl http://127.0.0.1:8003/verbose")
print(resp)
if resp != "test2/verbose":
raise Exception("wanted 'test2/verbose'")
dump = machine.succeed("journalctl -b -u mitmdump-test1.service")
print(dump)
if "HTTP/1.0 200 OK" not in dump:
raise Exception("expected to see HTTP/1.0 200 OK")
if "test1" not in dump:
raise Exception("expected to see test1")
dump = machine.succeed("journalctl -b -u mitmdump-test1.service")
print(dump)
if "HTTP/1.0 200 OK" not in dump:
raise Exception("expected to see HTTP/1.0 200 OK")
if "test1" not in dump:
raise Exception("expected to see test1")
dump = machine.succeed("journalctl -b -u mitmdump-test2.service")
print(dump)
if "HTTP/1.0 200 OK" not in dump:
raise Exception("expected to see HTTP/1.0 200 OK")
if "test2/notverbose" in dump:
raise Exception("expected not to see test2/notverbose")
if "test2/verbose" not in dump:
raise Exception("expected to see test2/verbose")
'';
dump = machine.succeed("journalctl -b -u mitmdump-test2.service")
print(dump)
if "HTTP/1.0 200 OK" not in dump:
raise Exception("expected to see HTTP/1.0 200 OK")
if "test2/notverbose" in dump:
raise Exception("expected not to see test2/notverbose")
if "test2/verbose" not in dump:
raise Exception("expected to see test2/verbose")
'';
};
}

View file

@ -6,177 +6,193 @@ in
peerWithoutUser = lib.shb.runNixOSTest {
name = "postgresql-peerWithoutUser";
nodes.machine = { config, pkgs, ... }: {
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/postgresql.nix
];
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/postgresql.nix
];
shb.postgresql.ensures = [
{
username = "me";
database = "me";
}
];
};
shb.postgresql.ensures = [
{
username = "me";
database = "me";
}
];
};
testScript = { nodes, ... }: ''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
def peer_cmd(user, database):
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
def peer_cmd(user, database):
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
with subtest("cannot login because of missing user"):
machine.fail(peer_cmd("me", "me"), timeout=10)
with subtest("cannot login because of missing user"):
machine.fail(peer_cmd("me", "me"), timeout=10)
with subtest("cannot login with unknown user"):
machine.fail(peer_cmd("notme", "me"), timeout=10)
with subtest("cannot login with unknown user"):
machine.fail(peer_cmd("notme", "me"), timeout=10)
with subtest("cannot login to unknown database"):
machine.fail(peer_cmd("me", "notmine"), timeout=10)
'';
with subtest("cannot login to unknown database"):
machine.fail(peer_cmd("me", "notmine"), timeout=10)
'';
};
peerAuth = lib.shb.runNixOSTest {
name = "postgresql-peerAuth";
nodes.machine = { config, pkgs, ... }: {
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/postgresql.nix
];
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/postgresql.nix
];
users.users.me = {
isSystemUser = true;
group = "me";
extraGroups = [ "sudoers" ];
users.users.me = {
isSystemUser = true;
group = "me";
extraGroups = [ "sudoers" ];
};
users.groups.me = { };
shb.postgresql.ensures = [
{
username = "me";
database = "me";
}
];
};
users.groups.me = {};
shb.postgresql.ensures = [
{
username = "me";
database = "me";
}
];
};
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
testScript = { nodes, ... }: ''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
def peer_cmd(user, database):
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
def peer_cmd(user, database):
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
def tcpip_cmd(user, database, port):
return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port)
def tcpip_cmd(user, database, port):
return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port)
with subtest("can login with provisioned user and database"):
machine.succeed(peer_cmd("me", "me"), timeout=10)
with subtest("can login with provisioned user and database"):
machine.succeed(peer_cmd("me", "me"), timeout=10)
with subtest("cannot login with unknown user"):
machine.fail(peer_cmd("notme", "me"), timeout=10)
with subtest("cannot login with unknown user"):
machine.fail(peer_cmd("notme", "me"), timeout=10)
with subtest("cannot login to unknown database"):
machine.fail(peer_cmd("me", "notmine"), timeout=10)
with subtest("cannot login to unknown database"):
machine.fail(peer_cmd("me", "notmine"), timeout=10)
with subtest("cannot login with tcpip"):
machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10)
'';
with subtest("cannot login with tcpip"):
machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10)
'';
};
tcpIPWithoutPasswordAuth = lib.shb.runNixOSTest {
name = "postgresql-tcpIpWithoutPasswordAuth";
nodes.machine = { config, pkgs, ... }: {
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/postgresql.nix
];
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/postgresql.nix
];
shb.postgresql.enableTCPIP = true;
shb.postgresql.ensures = [
{
username = "me";
database = "me";
}
];
};
shb.postgresql.enableTCPIP = true;
shb.postgresql.ensures = [
{
username = "me";
database = "me";
}
];
};
testScript = { nodes, ... }: ''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
def peer_cmd(user, database):
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
def peer_cmd(user, database):
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
def tcpip_cmd(user, database, port):
return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port)
def tcpip_cmd(user, database, port):
return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port)
with subtest("cannot login without existing user"):
machine.fail(peer_cmd("me", "me"), timeout=10)
with subtest("cannot login without existing user"):
machine.fail(peer_cmd("me", "me"), timeout=10)
with subtest("cannot login with user without password"):
machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10)
'';
with subtest("cannot login with user without password"):
machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10)
'';
};
tcpIPPasswordAuth = lib.shb.runNixOSTest {
name = "postgresql-tcpIPPasswordAuth";
nodes.machine = { config, pkgs, ... }: {
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/postgresql.nix
];
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/postgresql.nix
];
users.users.me = {
isSystemUser = true;
group = "me";
extraGroups = [ "sudoers" ];
users.users.me = {
isSystemUser = true;
group = "me";
extraGroups = [ "sudoers" ];
};
users.groups.me = { };
system.activationScripts.secret = ''
echo secretpw > /run/dbsecret
'';
shb.postgresql.enableTCPIP = true;
shb.postgresql.ensures = [
{
username = "me";
database = "me";
passwordFile = "/run/dbsecret";
}
];
};
users.groups.me = {};
system.activationScripts.secret = ''
echo secretpw > /run/dbsecret
testScript =
{ nodes, ... }:
''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
def peer_cmd(user, database):
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
def tcpip_cmd(user, database, port, password):
return "PGPASSWORD={password} psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port, password=password)
with subtest("can peer login with provisioned user and database"):
machine.succeed(peer_cmd("me", "me"), timeout=10)
with subtest("can tcpip login with provisioned user and database"):
machine.succeed(tcpip_cmd("me", "me", "5432", "secretpw"), timeout=10)
with subtest("cannot tcpip login with wrong password"):
machine.fail(tcpip_cmd("me", "me", "5432", "oops"), timeout=10)
'';
shb.postgresql.enableTCPIP = true;
shb.postgresql.ensures = [
{
username = "me";
database = "me";
passwordFile = "/run/dbsecret";
}
];
};
testScript = { nodes, ... }: ''
start_all()
machine.wait_for_unit("postgresql.service")
machine.wait_for_open_port(5432)
def peer_cmd(user, database):
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
def tcpip_cmd(user, database, port, password):
return "PGPASSWORD={password} psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port, password=password)
with subtest("can peer login with provisioned user and database"):
machine.succeed(peer_cmd("me", "me"), timeout=10)
with subtest("can tcpip login with provisioned user and database"):
machine.succeed(tcpip_cmd("me", "me", "5432", "secretpw"), timeout=10)
with subtest("cannot tcpip login with wrong password"):
machine.fail(tcpip_cmd("me", "me", "5432", "oops"), timeout=10)
'';
};
}

View file

@ -1,179 +1,188 @@
{ pkgs, lib, ... }:
let
testLib = pkgs.callPackage ../common.nix {};
testLib = pkgs.callPackage ../common.nix { };
commonTest = user: lib.shb.runNixOSTest {
name = "restic_backupAndRestore_${user}";
commonTest =
user:
lib.shb.runNixOSTest {
name = "restic_backupAndRestore_${user}";
nodes.machine = { config, ... }: {
imports = [
testLib.baseImports
nodes.machine =
{ config, ... }:
{
imports = [
testLib.baseImports
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/restic.nix
];
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/restic.nix
];
shb.hardcodedsecret.A = {
request = {
owner = "root";
group = "keys";
mode = "0440";
};
settings.content = "secretA";
};
shb.hardcodedsecret.B = {
request = {
owner = "root";
group = "keys";
mode = "0440";
};
settings.content = "secretB";
};
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."testinstance".settings.passphrase.request;
settings.content = "secretB";
};
shb.restic.instances."testinstance" = {
settings = {
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = "/opt/repos/A";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
shb.hardcodedsecret.A = {
request = {
owner = "root";
group = "keys";
mode = "0440";
};
# Those are not needed by the repository but are still included
# so we can test them in the hooks section.
secrets = {
A.source = config.shb.hardcodedsecret.A.result.path;
B.source = config.shb.hardcodedsecret.B.result.path;
settings.content = "secretA";
};
shb.hardcodedsecret.B = {
request = {
owner = "root";
group = "keys";
mode = "0440";
};
settings.content = "secretB";
};
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."testinstance".settings.passphrase.request;
settings.content = "secretB";
};
shb.restic.instances."testinstance" = {
settings = {
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = "/opt/repos/A";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
# Those are not needed by the repository but are still included
# so we can test them in the hooks section.
secrets = {
A.source = config.shb.hardcodedsecret.A.result.path;
B.source = config.shb.hardcodedsecret.B.result.path;
};
};
};
request = {
inherit user;
sourceDirectories = [
"/opt/files/A"
"/opt/files/B"
];
hooks.beforeBackup = [
''
echo $RUNTIME_DIRECTORY
if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then
if ! [ -f /run/secrets_restic/restic-backups-testinstance_opt_repos_A ]; then
exit 10
fi
if [ -z "$A" ] || ! [ "$A" = "secretA" ]; then
echo "A:$A"
exit 11
fi
if [ -z "$B" ] || ! [ "$B" = "secretB" ]; then
echo "B:$B"
exit 12
fi
fi
''
];
};
};
};
request = {
inherit user;
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
sourceDirectories = [
"/opt/files/A"
"/opt/files/B"
];
testScript =
{ nodes, ... }:
let
provider = nodes.machine.shb.restic.instances."testinstance";
backupService = provider.result.backupService;
restoreScript = provider.result.restoreScript;
in
''
from dictdiffer import diff
def list_files(dir):
files_and_content = {}
files = machine.succeed(f"""
find {dir} -type f
""").split("\n")[:-1]
for f in files:
content = machine.succeed(f"""
cat {f}
""").strip()
files_and_content[f] = content
return files_and_content
def assert_files(dir, files):
result = list(diff(list_files(dir), files))
if len(result) > 0:
raise Exception("Unexpected files:", result)
with subtest("Create initial content"):
machine.succeed("""
mkdir -p /opt/files/A
mkdir -p /opt/files/B
echo repoA_fileA_1 > /opt/files/A/fileA
echo repoA_fileB_1 > /opt/files/A/fileB
echo repoB_fileA_1 > /opt/files/B/fileA
echo repoB_fileB_1 > /opt/files/B/fileB
chown ${user}: -R /opt/files
chmod go-rwx -R /opt/files
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_1',
'/opt/files/B/fileB': 'repoB_fileB_1',
'/opt/files/A/fileA': 'repoA_fileA_1',
'/opt/files/A/fileB': 'repoA_fileB_1',
})
with subtest("First backup in repo A"):
machine.succeed("systemctl start ${backupService}")
with subtest("New content"):
machine.succeed("""
echo repoA_fileA_2 > /opt/files/A/fileA
echo repoA_fileB_2 > /opt/files/A/fileB
echo repoB_fileA_2 > /opt/files/B/fileA
echo repoB_fileB_2 > /opt/files/B/fileB
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_2',
'/opt/files/B/fileB': 'repoB_fileB_2',
'/opt/files/A/fileA': 'repoA_fileA_2',
'/opt/files/A/fileB': 'repoA_fileB_2',
})
with subtest("Delete content"):
machine.succeed("""
rm -r /opt/files/A /opt/files/B
""")
assert_files("/opt/files", {})
with subtest("Restore initial content from repo A"):
machine.succeed("""
${restoreScript} restore latest
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_1',
'/opt/files/B/fileB': 'repoB_fileB_1',
'/opt/files/A/fileA': 'repoA_fileA_1',
'/opt/files/A/fileB': 'repoA_fileB_1',
})
'';
hooks.beforeBackup = [''
echo $RUNTIME_DIRECTORY
if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then
if ! [ -f /run/secrets_restic/restic-backups-testinstance_opt_repos_A ]; then
exit 10
fi
if [ -z "$A" ] || ! [ "$A" = "secretA" ]; then
echo "A:$A"
exit 11
fi
if [ -z "$B" ] || ! [ "$B" = "secretB" ]; then
echo "B:$B"
exit 12
fi
fi
''];
};
};
};
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
testScript = { nodes, ... }: let
provider = nodes.machine.shb.restic.instances."testinstance";
backupService = provider.result.backupService;
restoreScript = provider.result.restoreScript;
in ''
from dictdiffer import diff
def list_files(dir):
files_and_content = {}
files = machine.succeed(f"""
find {dir} -type f
""").split("\n")[:-1]
for f in files:
content = machine.succeed(f"""
cat {f}
""").strip()
files_and_content[f] = content
return files_and_content
def assert_files(dir, files):
result = list(diff(list_files(dir), files))
if len(result) > 0:
raise Exception("Unexpected files:", result)
with subtest("Create initial content"):
machine.succeed("""
mkdir -p /opt/files/A
mkdir -p /opt/files/B
echo repoA_fileA_1 > /opt/files/A/fileA
echo repoA_fileB_1 > /opt/files/A/fileB
echo repoB_fileA_1 > /opt/files/B/fileA
echo repoB_fileB_1 > /opt/files/B/fileB
chown ${user}: -R /opt/files
chmod go-rwx -R /opt/files
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_1',
'/opt/files/B/fileB': 'repoB_fileB_1',
'/opt/files/A/fileA': 'repoA_fileA_1',
'/opt/files/A/fileB': 'repoA_fileB_1',
})
with subtest("First backup in repo A"):
machine.succeed("systemctl start ${backupService}")
with subtest("New content"):
machine.succeed("""
echo repoA_fileA_2 > /opt/files/A/fileA
echo repoA_fileB_2 > /opt/files/A/fileB
echo repoB_fileA_2 > /opt/files/B/fileA
echo repoB_fileB_2 > /opt/files/B/fileB
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_2',
'/opt/files/B/fileB': 'repoB_fileB_2',
'/opt/files/A/fileA': 'repoA_fileA_2',
'/opt/files/A/fileB': 'repoA_fileB_2',
})
with subtest("Delete content"):
machine.succeed("""
rm -r /opt/files/A /opt/files/B
""")
assert_files("/opt/files", {})
with subtest("Restore initial content from repo A"):
machine.succeed("""
${restoreScript} restore latest
""")
assert_files("/opt/files", {
'/opt/files/B/fileA': 'repoB_fileA_1',
'/opt/files/B/fileB': 'repoB_fileB_1',
'/opt/files/A/fileA': 'repoA_fileA_1',
'/opt/files/A/fileB': 'repoA_fileB_1',
})
'';
};
in
{
backupAndRestoreRoot = commonTest "root";

View file

@ -6,95 +6,100 @@ in
test = lib.shb.runNixOSTest {
name = "ssl-test";
nodes.server = { config, pkgs, ... }: {
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/ssl.nix
];
nodes.server =
{ config, pkgs, ... }:
{
imports = [
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
../../modules/blocks/ssl.nix
];
users.users = {
user1 = {
group = "group1";
isSystemUser = true;
};
user2 = {
group = "group2";
isSystemUser = true;
};
};
users.groups = {
group1 = {};
group2 = {};
};
shb.certs = {
cas.selfsigned = {
myca = {
name = "My CA";
users.users = {
user1 = {
group = "group1";
isSystemUser = true;
};
myotherca = {
name = "My Other CA";
};
};
certs.selfsigned = {
top = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "example.com";
group = "nginx";
};
subdomain = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "subdomain.example.com";
group = "nginx";
};
multi = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "multi1.example.com";
extraDomains = [ "multi2.example.com" "multi3.example.com" ];
group = "nginx";
};
cert1 = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "cert1.example.com";
};
cert2 = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "cert2.example.com";
user2 = {
group = "group2";
isSystemUser = true;
};
};
};
users.groups = {
group1 = { };
group2 = { };
};
# The configuration below is to create a webserver that uses the server certificate.
networking.hosts."127.0.0.1" = [
"example.com"
"subdomain.example.com"
"wrong.example.com"
"multi1.example.com"
"multi2.example.com"
"multi3.example.com"
];
services.nginx.enable = true;
services.nginx.virtualHosts =
let
mkVirtualHost = response: cert: {
onlySSL = true;
sslCertificate = cert.paths.cert;
sslCertificateKey = cert.paths.key;
locations."/".extraConfig = ''
add_header Content-Type text/plain;
return 200 '${response}';
'';
shb.certs = {
cas.selfsigned = {
myca = {
name = "My CA";
};
myotherca = {
name = "My Other CA";
};
};
in
certs.selfsigned = {
top = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "example.com";
group = "nginx";
};
subdomain = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "subdomain.example.com";
group = "nginx";
};
multi = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "multi1.example.com";
extraDomains = [
"multi2.example.com"
"multi3.example.com"
];
group = "nginx";
};
cert1 = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "cert1.example.com";
};
cert2 = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "cert2.example.com";
group = "group2";
};
};
};
# The configuration below is to create a webserver that uses the server certificate.
networking.hosts."127.0.0.1" = [
"example.com"
"subdomain.example.com"
"wrong.example.com"
"multi1.example.com"
"multi2.example.com"
"multi3.example.com"
];
services.nginx.enable = true;
services.nginx.virtualHosts =
let
mkVirtualHost = response: cert: {
onlySSL = true;
sslCertificate = cert.paths.cert;
sslCertificateKey = cert.paths.key;
locations."/".extraConfig = ''
add_header Content-Type text/plain;
return 200 '${response}';
'';
};
in
{
"example.com" = mkVirtualHost "Top domain" config.shb.certs.certs.selfsigned.top;
"subdomain.example.com" = mkVirtualHost "Subdomain" config.shb.certs.certs.selfsigned.subdomain;
@ -102,26 +107,27 @@ in
"multi2.example.com" = mkVirtualHost "multi2" config.shb.certs.certs.selfsigned.multi;
"multi3.example.com" = mkVirtualHost "multi3" config.shb.certs.certs.selfsigned.multi;
};
systemd.services.nginx = {
after = [
config.shb.certs.certs.selfsigned.top.systemdService
config.shb.certs.certs.selfsigned.subdomain.systemdService
config.shb.certs.certs.selfsigned.multi.systemdService
config.shb.certs.certs.selfsigned.cert1.systemdService
config.shb.certs.certs.selfsigned.cert2.systemdService
];
requires = [
config.shb.certs.certs.selfsigned.top.systemdService
config.shb.certs.certs.selfsigned.subdomain.systemdService
config.shb.certs.certs.selfsigned.multi.systemdService
config.shb.certs.certs.selfsigned.cert1.systemdService
config.shb.certs.certs.selfsigned.cert2.systemdService
];
systemd.services.nginx = {
after = [
config.shb.certs.certs.selfsigned.top.systemdService
config.shb.certs.certs.selfsigned.subdomain.systemdService
config.shb.certs.certs.selfsigned.multi.systemdService
config.shb.certs.certs.selfsigned.cert1.systemdService
config.shb.certs.certs.selfsigned.cert2.systemdService
];
requires = [
config.shb.certs.certs.selfsigned.top.systemdService
config.shb.certs.certs.selfsigned.subdomain.systemdService
config.shb.certs.certs.selfsigned.multi.systemdService
config.shb.certs.certs.selfsigned.cert1.systemdService
config.shb.certs.certs.selfsigned.cert2.systemdService
];
};
};
};
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
testScript = { nodes, ... }:
testScript =
{ nodes, ... }:
let
myca = nodes.server.shb.certs.cas.selfsigned.myca;
myotherca = nodes.server.shb.certs.cas.selfsigned.myotherca;
@ -131,7 +137,7 @@ in
cert1 = nodes.server.shb.certs.certs.selfsigned.cert1;
cert2 = nodes.server.shb.certs.certs.selfsigned.cert2;
in
''
''
start_all()
# Make sure certs are generated.
@ -203,6 +209,6 @@ in
server.fail("curl --cacert /etc/static/ssl/certs/ca-bundle.crt --fail-with-body -v https://subdomain.example.com")
server.fail("curl --cacert /etc/static/ssl/certs/ca-certificates.crt --fail-with-body -v https://example.com")
server.fail("curl --cacert /etc/static/ssl/certs/ca-certificates.crt --fail-with-body -v https://subdomain.example.com")
'';
'';
};
}

View file

@ -1,7 +1,14 @@
{ pkgs, lib }:
let
inherit (lib) hasAttr mkOption optionalString;
inherit (lib.types) bool enum listOf nullOr submodule str;
inherit (lib.types)
bool
enum
listOf
nullOr
submodule
str
;
baseImports = {
imports = [
@ -10,15 +17,17 @@ let
];
};
accessScript = lib.makeOverridable ({
hasSSL
, waitForServices ? s: []
, waitForPorts ? p: []
, waitForUnixSocket ? u: []
, waitForUrls ? u: []
, extraScript ? {...}: ""
, redirectSSO ? false
}: { nodes, ... }:
accessScript = lib.makeOverridable (
{
hasSSL,
waitForServices ? s: [ ],
waitForPorts ? p: [ ],
waitForUnixSocket ? u: [ ],
waitForUrls ? u: [ ],
extraScript ? { ... }: "",
redirectSSO ? false,
}:
{ nodes, ... }:
let
cfg = nodes.server.test;
@ -35,34 +44,34 @@ let
lldapEnabled = (hasAttr "lldap" nodes.server.shb) && nodes.server.shb.lldap.enable;
in
''
import json
import os
import pathlib
import json
import os
import pathlib
start_all()
start_all()
def curl(target, format, endpoint, data="", extra=""):
cmd = ("curl --show-error --location"
+ " --cookie-jar cookie.txt"
+ " --cookie cookie.txt"
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
# Client must be able to resolve talking to auth server
+ " --connect-to auth.${cfg.domain}:443:server:443"
+ (f" --data '{data}'" if data != "" else "")
+ (f" --silent --output /dev/null --write-out '{format}'" if format != "" else "")
+ (f" {extra}" if extra != "" else "")
+ f" {endpoint}")
print(cmd)
_, r = target.execute(cmd)
print(r)
try:
return json.loads(r)
except:
return r
def curl(target, format, endpoint, data="", extra=""):
cmd = ("curl --show-error --location"
+ " --cookie-jar cookie.txt"
+ " --cookie cookie.txt"
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
# Client must be able to resolve talking to auth server
+ " --connect-to auth.${cfg.domain}:443:server:443"
+ (f" --data '{data}'" if data != "" else "")
+ (f" --silent --output /dev/null --write-out '{format}'" if format != "" else "")
+ (f" {extra}" if extra != "" else "")
+ f" {endpoint}")
print(cmd)
_, r = target.execute(cmd)
print(r)
try:
return json.loads(r)
except:
return r
def unline_with(j, s):
return j.join((x.strip() for x in s.split("\n")))
def unline_with(j, s):
return j.join((x.strip() for x in s.split("\n")))
''
+ lib.strings.concatMapStrings (s: ''server.wait_for_unit("${s}")'' + "\n") (
waitForServices args
@ -72,19 +81,25 @@ let
+ lib.strings.concatMapStrings (p: ''server.wait_for_open_port(${toString p})'' + "\n") (
waitForPorts args
# TODO: when the SSO block exists, replace this hardcoded port.
++ (lib.optionals autheliaEnabled [ 9091 /* nodes.server.services.authelia.instances."auth.${domain}".settings.server.port */ ])
++ (lib.optionals autheliaEnabled [
9091 # nodes.server.services.authelia.instances."auth.${domain}".settings.server.port
])
)
+ lib.strings.concatMapStrings (u: ''server.wait_for_open_unix_socket("${u}")'' + "\n") (
waitForUnixSocket args
)
+ lib.strings.concatMapStrings (u: ''server.wait_for_open_unix_socket("${u}")'' + "\n") (waitForUnixSocket args)
+ ''
if ${if hasSSL args then "True" else "False"}:
server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
client.succeed("rm -r /etc/ssl/certs")
client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
if ${if hasSSL args then "True" else "False"}:
server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
client.succeed("rm -r /etc/ssl/certs")
client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
''
# Making a curl request to an URL needs to happen after we copied the certificates over,
# otherwise curl will not be able to verify the "legitimacy of the server".
+ lib.strings.concatMapStrings (u: ''
+ lib.strings.concatMapStrings (
u:
''
import time
done = False
@ -97,416 +112,472 @@ let
done = response.get('code') == 200
if not done:
raise Exception(f"Response was never 200, got last: {response}")
'' + "\n") (
waitForUrls args
''
+ "\n"
) (waitForUrls args)
+ (
if (!redirectSSO) then
''
with subtest("access"):
response = curl(client, """{"code":%{response_code}}""", "${proto_fqdn}")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
''
else
''
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}")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
if response['auth_host'] != "auth.${cfg.domain}":
raise Exception(f"auth host should be auth.${cfg.domain} but is {response['auth_host']}")
if response['auth_query'] != "rd=${proto_fqdn}/":
raise Exception(f"auth query should be rd=${proto_fqdn}/ but is {response['auth_query']}")
''
)
+ (
let
script = extraScript args;
in
lib.optionalString (script != "") script
)
+ (if (! redirectSSO) then ''
with subtest("access"):
response = curl(client, """{"code":%{response_code}}""", "${proto_fqdn}")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
'' else ''
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}")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
if response['auth_host'] != "auth.${cfg.domain}":
raise Exception(f"auth host should be auth.${cfg.domain} but is {response['auth_host']}")
if response['auth_query'] != "rd=${proto_fqdn}/":
raise Exception(f"auth query should be rd=${proto_fqdn}/ but is {response['auth_query']}")
'')
+ (let
script = extraScript args;
in
lib.optionalString (script != "") script)
+ (optionalString (hasAttr "test" nodes.server && hasAttr "login" nodes.server.test) ''
with subtest("Login from server"):
code, logs = server.execute("login_playwright")
print(logs)
try:
server.copy_from_vm("trace")
except:
print("No trace found on server")
if code != 0:
raise Exception("login_playwright did not succeed")
with subtest("Login from server"):
code, logs = server.execute("login_playwright")
print(logs)
try:
server.copy_from_vm("trace")
except:
print("No trace found on server")
if code != 0:
raise Exception("login_playwright did not succeed")
'')
+ (optionalString (hasAttr "test" nodes.client && hasAttr "login" nodes.client.test) ''
with subtest("Login from client"):
code, logs = client.execute("login_playwright")
print(logs)
try:
client.copy_from_vm("trace")
except:
print("No trace found on client")
if code != 0:
raise Exception("login_playwright did not succeed")
with subtest("Login from client"):
code, logs = client.execute("login_playwright")
print(logs)
try:
client.copy_from_vm("trace")
except:
print("No trace found on client")
if code != 0:
raise Exception("login_playwright did not succeed")
'')
);
backupScript = args: (accessScript args).override {
extraScript = { proto_fqdn, ... }: ''
with subtest("backup"):
server.succeed("systemctl start restic-backups-testinstance_opt_repos_A")
'';
};
backupScript =
args:
(accessScript args).override {
extraScript =
{ proto_fqdn, ... }:
''
with subtest("backup"):
server.succeed("systemctl start restic-backups-testinstance_opt_repos_A")
'';
};
in
{
inherit baseImports accessScript;
runNixOSTest = args: pkgs.testers.runNixOSTest ({
interactive.sshBackdoor.enable = true;
} // args);
runNixOSTest =
args:
pkgs.testers.runNixOSTest (
{
interactive.sshBackdoor.enable = true;
}
// args
);
mkScripts = args:
mkScripts = args: {
access = accessScript args;
backup = backupScript args;
};
baseModule =
{ config, ... }:
{
access = accessScript args;
backup = backupScript args;
};
baseModule = { config, ... }: {
options.test = {
domain = mkOption {
type = str;
default = "example.com";
options.test = {
domain = mkOption {
type = str;
default = "example.com";
};
subdomain = mkOption {
type = str;
};
fqdn = mkOption {
type = str;
readOnly = true;
default = "${config.test.subdomain}.${config.test.domain}";
};
hasSSL = mkOption {
type = bool;
default = false;
};
proto = mkOption {
type = str;
readOnly = true;
default = if config.test.hasSSL then "https" else "http";
};
proto_fqdn = mkOption {
type = str;
readOnly = true;
default = "${config.test.proto}://${config.test.fqdn}";
};
};
subdomain = mkOption {
type = str;
};
fqdn = mkOption {
type = str;
readOnly = true;
default = "${config.test.subdomain}.${config.test.domain}";
};
hasSSL = mkOption {
type = bool;
default = false;
};
proto = mkOption {
type = str;
readOnly = true;
default = if config.test.hasSSL then "https" else "http";
};
proto_fqdn = mkOption {
type = str;
readOnly = true;
default = "${config.test.proto}://${config.test.fqdn}";
};
};
imports = [
baseImports
../modules/blocks/authelia.nix
../modules/blocks/hardcodedsecret.nix
../modules/blocks/mitmdump.nix
../modules/blocks/nginx.nix
../modules/blocks/postgresql.nix
];
config = {
# HTTP(s) server port.
networking.firewall.allowedTCPPorts = [ 80 443 ];
shb.nginx.accessLog = true;
networking.hosts = {
"192.168.1.2" = [ config.test.fqdn "auth.${config.test.domain}" ];
};
};
};
clientLoginModule = { config, pkgs, ... }: let
cfg = config.test.login;
in {
options.test.login = {
browser = mkOption {
type = enum [ "firefox" "chromium" "webkit" ];
default = "firefox";
};
usernameFieldLabelRegex = mkOption {
type = str;
default = "[Uu]sername";
};
usernameFieldSelector = mkOption {
type = str;
default = "get_by_label(re.compile('${cfg.usernameFieldLabelRegex}'))";
};
passwordFieldLabelRegex = mkOption {
type = str;
default = "[Pp]assword";
};
passwordFieldSelector = mkOption {
type = str;
default = "get_by_label(re.compile('${cfg.passwordFieldLabelRegex}'))";
};
loginButtonNameRegex = mkOption {
type = str;
default = "[Ll]ogin";
};
testLoginWith = mkOption {
type = listOf (submodule {
options = {
username = mkOption {
type = nullOr str;
default = null;
};
password = mkOption {
type = nullOr str;
default = null;
};
nextPageExpect = mkOption {
type = listOf str;
};
};
});
};
startUrl = mkOption {
type = str;
default = "http://${config.test.fqdn}";
};
beforeHook = mkOption {
type = str;
default = "";
};
};
config = {
networking.hosts = {
"192.168.1.2" = [ config.test.fqdn "auth.${config.test.domain}" ];
};
environment.variables = {
PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers;
};
environment.systemPackages = [
(pkgs.writers.writePython3Bin "login_playwright"
{
libraries = [ pkgs.python3Packages.playwright ];
flakeIgnore = [ "F401" "E501" ];
}
(let
testCfg = pkgs.writeText "users.json" (builtins.toJSON cfg);
in ''
import json
import re
import sys
from playwright.sync_api import expect
from playwright.sync_api import sync_playwright
browsers = {
"chromium": {'args': ["--headless", "--disable-gpu"], 'channel': 'chromium'},
"firefox": {'args': ["--reporter", "html"]},
"webkit": {},
}
with open("${testCfg}") as f:
testCfg = json.load(f)
browser_name = testCfg['browser']
browser_args = browsers.get(browser_name)
print(f"Running test on {browser_name} {' '.join(browser_args)}")
with sync_playwright() as p:
browser = getattr(p, browser_name).launch(**browser_args)
for i, u in enumerate(testCfg["testLoginWith"]):
print(f"Testing for user {u['username']} and password {u['password']}")
context = browser.new_context(ignore_https_errors=True)
context.set_default_navigation_timeout(2 * 60 * 1000)
context.tracing.start(screenshots=True, snapshots=True, sources=True)
try:
page = context.new_page()
print(f"Going to {testCfg['startUrl']}")
page.goto(testCfg['startUrl'])
if testCfg.get("beforeHook") is not None:
exec(testCfg.get("beforeHook"))
if u['username'] is not None:
print(f"Filling field username with {u['username']}")
page.${cfg.usernameFieldSelector}.fill(u['username'])
if u['password'] is not None:
print(f"Filling field password with {u['password']}")
page.${cfg.passwordFieldSelector}.fill(u['password'])
# Assumes we don't need to login, so skip this.
if u['username'] is not None or u['password'] is not None:
print(f"Clicking button {testCfg['loginButtonNameRegex']}")
page.get_by_role("button", name=re.compile(testCfg['loginButtonNameRegex'])).click()
for line in u['nextPageExpect']:
print(f"Running: {line}")
print(f"Page has title: {page.title()}")
exec(line)
finally:
print(f'Saving trace at trace/{i}.zip')
context.tracing.stop(path=f"trace/{i}.zip")
browser.close()
'')
)
imports = [
baseImports
../modules/blocks/authelia.nix
../modules/blocks/hardcodedsecret.nix
../modules/blocks/mitmdump.nix
../modules/blocks/nginx.nix
../modules/blocks/postgresql.nix
];
};
};
config = {
# HTTP(s) server port.
networking.firewall.allowedTCPPorts = [
80
443
];
shb.nginx.accessLog = true;
backup = backupOption: { config, ... }: {
imports = [
../modules/blocks/restic.nix
];
shb.restic.instances."testinstance" = {
request = backupOption.request;
settings = {
enable = true;
passphrase.result = config.shb.hardcodedsecret.backupPassphrase.result;
repository = {
path = "/opt/repos/A";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
networking.hosts = {
"192.168.1.2" = [
config.test.fqdn
"auth.${config.test.domain}"
];
};
};
};
clientLoginModule =
{ config, pkgs, ... }:
let
cfg = config.test.login;
in
{
options.test.login = {
browser = mkOption {
type = enum [
"firefox"
"chromium"
"webkit"
];
default = "firefox";
};
usernameFieldLabelRegex = mkOption {
type = str;
default = "[Uu]sername";
};
usernameFieldSelector = mkOption {
type = str;
default = "get_by_label(re.compile('${cfg.usernameFieldLabelRegex}'))";
};
passwordFieldLabelRegex = mkOption {
type = str;
default = "[Pp]assword";
};
passwordFieldSelector = mkOption {
type = str;
default = "get_by_label(re.compile('${cfg.passwordFieldLabelRegex}'))";
};
loginButtonNameRegex = mkOption {
type = str;
default = "[Ll]ogin";
};
testLoginWith = mkOption {
type = listOf (submodule {
options = {
username = mkOption {
type = nullOr str;
default = null;
};
password = mkOption {
type = nullOr str;
default = null;
};
nextPageExpect = mkOption {
type = listOf str;
};
};
});
};
startUrl = mkOption {
type = str;
default = "http://${config.test.fqdn}";
};
beforeHook = mkOption {
type = str;
default = "";
};
};
config = {
networking.hosts = {
"192.168.1.2" = [
config.test.fqdn
"auth.${config.test.domain}"
];
};
environment.variables = {
PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers;
};
environment.systemPackages = [
(pkgs.writers.writePython3Bin "login_playwright"
{
libraries = [ pkgs.python3Packages.playwright ];
flakeIgnore = [
"F401"
"E501"
];
}
(
let
testCfg = pkgs.writeText "users.json" (builtins.toJSON cfg);
in
''
import json
import re
import sys
from playwright.sync_api import expect
from playwright.sync_api import sync_playwright
browsers = {
"chromium": {'args': ["--headless", "--disable-gpu"], 'channel': 'chromium'},
"firefox": {'args': ["--reporter", "html"]},
"webkit": {},
}
with open("${testCfg}") as f:
testCfg = json.load(f)
browser_name = testCfg['browser']
browser_args = browsers.get(browser_name)
print(f"Running test on {browser_name} {' '.join(browser_args)}")
with sync_playwright() as p:
browser = getattr(p, browser_name).launch(**browser_args)
for i, u in enumerate(testCfg["testLoginWith"]):
print(f"Testing for user {u['username']} and password {u['password']}")
context = browser.new_context(ignore_https_errors=True)
context.set_default_navigation_timeout(2 * 60 * 1000)
context.tracing.start(screenshots=True, snapshots=True, sources=True)
try:
page = context.new_page()
print(f"Going to {testCfg['startUrl']}")
page.goto(testCfg['startUrl'])
if testCfg.get("beforeHook") is not None:
exec(testCfg.get("beforeHook"))
if u['username'] is not None:
print(f"Filling field username with {u['username']}")
page.${cfg.usernameFieldSelector}.fill(u['username'])
if u['password'] is not None:
print(f"Filling field password with {u['password']}")
page.${cfg.passwordFieldSelector}.fill(u['password'])
# Assumes we don't need to login, so skip this.
if u['username'] is not None or u['password'] is not None:
print(f"Clicking button {testCfg['loginButtonNameRegex']}")
page.get_by_role("button", name=re.compile(testCfg['loginButtonNameRegex'])).click()
for line in u['nextPageExpect']:
print(f"Running: {line}")
print(f"Page has title: {page.title()}")
exec(line)
finally:
print(f'Saving trace at trace/{i}.zip')
context.tracing.stop(path=f"trace/{i}.zip")
browser.close()
''
)
)
];
};
};
backup =
backupOption:
{ config, ... }:
{
imports = [
../modules/blocks/restic.nix
];
shb.restic.instances."testinstance" = {
request = backupOption.request;
settings = {
enable = true;
passphrase.result = config.shb.hardcodedsecret.backupPassphrase.result;
repository = {
path = "/opt/repos/A";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
};
};
};
};
shb.hardcodedsecret.backupPassphrase = {
request = config.shb.restic.instances."testinstance".settings.passphrase.request;
settings.content = "PassPhrase";
};
};
certs = { config, ... }: {
imports = [
../modules/blocks/ssl.nix
];
shb.certs = {
cas.selfsigned.myca = {
name = "My CA";
shb.hardcodedsecret.backupPassphrase = {
request = config.shb.restic.instances."testinstance".settings.passphrase.request;
settings.content = "PassPhrase";
};
certs.selfsigned = {
n = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "*.${config.test.domain}";
group = "nginx";
};
certs =
{ config, ... }:
{
imports = [
../modules/blocks/ssl.nix
];
shb.certs = {
cas.selfsigned.myca = {
name = "My CA";
};
certs.selfsigned = {
n = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "*.${config.test.domain}";
group = "nginx";
};
};
};
systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ];
systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
};
ldap =
{ config, pkgs, ... }:
{
imports = [
../modules/blocks/lldap.nix
];
networking.hosts = {
"127.0.0.1" = [ "ldap.${config.test.domain}" ];
};
shb.hardcodedsecret.ldapUserPassword = {
request = config.shb.lldap.ldapUserPassword.request;
settings.content = "ldapUserPassword";
};
shb.hardcodedsecret.jwtSecret = {
request = config.shb.lldap.jwtSecret.request;
settings.content = "jwtSecrets";
};
shb.lldap = {
enable = true;
inherit (config.test) domain;
subdomain = "ldap";
ldapPort = 3890;
webUIListenPort = 17170;
dcdomain = "dc=example,dc=com";
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
debug = false; # Enable this if needed, but beware it is _very_ verbose.
ensureUsers = {
alice = {
email = "alice@example.com";
groups = [ "user_group" ];
password.result.path = pkgs.writeText "alicePassword" "AlicePassword";
};
bob = {
email = "bob@example.com";
groups = [
"user_group"
"admin_group"
];
password.result.path = pkgs.writeText "bobPassword" "BobPassword";
};
charlie = {
email = "charlie@example.com";
groups = [ "other_group" ];
password.result.path = pkgs.writeText "charliePassword" "CharliePassword";
};
};
ensureGroups = {
user_group = { };
admin_group = { };
other_group = { };
};
};
};
systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ];
systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
};
sso =
ssl:
{ config, pkgs, ... }:
{
imports = [
../modules/blocks/authelia.nix
];
ldap = { config, pkgs, ... }: {
imports = [
../modules/blocks/lldap.nix
];
networking.hosts = {
"127.0.0.1" = [ "${config.shb.authelia.subdomain}.${config.shb.authelia.domain}" ];
};
networking.hosts = {
"127.0.0.1" = [ "ldap.${config.test.domain}" ];
};
shb.authelia = {
enable = true;
inherit (config.test) domain;
subdomain = "auth";
ssl = config.shb.certs.certs.selfsigned.n;
debug = true;
shb.hardcodedsecret.ldapUserPassword = {
request = config.shb.lldap.ldapUserPassword.request;
settings.content = "ldapUserPassword";
};
shb.hardcodedsecret.jwtSecret = {
request = config.shb.lldap.jwtSecret.request;
settings.content = "jwtSecrets";
};
ldapHostname = "127.0.0.1";
ldapPort = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
shb.lldap = {
enable = true;
inherit (config.test) domain;
subdomain = "ldap";
ldapPort = 3890;
webUIListenPort = 17170;
dcdomain = "dc=example,dc=com";
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
debug = false; # Enable this if needed, but beware it is _very_ verbose.
ensureUsers = {
alice = {
email = "alice@example.com";
groups = [ "user_group" ];
password.result.path = pkgs.writeText "alicePassword" "AlicePassword";
};
bob = {
email = "bob@example.com";
groups = [ "user_group" "admin_group" ];
password.result.path = pkgs.writeText "bobPassword" "BobPassword";
};
charlie = {
email = "charlie@example.com";
groups = [ "other_group" ];
password.result.path = pkgs.writeText "charliePassword" "CharliePassword";
secrets = {
jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result;
ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
identityProvidersOIDCHMACSecret.result =
config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result =
config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
};
};
ensureGroups = {
user_group = {};
admin_group = {};
other_group = {};
shb.hardcodedsecret.autheliaJwtSecret = {
request = config.shb.authelia.secrets.jwtSecret.request;
settings.content = "jwtSecret";
};
shb.hardcodedsecret.ldapAdminPassword = {
request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.content = "ldapUserPassword";
};
shb.hardcodedsecret.sessionSecret = {
request = config.shb.authelia.secrets.sessionSecret.request;
settings.content = "sessionSecret";
};
shb.hardcodedsecret.storageEncryptionKey = {
request = config.shb.authelia.secrets.storageEncryptionKey.request;
settings.content = "storageEncryptionKey";
};
shb.hardcodedsecret.identityProvidersOIDCHMACSecret = {
request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
settings.content = "identityProvidersOIDCHMACSecret";
};
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = {
request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
settings.source =
(pkgs.runCommand "gen-private-key" { } ''
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'')
+ "/private.pem";
};
};
};
sso = ssl: { config, pkgs, ... }: {
imports = [
../modules/blocks/authelia.nix
];
networking.hosts = {
"127.0.0.1" = [ "${config.shb.authelia.subdomain}.${config.shb.authelia.domain}" ];
};
shb.authelia = {
enable = true;
inherit (config.test) domain;
subdomain = "auth";
ssl = config.shb.certs.certs.selfsigned.n;
debug = true;
ldapHostname = "127.0.0.1";
ldapPort = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
secrets = {
jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result;
ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result;
};
};
shb.hardcodedsecret.autheliaJwtSecret = {
request = config.shb.authelia.secrets.jwtSecret.request;
settings.content = "jwtSecret";
};
shb.hardcodedsecret.ldapAdminPassword = {
request = config.shb.authelia.secrets.ldapAdminPassword.request;
settings.content = "ldapUserPassword";
};
shb.hardcodedsecret.sessionSecret = {
request = config.shb.authelia.secrets.sessionSecret.request;
settings.content = "sessionSecret";
};
shb.hardcodedsecret.storageEncryptionKey = {
request = config.shb.authelia.secrets.storageEncryptionKey.request;
settings.content = "storageEncryptionKey";
};
shb.hardcodedsecret.identityProvidersOIDCHMACSecret = {
request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
settings.content = "identityProvidersOIDCHMACSecret";
};
shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = {
request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
settings.source = (pkgs.runCommand "gen-private-key" {} ''
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem";
};
};
}

View file

@ -1,57 +1,75 @@
{ pkgs, ... }:
let
contracts = pkgs.callPackage ../../modules/contracts {};
contracts = pkgs.callPackage ../../modules/contracts { };
in
{
restic_root = contracts.test.backup {
name = "restic_root";
username = "root";
providerRoot = [ "shb" "restic" "instances" "mytest" ];
providerRoot = [
"shb"
"restic"
"instances"
"mytest"
];
modules = [
../../modules/blocks/restic.nix
../../modules/blocks/hardcodedsecret.nix
];
settings = { repository, config, ... }: {
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
settings =
{ repository, config, ... }:
{
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
};
};
};
};
extraConfig = { username, config, ... }: {
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."mytest".settings.passphrase.request;
settings.content = "passphrase";
extraConfig =
{ username, config, ... }:
{
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."mytest".settings.passphrase.request;
settings.content = "passphrase";
};
};
};
};
restic_nonroot = contracts.test.backup {
name = "restic_nonroot";
username = "me";
providerRoot = [ "shb" "restic" "instances" "mytest" ];
providerRoot = [
"shb"
"restic"
"instances"
"mytest"
];
modules = [
../../modules/blocks/restic.nix
../../modules/blocks/hardcodedsecret.nix
];
settings = { repository, config, ... }: {
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
settings =
{ repository, config, ... }:
{
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
};
};
};
};
extraConfig = { username, config, ... }: {
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."mytest".settings.passphrase.request;
settings.content = "passphrase";
extraConfig =
{ username, config, ... }:
{
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.instances."mytest".settings.passphrase.request;
settings.content = "passphrase";
};
};
};
};
}

View file

@ -1,38 +1,51 @@
{ pkgs, ... }:
let
contracts = pkgs.callPackage ../../modules/contracts {};
contracts = pkgs.callPackage ../../modules/contracts { };
in
{
restic_postgres = contracts.test.databasebackup {
name = "restic_postgres";
requesterRoot = [ "shb" "postgresql" "databasebackup" ];
providerRoot = [ "shb" "restic" "databases" "postgresql" ];
requesterRoot = [
"shb"
"postgresql"
"databasebackup"
];
providerRoot = [
"shb"
"restic"
"databases"
"postgresql"
];
modules = [
../../modules/blocks/postgresql.nix
../../modules/blocks/restic.nix
../../modules/blocks/hardcodedsecret.nix
];
settings = { repository, config, ... }: {
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
settings =
{ repository, config, ... }:
{
enable = true;
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
repository = {
path = repository;
timerConfig = {
OnCalendar = "00:00:00";
};
};
};
};
extraConfig = { config, database, ... }: {
shb.postgresql.ensures = [
{
inherit database;
username = database;
}
];
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.databases.postgresql.settings.passphrase.request;
settings.content = "passphrase";
extraConfig =
{ config, database, ... }:
{
shb.postgresql.ensures = [
{
inherit database;
username = database;
}
];
shb.hardcodedsecret.passphrase = {
request = config.shb.restic.databases.postgresql.settings.passphrase.request;
settings.content = "passphrase";
};
};
};
};
}

View file

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

View file

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

View file

@ -17,11 +17,12 @@ in
c = "%SECRET_${root}C%";
};
in
(item "") // {
nestedAttr = item "NESTEDATTR_";
nestedList = [ (item "NESTEDLIST_0_") ];
doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ];
};
(item "")
// {
nestedAttr = item "NESTEDATTR_";
nestedList = [ (item "NESTEDLIST_0_") ];
doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ];
};
expr =
let
item = {
@ -33,13 +34,14 @@ in
c.other = "other";
};
in
lib.shb.withReplacements (
item // {
nestedAttr = item;
nestedList = [ item ];
doubleNestedList = [ { n = item; } ];
}
);
lib.shb.withReplacements (
item
// {
nestedAttr = item;
nestedList = [ item ];
doubleNestedList = [ { n = item; } ];
}
);
};
testLibWithReplacementsRootList = {
@ -51,12 +53,12 @@ in
c = "%SECRET_${root}C%";
};
in
[
(item "0_")
(item "1_")
[ (item "2_0_") ]
[ { n = (item "3_0_N_"); } ]
];
[
(item "0_")
(item "1_")
[ (item "2_0_") ]
[ { n = (item "3_0_N_"); } ]
];
expr =
let
item = {
@ -68,12 +70,12 @@ in
c.other = "other";
};
in
lib.shb.withReplacements [
item
item
[ item ]
[ { n = item; } ]
];
lib.shb.withReplacements [
item
item
[ item ]
[ { n = item; } ]
];
};
testLibGetReplacements = {
@ -84,10 +86,10 @@ in
(nameValuePair "%SECRET_${root}C%" "prefix-$(cat /path/C)-suffix")
];
in
(secrets "") ++
(secrets "DOUBLENESTEDLIST_0_N_") ++
(secrets "NESTEDATTR_") ++
(secrets "NESTEDLIST_0_");
(secrets "")
++ (secrets "DOUBLENESTEDLIST_0_N_")
++ (secrets "NESTEDATTR_")
++ (secrets "NESTEDLIST_0_");
expr =
let
item = {
@ -99,13 +101,16 @@ in
c.other = "other";
};
in
map lib.shb.genReplacement (lib.shb.getReplacements (
item // {
map lib.shb.genReplacement (
lib.shb.getReplacements (
item
// {
nestedAttr = item;
nestedList = [ item ];
doubleNestedList = [ { n = item; } ];
}
));
)
);
};
testParseXML = {
@ -119,10 +124,10 @@ in
};
expr = lib.shb.parseXML ''
<a>
<b>1</b>
<c><d>1</d></c>
</a>
<a>
<b>1</b>
<c><d>1</d></c>
</a>
'';
};
}

View file

@ -4,163 +4,206 @@ let
loginUrl = "/UI/Login";
# TODO: Test login
commonTestScript = appname: cfgPathFn: lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.arr.${appname}.ssl);
waitForServices = { ... }: [
"${appname}.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
node.config.shb.arr.${appname}.settings.Port
];
extraScript = { node, fqdn, proto_fqdn, ... }: let
shbapp = node.config.shb.arr.${appname};
cfgPath = cfgPathFn shbapp;
apiKey = if (shbapp.settings ? ApiKey) then "01234567890123456789" else null;
in ''
# These curl requests still return a 200 even with sso redirect.
with subtest("health"):
response = curl(client, """{"code":%{response_code}}""", "${fqdn}${healthUrl}")
print("response =", response)
commonTestScript =
appname: cfgPathFn:
lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.arr.${appname}.ssl);
waitForServices =
{ ... }:
[
"${appname}.service"
"nginx.service"
];
waitForPorts =
{ node, ... }:
[
node.config.shb.arr.${appname}.settings.Port
];
extraScript =
{
node,
fqdn,
proto_fqdn,
...
}:
let
shbapp = node.config.shb.arr.${appname};
cfgPath = cfgPathFn shbapp;
apiKey = if (shbapp.settings ? ApiKey) then "01234567890123456789" else null;
in
''
# These curl requests still return a 200 even with sso redirect.
with subtest("health"):
response = curl(client, """{"code":%{response_code}}""", "${fqdn}${healthUrl}")
print("response =", response)
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
with subtest("login"):
response = curl(client, """{"code":%{response_code}}""", "${fqdn}${loginUrl}")
with subtest("login"):
response = curl(client, """{"code":%{response_code}}""", "${fqdn}${loginUrl}")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
'' + lib.optionalString (apiKey != null) ''
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
''
+ lib.optionalString (apiKey != null) ''
with subtest("apikey"):
config = server.succeed("cat ${cfgPath}")
if "${apiKey}" not in config:
raise Exception(f"Unexpected API Key. Want '${apiKey}', got '{config}'")
'';
};
basic = appname: { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/services/arr.nix
];
test = {
subdomain = appname;
with subtest("apikey"):
config = server.succeed("cat ${cfgPath}")
if "${apiKey}" not in config:
raise Exception(f"Unexpected API Key. Want '${apiKey}', got '{config}'")
'';
};
shb.arr.${appname} = {
enable = true;
inherit (config.test) subdomain domain;
settings.ApiKey.source = pkgs.writeText "APIKey" "01234567890123456789"; # Needs to be >=20 characters.
};
};
clientLogin = appname: { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = appname;
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "[Uu]sername";
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{ nextPageExpect = [
"expect(page).to_have_title(re.compile('${appname}', re.IGNORECASE))"
]; }
];
};
};
basicTest = appname: cfgPathFn: lib.shb.runNixOSTest {
name = "arr_${appname}_basic";
nodes.client = {
basic =
appname:
{ config, ... }:
{
imports = [
(clientLogin appname)
lib.shb.baseModule
../../modules/services/arr.nix
];
test = {
subdomain = appname;
};
shb.arr.${appname} = {
enable = true;
inherit (config.test) subdomain domain;
settings.ApiKey.source = pkgs.writeText "APIKey" "01234567890123456789"; # Needs to be >=20 characters.
};
};
nodes.server = {
clientLogin =
appname:
{ config, ... }:
{
imports = [
(basic appname)
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = appname;
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "[Uu]sername";
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{
nextPageExpect = [
"expect(page).to_have_title(re.compile('${appname}', re.IGNORECASE))"
];
}
];
};
};
testScript = (commonTestScript appname cfgPathFn).access;
};
basicTest =
appname: cfgPathFn:
lib.shb.runNixOSTest {
name = "arr_${appname}_basic";
backupTest = appname: cfgPathFn: lib.shb.runNixOSTest {
name = "arr_${appname}_backup";
nodes.client = {
imports = [
(clientLogin appname)
];
};
nodes.server = {
imports = [
(basic appname)
];
};
nodes.server = { config, ... }: {
imports = [
(basic appname)
(lib.shb.backup config.shb.arr.${appname}.backup)
];
testScript = (commonTestScript appname cfgPathFn).access;
};
nodes.client = {};
backupTest =
appname: cfgPathFn:
lib.shb.runNixOSTest {
name = "arr_${appname}_backup";
testScript = (commonTestScript appname cfgPathFn).backup;
};
nodes.server =
{ config, ... }:
{
imports = [
(basic appname)
(lib.shb.backup config.shb.arr.${appname}.backup)
];
};
https = appname: { config, ...}: {
shb.arr.${appname} = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
nodes.client = { };
httpsTest = appname: cfgPathFn: lib.shb.runNixOSTest {
name = "arr_${appname}_https";
nodes.server = { config, pkgs, ... }: {
imports = [
(basic appname)
lib.shb.certs
(https appname)
];
testScript = (commonTestScript appname cfgPathFn).backup;
};
nodes.client = {};
testScript = (commonTestScript appname cfgPathFn).access;
};
sso = appname: { config, ...}: {
shb.arr.${appname} = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
};
ssoTest = appname: cfgPathFn: lib.shb.runNixOSTest {
name = "arr_${appname}_sso";
nodes.server = { config, pkgs, ... }: {
imports = [
(basic appname)
lib.shb.certs
(https appname)
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
(sso appname)
];
https =
appname:
{ config, ... }:
{
shb.arr.${appname} = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
nodes.client = {};
httpsTest =
appname: cfgPathFn:
lib.shb.runNixOSTest {
name = "arr_${appname}_https";
testScript = (commonTestScript appname cfgPathFn).access.override {
redirectSSO = true;
nodes.server =
{ config, pkgs, ... }:
{
imports = [
(basic appname)
lib.shb.certs
(https appname)
];
};
nodes.client = { };
testScript = (commonTestScript appname cfgPathFn).access;
};
sso =
appname:
{ config, ... }:
{
shb.arr.${appname} = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
};
ssoTest =
appname: cfgPathFn:
lib.shb.runNixOSTest {
name = "arr_${appname}_sso";
nodes.server =
{ config, pkgs, ... }:
{
imports = [
(basic appname)
lib.shb.certs
(https appname)
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
(sso appname)
];
};
nodes.client = { };
testScript = (commonTestScript appname cfgPathFn).access.override {
redirectSSO = true;
};
};
};
radarrCfgFn = cfg: "${cfg.dataDir}/config.xml";
sonarrCfgFn = cfg: "${cfg.dataDir}/config.xml";
@ -170,33 +213,33 @@ let
jackettCfgFn = cfg: "${cfg.dataDir}/ServerConfig.json";
in
{
radarr_basic = basicTest "radarr" radarrCfgFn;
radarr_basic = basicTest "radarr" radarrCfgFn;
radarr_backup = backupTest "radarr" radarrCfgFn;
radarr_https = httpsTest "radarr" radarrCfgFn;
radarr_sso = ssoTest "radarr" radarrCfgFn;
radarr_https = httpsTest "radarr" radarrCfgFn;
radarr_sso = ssoTest "radarr" radarrCfgFn;
sonarr_basic = basicTest "sonarr" sonarrCfgFn;
sonarr_basic = basicTest "sonarr" sonarrCfgFn;
sonarr_backup = backupTest "sonarr" sonarrCfgFn;
sonarr_https = httpsTest "sonarr" sonarrCfgFn;
sonarr_sso = ssoTest "sonarr" sonarrCfgFn;
sonarr_https = httpsTest "sonarr" sonarrCfgFn;
sonarr_sso = ssoTest "sonarr" sonarrCfgFn;
bazarr_basic = basicTest "bazarr" bazarrCfgFn;
bazarr_basic = basicTest "bazarr" bazarrCfgFn;
bazarr_backup = backupTest "bazarr" bazarrCfgFn;
bazarr_https = httpsTest "bazarr" bazarrCfgFn;
bazarr_sso = ssoTest "bazarr" bazarrCfgFn;
bazarr_https = httpsTest "bazarr" bazarrCfgFn;
bazarr_sso = ssoTest "bazarr" bazarrCfgFn;
readarr_basic = basicTest "readarr" readarrCfgFn;
readarr_basic = basicTest "readarr" readarrCfgFn;
readarr_backup = backupTest "readarr" readarrCfgFn;
readarr_https = httpsTest "readarr" readarrCfgFn;
readarr_sso = ssoTest "readarr" readarrCfgFn;
readarr_https = httpsTest "readarr" readarrCfgFn;
readarr_sso = ssoTest "readarr" readarrCfgFn;
lidarr_basic = basicTest "lidarr" lidarrCfgFn;
lidarr_basic = basicTest "lidarr" lidarrCfgFn;
lidarr_backup = backupTest "lidarr" lidarrCfgFn;
lidarr_https = httpsTest "lidarr" lidarrCfgFn;
lidarr_sso = ssoTest "lidarr" lidarrCfgFn;
lidarr_https = httpsTest "lidarr" lidarrCfgFn;
lidarr_sso = ssoTest "lidarr" lidarrCfgFn;
jackett_basic = basicTest "jackett" jackettCfgFn;
jackett_basic = basicTest "jackett" jackettCfgFn;
jackett_backup = backupTest "jackett" jackettCfgFn;
jackett_https = httpsTest "jackett" jackettCfgFn;
jackett_sso = ssoTest "jackett" jackettCfgFn;
jackett_https = httpsTest "jackett" jackettCfgFn;
jackett_sso = ssoTest "jackett" jackettCfgFn;
}

View file

@ -2,89 +2,106 @@
let
commonTestScript = lib.shb.accessScript {
hasSSL = { node, ... }: !(isNull node.config.shb.audiobookshelf.ssl);
waitForServices = { ... }: [
"audiobookshelf.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
node.config.shb.audiobookshelf.webPort
];
waitForServices =
{ ... }:
[
"audiobookshelf.service"
"nginx.service"
];
waitForPorts =
{ node, ... }:
[
node.config.shb.audiobookshelf.webPort
];
# TODO: Test login
# extraScript = { ... }: ''
# '';
};
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/services/audiobookshelf.nix
];
test = {
subdomain = "a";
};
shb.audiobookshelf = {
enable = true;
inherit (config.test) subdomain domain;
};
};
clientLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "a";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "[Uu]sername";
passwordFieldLabelRegex = "[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
# Failure is after so we're not throttled too much.
{ username = "root"; password = "rootpw"; nextPageExpect = [
"expect(page.get_by_text('Wrong username or password')).to_be_visible()"
]; }
# { username = adminUser; password = adminPass; nextPageExpect = [
# "expect(page.get_by_text('Wrong username or password')).not_to_be_visible()"
# "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
# "expect(page).to_have_title(re.compile('Dashboard'))"
# ]; }
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/audiobookshelf.nix
];
};
};
https = { config, ... }: {
shb.audiobookshelf = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
sso = { config, ... }: {
shb.audiobookshelf = {
sso = {
test = {
subdomain = "a";
};
shb.audiobookshelf = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result = config.shb.hardcodedsecret.audiobookshelfSSOPassword.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia.result;
inherit (config.test) subdomain domain;
};
};
shb.hardcodedsecret.audiobookshelfSSOPassword = {
request = config.shb.audiobookshelf.sso.sharedSecret.request;
settings.content = "ssoPassword";
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "a";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "[Uu]sername";
passwordFieldLabelRegex = "[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
# Failure is after so we're not throttled too much.
{
username = "root";
password = "rootpw";
nextPageExpect = [
"expect(page.get_by_text('Wrong username or password')).to_be_visible()"
];
}
# { username = adminUser; password = adminPass; nextPageExpect = [
# "expect(page.get_by_text('Wrong username or password')).not_to_be_visible()"
# "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
# "expect(page).to_have_title(re.compile('Dashboard'))"
# ]; }
];
};
};
shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia = {
request = config.shb.audiobookshelf.sso.sharedSecretForAuthelia.request;
settings.content = "ssoPassword";
https =
{ config, ... }:
{
shb.audiobookshelf = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
sso =
{ config, ... }:
{
shb.audiobookshelf = {
sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result = config.shb.hardcodedsecret.audiobookshelfSSOPassword.result;
sharedSecretForAuthelia.result =
config.shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia.result;
};
};
shb.hardcodedsecret.audiobookshelfSSOPassword = {
request = config.shb.audiobookshelf.sso.sharedSecret.request;
settings.content = "ssoPassword";
};
shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia = {
request = config.shb.audiobookshelf.sso.sharedSecretForAuthelia.request;
settings.content = "ssoPassword";
};
};
};
in
{
basic = lib.shb.runNixOSTest {
@ -116,7 +133,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};
@ -124,18 +141,20 @@ in
sso = lib.shb.runNixOSTest {
name = "audiobookshelf-sso";
nodes.server = { config, ... }: {
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};

View file

@ -2,146 +2,169 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.deluge.ssl);
waitForServices = { ... }: [
"nginx.service"
"deluged.service"
"delugeweb.service"
];
waitForPorts = { node, ... }: [
node.config.shb.deluge.daemonPort
node.config.shb.deluge.webPort
];
extraScript = { node, proto_fqdn, ... }: ''
print(${node.name}.succeed('journalctl -n100 -u deluged'))
print(${node.name}.succeed('systemctl status deluged'))
print(${node.name}.succeed('systemctl status delugeweb'))
with subtest("web connect"):
print(server.succeed("cat ${node.config.services.deluge.dataDir}/.config/deluge/auth"))
response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """
-H "Content-Type: application/json"
-H "Accept: application/json"
"""), data = unline_with(" ", """
{"method": "auth.login", "params": ["deluge"], "id": 1}
"""))
print(response)
if response['error']:
raise Exception(f"error is {response['error']}")
if not response['result']:
raise Exception(f"response is {response}")
response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """
-H "Content-Type: application/json"
-H "Accept: application/json"
"""), data = unline_with(" ", """
{"method": "web.get_hosts", "params": [], "id": 1}
"""))
print(response)
if response['error']:
raise Exception(f"error is {response['error']}")
hostID = response['result'][0][0]
response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """
-H "Content-Type: application/json"
-H "Accept: application/json"
"""), data = unline_with(" ", f"""
{{"method": "web.connect", "params": ["{hostID}"], "id": 1}}
"""))
print(response)
if response['error']:
raise Exception(f"result had an error {response['error']}")
'';
};
prometheusTestScript = { nodes, ... }:
''
server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.deluge.port})
with subtest("prometheus"):
response = server.succeed(
"curl -sSf "
+ " http://localhost:${toString nodes.server.services.prometheus.exporters.deluge.port}/metrics"
)
print(response)
'';
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/deluge.nix
];
test = {
subdomain = "d";
};
shb.deluge = {
enable = true;
inherit (config.test) domain subdomain;
settings = {
downloadLocation = "/var/lib/deluge";
};
extraUsers = {
user.password.source = pkgs.writeText "userpw" "userpw";
};
localclientPassword.result = config.shb.hardcodedsecret."localclientpassword".result;
};
shb.hardcodedsecret."localclientpassword" = {
request = config.shb.deluge.localclientPassword.request;
settings.content = "localpw";
};
};
clientLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = "d";
};
test.login = {
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "Login";
testLoginWith = [
{ password = "deluge"; nextPageExpect = [
"expect(page.get_by_role('button', name='Login')).not_to_be_visible()"
"expect(page.get_by_text('Login Failed')).not_to_be_visible()"
]; }
{ password = "other"; nextPageExpect = [
"expect(page.get_by_role('button', name='Login')).to_be_visible()"
"expect(page.get_by_text('Login Failed')).to_be_visible()"
]; }
waitForServices =
{ ... }:
[
"nginx.service"
"deluged.service"
"delugeweb.service"
];
};
waitForPorts =
{ node, ... }:
[
node.config.shb.deluge.daemonPort
node.config.shb.deluge.webPort
];
extraScript =
{ node, proto_fqdn, ... }:
''
print(${node.name}.succeed('journalctl -n100 -u deluged'))
print(${node.name}.succeed('systemctl status deluged'))
print(${node.name}.succeed('systemctl status delugeweb'))
with subtest("web connect"):
print(server.succeed("cat ${node.config.services.deluge.dataDir}/.config/deluge/auth"))
response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """
-H "Content-Type: application/json"
-H "Accept: application/json"
"""), data = unline_with(" ", """
{"method": "auth.login", "params": ["deluge"], "id": 1}
"""))
print(response)
if response['error']:
raise Exception(f"error is {response['error']}")
if not response['result']:
raise Exception(f"response is {response}")
response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """
-H "Content-Type: application/json"
-H "Accept: application/json"
"""), data = unline_with(" ", """
{"method": "web.get_hosts", "params": [], "id": 1}
"""))
print(response)
if response['error']:
raise Exception(f"error is {response['error']}")
hostID = response['result'][0][0]
response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """
-H "Content-Type: application/json"
-H "Accept: application/json"
"""), data = unline_with(" ", f"""
{{"method": "web.connect", "params": ["{hostID}"], "id": 1}}
"""))
print(response)
if response['error']:
raise Exception(f"result had an error {response['error']}")
'';
};
prometheus = { config, ... }: {
shb.deluge = {
prometheusScraperPassword.result = config.shb.hardcodedsecret."scraper".result;
};
shb.hardcodedsecret."scraper" = {
request = config.shb.deluge.prometheusScraperPassword.request;
settings.content = "scraperpw";
};
};
prometheusTestScript =
{ nodes, ... }:
''
server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.deluge.port})
with subtest("prometheus"):
response = server.succeed(
"curl -sSf "
+ " http://localhost:${toString nodes.server.services.prometheus.exporters.deluge.port}/metrics"
)
print(response)
'';
https = { config, ...}: {
shb.deluge = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/deluge.nix
];
sso = { config, ... }: {
shb.deluge = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
test = {
subdomain = "d";
};
shb.deluge = {
enable = true;
inherit (config.test) domain subdomain;
settings = {
downloadLocation = "/var/lib/deluge";
};
extraUsers = {
user.password.source = pkgs.writeText "userpw" "userpw";
};
localclientPassword.result = config.shb.hardcodedsecret."localclientpassword".result;
};
shb.hardcodedsecret."localclientpassword" = {
request = config.shb.deluge.localclientPassword.request;
settings.content = "localpw";
};
};
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = "d";
};
test.login = {
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "Login";
testLoginWith = [
{
password = "deluge";
nextPageExpect = [
"expect(page.get_by_role('button', name='Login')).not_to_be_visible()"
"expect(page.get_by_text('Login Failed')).not_to_be_visible()"
];
}
{
password = "other";
nextPageExpect = [
"expect(page.get_by_role('button', name='Login')).to_be_visible()"
"expect(page.get_by_text('Login Failed')).to_be_visible()"
];
}
];
};
};
prometheus =
{ config, ... }:
{
shb.deluge = {
prometheusScraperPassword.result = config.shb.hardcodedsecret."scraper".result;
};
shb.hardcodedsecret."scraper" = {
request = config.shb.deluge.prometheusScraperPassword.request;
settings.content = "scraperpw";
};
};
https =
{ config, ... }:
{
shb.deluge = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
sso =
{ config, ... }:
{
shb.deluge = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
};
};
in
{
basic = lib.shb.runNixOSTest {
@ -164,14 +187,16 @@ in
backup = lib.shb.runNixOSTest {
name = "deluge_backup";
nodes.server = { config, ... }: {
imports = [
basic
(lib.shb.backup config.shb.deluge.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.deluge.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -187,27 +212,29 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
sso = lib.shb.runNixOSTest {
name = "deluge_sso";
nodes.server = { config, ... }: {
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.client = {};
nodes.server =
{ config, ... }:
{
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.client = { };
testScript = commonTestScript.access.override {
redirectSSO = true;
};
@ -225,10 +252,8 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = inputs:
(commonTestScript.access inputs)
+ (prometheusTestScript inputs);
testScript = inputs: (commonTestScript.access inputs) + (prometheusTestScript inputs);
};
}

View file

@ -4,150 +4,182 @@ let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.forgejo.ssl);
waitForServices = { ... }: [
"forgejo.service"
"nginx.service"
];
waitForUnixSocket = { node, ... }: [
node.config.services.forgejo.settings.server.HTTP_ADDR
];
extraScript = { node, ... }: ''
server.wait_for_unit("gitea-runner-local.service", timeout=10)
server.succeed("journalctl -o cat -u gitea-runner-local.service | grep -q 'Runner registered successfully'")
'';
};
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/forgejo.nix
];
test = {
subdomain = "f";
};
shb.forgejo = {
enable = true;
inherit (config.test) subdomain domain;
users = {
"theadmin" = {
isAdmin = true;
email = "theadmin@example.com";
password.result = config.shb.hardcodedsecret.forgejoAdminPassword.result;
};
"theuser" = {
email = "theuser@example.com";
password.result = config.shb.hardcodedsecret.forgejoUserPassword.result;
};
};
databasePassword.result = config.shb.hardcodedsecret.forgejoDatabasePassword.result;
};
# Needed for gitea-runner-local to be able to ping forgejo.
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
shb.hardcodedsecret.forgejoAdminPassword = {
request = config.shb.forgejo.users."theadmin".password.request;
settings.content = adminPassword;
};
shb.hardcodedsecret.forgejoUserPassword = {
request = config.shb.forgejo.users."theuser".password.request;
settings.content = "userPassword";
};
shb.hardcodedsecret.forgejoDatabasePassword = {
request = config.shb.forgejo.databasePassword.request;
settings.content = "databasePassword";
};
};
clientLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = "f";
};
test.login = {
startUrl = "http://${config.test.fqdn}/user/login";
usernameFieldLabelRegex = "Username or email address";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "theadmin"; password = adminPassword + "oops"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "theadmin"; password = adminPassword; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "theuser"; password = "userPasswordOops"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "theuser"; password = "userPassword"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
waitForServices =
{ ... }:
[
"forgejo.service"
"nginx.service"
];
};
waitForUnixSocket =
{ node, ... }:
[
node.config.services.forgejo.settings.server.HTTP_ADDR
];
extraScript =
{ node, ... }:
''
server.wait_for_unit("gitea-runner-local.service", timeout=10)
server.succeed("journalctl -o cat -u gitea-runner-local.service | grep -q 'Runner registered successfully'")
'';
};
https = { config, ... }: {
shb.forgejo = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/forgejo.nix
];
ldap = { config, ... }: {
shb.forgejo = {
ldap = {
test = {
subdomain = "f";
};
shb.forgejo = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminPassword.result = config.shb.hardcodedsecret.forgejoLdapUserPassword.result;
waitForSystemdServices = [ "lldap.service" ];
inherit (config.test) subdomain domain;
userGroup = "user_group";
users = {
"theadmin" = {
isAdmin = true;
email = "theadmin@example.com";
password.result = config.shb.hardcodedsecret.forgejoAdminPassword.result;
};
"theuser" = {
email = "theuser@example.com";
password.result = config.shb.hardcodedsecret.forgejoUserPassword.result;
};
};
databasePassword.result = config.shb.hardcodedsecret.forgejoDatabasePassword.result;
};
# Needed for gitea-runner-local to be able to ping forgejo.
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
shb.hardcodedsecret.forgejoAdminPassword = {
request = config.shb.forgejo.users."theadmin".password.request;
settings.content = adminPassword;
};
shb.hardcodedsecret.forgejoUserPassword = {
request = config.shb.forgejo.users."theuser".password.request;
settings.content = "userPassword";
};
shb.hardcodedsecret.forgejoDatabasePassword = {
request = config.shb.forgejo.databasePassword.request;
settings.content = "databasePassword";
};
};
shb.hardcodedsecret.forgejoLdapUserPassword = {
request = config.shb.forgejo.ldap.adminPassword.request;
settings.content = "ldapUserPassword";
};
};
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = "f";
};
sso = { config, ... }: {
shb.forgejo = {
sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result = config.shb.hardcodedsecret.forgejoSSOPassword.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.result;
test.login = {
startUrl = "http://${config.test.fqdn}/user/login";
usernameFieldLabelRegex = "Username or email address";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{
username = "theadmin";
password = adminPassword + "oops";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
];
}
{
username = "theadmin";
password = adminPassword;
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
];
}
{
username = "theuser";
password = "userPasswordOops";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
];
}
{
username = "theuser";
password = "userPassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
];
}
];
};
};
shb.hardcodedsecret.forgejoSSOPassword = {
request = config.shb.forgejo.sso.sharedSecret.request;
settings.content = "ssoPassword";
https =
{ config, ... }:
{
shb.forgejo = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
shb.hardcodedsecret.forgejoSSOPasswordAuthelia = {
request = config.shb.forgejo.sso.sharedSecretForAuthelia.request;
settings.content = "ssoPassword";
ldap =
{ config, ... }:
{
shb.forgejo = {
ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminPassword.result = config.shb.hardcodedsecret.forgejoLdapUserPassword.result;
waitForSystemdServices = [ "lldap.service" ];
userGroup = "user_group";
};
};
shb.hardcodedsecret.forgejoLdapUserPassword = {
request = config.shb.forgejo.ldap.adminPassword.request;
settings.content = "ldapUserPassword";
};
};
sso =
{ config, ... }:
{
shb.forgejo = {
sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result = config.shb.hardcodedsecret.forgejoSSOPassword.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.result;
};
};
shb.hardcodedsecret.forgejoSSOPassword = {
request = config.shb.forgejo.sso.sharedSecret.request;
settings.content = "ssoPassword";
};
shb.hardcodedsecret.forgejoSSOPasswordAuthelia = {
request = config.shb.forgejo.sso.sharedSecretForAuthelia.request;
settings.content = "ssoPassword";
};
};
};
in
{
basic = lib.shb.runNixOSTest {
@ -170,14 +202,16 @@ in
backup = lib.shb.runNixOSTest {
name = "forgejo_backup";
nodes.server = { config, ... }: {
imports = [
basic
(lib.shb.backup config.shb.forgejo.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.forgejo.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -193,7 +227,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -208,71 +242,100 @@ in
ldap
];
};
nodes.client = {
imports = [
({ config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = "f";
};
test.login = {
startUrl = "http://${config.test.fqdn}/user/login";
usernameFieldLabelRegex = "Username or email address";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
]; }
(
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
};
})
test = {
subdomain = "f";
};
test.login = {
startUrl = "http://${config.test.fqdn}/user/login";
usernameFieldLabelRegex = "Username or email address";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
];
}
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
"expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()"
];
}
];
};
}
)
];
};
testScript = commonTestScript.access;
};
sso = lib.shb.runNixOSTest {
name = "forgejo_sso";
nodes.server = { config, pkgs, ... }: {
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};

View file

@ -2,65 +2,83 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.grocy.ssl);
waitForServices = { ... }: [
"phpfpm-grocy.service"
"nginx.service"
];
waitForUnixSocket = { node, ... }: [
node.config.services.phpfpm.pools.grocy.socket
];
};
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/services/grocy.nix
];
test = {
subdomain = "g";
};
shb.grocy = {
enable = true;
inherit (config.test) subdomain domain;
};
};
clientLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "g";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "OK";
testLoginWith = [
{ username = "admin"; password = "admin oops"; nextPageExpect = [
"expect(page.get_by_text('Invalid credentials, please try again')).to_be_visible()"
]; }
{ username = "admin"; password = "admin"; nextPageExpect = [
"expect(page.get_by_text('Invalid credentials, please try again')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('OK'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Grocy'))"
]; }
waitForServices =
{ ... }:
[
"phpfpm-grocy.service"
"nginx.service"
];
waitForUnixSocket =
{ node, ... }:
[
node.config.services.phpfpm.pools.grocy.socket
];
};
};
https = { config, ...}: {
shb.grocy = {
ssl = config.shb.certs.certs.selfsigned.n;
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/grocy.nix
];
test = {
subdomain = "g";
};
shb.grocy = {
enable = true;
inherit (config.test) subdomain domain;
};
};
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "g";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "OK";
testLoginWith = [
{
username = "admin";
password = "admin oops";
nextPageExpect = [
"expect(page.get_by_text('Invalid credentials, please try again')).to_be_visible()"
];
}
{
username = "admin";
password = "admin";
nextPageExpect = [
"expect(page.get_by_text('Invalid credentials, please try again')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('OK'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Grocy'))"
];
}
];
};
};
https =
{ config, ... }:
{
shb.grocy = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
};
in
{
basic = lib.shb.runNixOSTest {
@ -91,7 +109,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};

View file

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

View file

@ -2,89 +2,103 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.home-assistant.ssl);
waitForServices = { ... }: [
"home-assistant.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
8123
];
};
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/services/home-assistant.nix
];
test = {
subdomain = "ha";
};
shb.home-assistant = {
enable = true;
inherit (config.test) subdomain domain;
config = {
name = "Tiserbox";
country = "CH";
latitude = "01.0000000000";
longitude.source = pkgs.writeText "longitude" "01.0000000000";
time_zone = "Europe/Zurich";
unit_system = "metric";
};
};
};
clientLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "ha";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
testLoginWith = [
{ nextPageExpect = [
"page.get_by_role('button', name=re.compile('Create my smart home')).click()"
"expect(page.get_by_text('Create user')).to_be_visible()"
"page.get_by_label(re.compile('Name')).fill('Admin')"
"page.get_by_label(re.compile('Username')).fill('admin')"
"page.get_by_label(re.compile('Password')).fill('adminpassword')"
"page.get_by_label(re.compile('Confirm password')).fill('adminpassword')"
"page.get_by_role('button', name=re.compile('Create account')).click()"
"expect(page.get_by_text('All set!')).to_be_visible()"
"page.get_by_role('button', name=re.compile('Finish')).click()"
"expect(page).to_have_title(re.compile('Overview'), timeout=15000)"
]; }
waitForServices =
{ ... }:
[
"home-assistant.service"
"nginx.service"
];
waitForPorts =
{ node, ... }:
[
8123
];
};
};
https = { config, ...}: {
shb.home-assistant = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/home-assistant.nix
];
ldap = { config, ... }: {
shb.home-assistant = {
ldap = {
test = {
subdomain = "ha";
};
shb.home-assistant = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.webUIListenPort;
userGroup = "homeassistant_user";
inherit (config.test) subdomain domain;
config = {
name = "Tiserbox";
country = "CH";
latitude = "01.0000000000";
longitude.source = pkgs.writeText "longitude" "01.0000000000";
time_zone = "Europe/Zurich";
unit_system = "metric";
};
};
};
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "ha";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
testLoginWith = [
{
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Create my smart home')).click()"
"expect(page.get_by_text('Create user')).to_be_visible()"
"page.get_by_label(re.compile('Name')).fill('Admin')"
"page.get_by_label(re.compile('Username')).fill('admin')"
"page.get_by_label(re.compile('Password')).fill('adminpassword')"
"page.get_by_label(re.compile('Confirm password')).fill('adminpassword')"
"page.get_by_role('button', name=re.compile('Create account')).click()"
"expect(page.get_by_text('All set!')).to_be_visible()"
"page.get_by_role('button', name=re.compile('Finish')).click()"
"expect(page).to_have_title(re.compile('Overview'), timeout=15000)"
];
}
];
};
};
https =
{ config, ... }:
{
shb.home-assistant = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
ldap =
{ config, ... }:
{
shb.home-assistant = {
ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.webUIListenPort;
userGroup = "homeassistant_user";
};
};
};
};
# Not yet supported
#
@ -95,58 +109,60 @@ let
# };
# };
voice = { config, ... }: {
# For now, verifying the packages can build is good enough.
environment.systemPackages = [
config.services.wyoming.piper.package
config.services.wyoming.openwakeword.package
config.services.wyoming.faster-whisper.package
];
voice =
{ config, ... }:
{
# For now, verifying the packages can build is good enough.
environment.systemPackages = [
config.services.wyoming.piper.package
config.services.wyoming.openwakeword.package
config.services.wyoming.faster-whisper.package
];
# TODO: enable this back. The issue id the services cannot talk to the internet
# to download the models so they fail to start..
# shb.home-assistant.voice.text-to-speech = {
# "fr" = {
# enable = true;
# voice = "fr-siwis-medium";
# uri = "tcp://0.0.0.0:10200";
# speaker = 0;
# };
# "en" = {
# enable = true;
# voice = "en_GB-alba-medium";
# uri = "tcp://0.0.0.0:10201";
# speaker = 0;
# };
# };
# shb.home-assistant.voice.speech-to-text = {
# "tiny-fr" = {
# enable = true;
# model = "base-int8";
# language = "fr";
# uri = "tcp://0.0.0.0:10300";
# device = "cpu";
# };
# "tiny-en" = {
# enable = true;
# model = "base-int8";
# language = "en";
# uri = "tcp://0.0.0.0:10301";
# device = "cpu";
# };
# };
# shb.home-assistant.voice.wakeword = {
# enable = true;
# uri = "tcp://127.0.0.1:10400";
# preloadModels = [
# "alexa"
# "hey_jarvis"
# "hey_mycroft"
# "hey_rhasspy"
# "ok_nabu"
# ];
# };
};
# TODO: enable this back. The issue id the services cannot talk to the internet
# to download the models so they fail to start..
# shb.home-assistant.voice.text-to-speech = {
# "fr" = {
# enable = true;
# voice = "fr-siwis-medium";
# uri = "tcp://0.0.0.0:10200";
# speaker = 0;
# };
# "en" = {
# enable = true;
# voice = "en_GB-alba-medium";
# uri = "tcp://0.0.0.0:10201";
# speaker = 0;
# };
# };
# shb.home-assistant.voice.speech-to-text = {
# "tiny-fr" = {
# enable = true;
# model = "base-int8";
# language = "fr";
# uri = "tcp://0.0.0.0:10300";
# device = "cpu";
# };
# "tiny-en" = {
# enable = true;
# model = "base-int8";
# language = "en";
# uri = "tcp://0.0.0.0:10301";
# device = "cpu";
# };
# };
# shb.home-assistant.voice.wakeword = {
# enable = true;
# uri = "tcp://127.0.0.1:10400";
# preloadModels = [
# "alexa"
# "hey_jarvis"
# "hey_mycroft"
# "hey_rhasspy"
# "ok_nabu"
# ];
# };
};
in
{
basic = lib.shb.runNixOSTest {
@ -169,14 +185,16 @@ in
backup = lib.shb.runNixOSTest {
name = "homeassistant_backup";
nodes.server = { config, ... }: {
imports = [
basic
(lib.shb.backup config.shb.home-assistant.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.home-assistant.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -192,24 +210,24 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
ldap = lib.shb.runNixOSTest {
name = "homeassistant_ldap";
nodes.server = {
imports = [
imports = [
basic
lib.shb.ldap
ldap
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -218,7 +236,7 @@ in
# sso = lib.shb.runNixOSTest {
# name = "vaultwarden_sso";
#
# nodes.server = lib.mkMerge [
# nodes.server = lib.mkMerge [
# basic
# (lib.shb.certs domain)
# https
@ -235,16 +253,16 @@ in
voice = lib.shb.runNixOSTest {
name = "homeassistant_voice";
nodes.server = {
imports = [
imports = [
basic
voice
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
}

View file

@ -5,117 +5,138 @@ let
commonTestScript = lib.shb.accessScript {
hasSSL = { node, ... }: !(isNull node.config.shb.immich.ssl);
waitForServices = { ... }: [ "immich-server.service" "postgresql.service" "nginx.service" ];
waitForPorts = { ... }: [ 2283 80 ];
waitForServices =
{ ... }:
[
"immich-server.service"
"postgresql.service"
"nginx.service"
];
waitForPorts =
{ ... }:
[
2283
80
];
waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}" ];
};
base = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/services/immich.nix
];
base =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/immich.nix
];
virtualisation.memorySize = 4096;
virtualisation.cores = 2;
virtualisation.memorySize = 4096;
virtualisation.cores = 2;
test = {
inherit subdomain domain;
test = {
inherit subdomain domain;
};
shb.immich = {
enable = true;
inherit subdomain domain;
debug = true;
};
# Required for tests
environment.systemPackages = [ pkgs.curl ];
};
shb.immich = {
enable = true;
inherit subdomain domain;
basic =
{ config, ... }:
{
imports = [ base ];
debug = true;
test.hasSSL = false;
};
# Required for tests
environment.systemPackages = [ pkgs.curl ];
};
https =
{ config, ... }:
{
imports = [
base
lib.shb.certs
];
basic = { config, ... }: {
imports = [ base ];
test.hasSSL = false;
};
https = { config, ... }: {
imports = [
base
lib.shb.certs
];
test.hasSSL = true;
shb.immich.ssl = config.shb.certs.certs.selfsigned.n;
};
backup = { config, ... }: {
imports = [
https
(lib.shb.backup config.shb.immich.backup)
];
};
sso = { config, ... }: {
imports = [
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
];
shb.immich.sso = {
enable = true;
provider = "Authelia";
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
clientID = "immich";
autoLaunch = true;
sharedSecret.result = config.shb.hardcodedsecret.immichSSOSecret.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.immichSSOSecretAuthelia.result;
test.hasSSL = true;
shb.immich.ssl = config.shb.certs.certs.selfsigned.n;
};
shb.hardcodedsecret.immichSSOSecret = {
request = config.shb.immich.sso.sharedSecret.request;
settings.content = "immichSSOSecret";
backup =
{ config, ... }:
{
imports = [
https
(lib.shb.backup config.shb.immich.backup)
];
};
shb.hardcodedsecret.immichSSOSecretAuthelia = {
request = config.shb.immich.sso.sharedSecretForAuthelia.request;
settings.content = "immichSSOSecret";
};
sso =
{ config, ... }:
{
imports = [
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
];
# Configure LDAP groups for group-based access control
shb.lldap.ensureGroups.immich_user = {};
shb.immich.sso = {
enable = true;
provider = "Authelia";
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
clientID = "immich";
autoLaunch = true;
sharedSecret.result = config.shb.hardcodedsecret.immichSSOSecret.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.immichSSOSecretAuthelia.result;
};
shb.lldap.ensureUsers.immich_test_user = {
email = "immich_user@example.com";
groups = [ "immich_user" ];
password.result = config.shb.hardcodedsecret.ldapImmichUserPassword.result;
};
shb.hardcodedsecret.immichSSOSecret = {
request = config.shb.immich.sso.sharedSecret.request;
settings.content = "immichSSOSecret";
};
shb.lldap.ensureUsers.regular_test_user = {
email = "regular_user@example.com";
groups = [ ];
password.result = config.shb.hardcodedsecret.ldapRegularUserPassword.result;
};
shb.hardcodedsecret.immichSSOSecretAuthelia = {
request = config.shb.immich.sso.sharedSecretForAuthelia.request;
settings.content = "immichSSOSecret";
};
shb.hardcodedsecret.ldapImmichUserPassword = {
request = config.shb.lldap.ensureUsers.immich_test_user.password.request;
settings.content = "immich_user_password";
};
# Configure LDAP groups for group-based access control
shb.lldap.ensureGroups.immich_user = { };
shb.hardcodedsecret.ldapRegularUserPassword = {
request = config.shb.lldap.ensureUsers.regular_test_user.password.request;
settings.content = "regular_user_password";
shb.lldap.ensureUsers.immich_test_user = {
email = "immich_user@example.com";
groups = [ "immich_user" ];
password.result = config.shb.hardcodedsecret.ldapImmichUserPassword.result;
};
shb.lldap.ensureUsers.regular_test_user = {
email = "regular_user@example.com";
groups = [ ];
password.result = config.shb.hardcodedsecret.ldapRegularUserPassword.result;
};
shb.hardcodedsecret.ldapImmichUserPassword = {
request = config.shb.lldap.ensureUsers.immich_test_user.password.request;
settings.content = "immich_user_password";
};
shb.hardcodedsecret.ldapRegularUserPassword = {
request = config.shb.lldap.ensureUsers.regular_test_user.password.request;
settings.content = "regular_user_password";
};
};
};
in
{
basic = pkgs.nixosTest {
name = "immich-basic";
nodes.server = basic;
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};
@ -124,7 +145,7 @@ in
name = "immich-https";
nodes.server = https;
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};
@ -133,13 +154,21 @@ in
name = "immich-backup";
nodes.server = backup;
nodes.client = {};
nodes.client = { };
testScript = (lib.shb.mkScripts {
hasSSL = args: !(isNull args.node.config.shb.immich.ssl);
waitForServices = args: [ "immich-server.service" "postgresql.service" "nginx.service" ];
waitForPorts = args: [ 2283 80 ];
waitForUrls = args: [ "${args.proto_fqdn}" ];
}).backup;
testScript =
(lib.shb.mkScripts {
hasSSL = args: !(isNull args.node.config.shb.immich.ssl);
waitForServices = args: [
"immich-server.service"
"postgresql.service"
"nginx.service"
];
waitForPorts = args: [
2283
80
];
waitForUrls = args: [ "${args.proto_fqdn}" ];
}).backup;
};
}

View file

@ -4,164 +4,185 @@ let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl);
waitForServices = { ... }: [
"jellyfin.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
port
];
waitForUrls = { proto_fqdn, ... }: [
"${proto_fqdn}/System/Info/Public"
];
extraScript = { node, ... }: ''
headers = unline_with(" ", """
-H 'Content-Type: application/json'
-H 'Authorization: MediaBrowser Client="Android TV", Device="Nvidia Shield", DeviceId="ZQ9YQHHrUzk24vV", Version="0.15.3"'
""")
with subtest("api login success"):
response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName",
data="""{"Username": "jellyfin", "Pw": "admin"}""",
extra=headers)
if response['code'] != 200:
raise Exception(f"Expected success, got: {response['code']}")
with subtest("api login failure"):
response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName",
data="""{"Username": "jellyfin", "Pw": "badpassword"}""",
extra=headers)
if response['code'] != 401:
raise Exception(f"Expected failure, got: {response['code']}")
'';
};
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/services/jellyfin.nix
];
test = {
subdomain = "j";
};
shb.jellyfin = {
enable = true;
inherit (config.test) subdomain domain;
inherit port;
admin = {
username = "jellyfin";
password.result = config.shb.hardcodedsecret.jellyfinAdminPassword.result;
};
debug = true;
};
shb.hardcodedsecret.jellyfinAdminPassword = {
request = config.shb.jellyfin.admin.password.request;
settings.content = "admin";
};
environment.systemPackages = [
pkgs.sqlite
];
};
clientLogin = { config, ... }: {
imports = [
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "j";
};
test.login = {
browser = "firefox";
# I tried without the path part but it randomly selects either the wizard
# or the page that selects a server.
# startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/wizardstart.html";
# startUrl = "${config.test.proto}://${config.test.fqdn}";
startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/login.html";
usernameFieldLabelRegex = "[Uu]ser";
loginButtonNameRegex = "Sign In";
testLoginWith = [
# I just couldn't make this work. It's very flaky.
# Most of the time, the login jellyfin page doesn't even load
# and the playwright browser is stuck on the splash page.
# I resorted to test the API directly.
# { username = "jellyfin"; password = "badpassword"; nextPageExpect = [
# "expect(page).to_have_title(re.compile('Jellyfin'))"
# "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=30000)"
# ]; }
# { username = "jellyfin"; password = "admin"; nextPageExpect = [
# "expect(page).to_have_title(re.compile('Jellyfin'))"
# "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=30000)"
# "expect(page.get_by_role('label', re.compile('[Uu]ser'))).not_to_be_visible(timeout=30000)"
# "expect(page.get_by_text(re.compile('[Pp]assword'))).not_to_be_visible(timeout=30000)"
# ]; }
waitForServices =
{ ... }:
[
"jellyfin.service"
"nginx.service"
];
};
waitForPorts =
{ node, ... }:
[
port
];
waitForUrls =
{ proto_fqdn, ... }:
[
"${proto_fqdn}/System/Info/Public"
];
extraScript =
{ node, ... }:
''
headers = unline_with(" ", """
-H 'Content-Type: application/json'
-H 'Authorization: MediaBrowser Client="Android TV", Device="Nvidia Shield", DeviceId="ZQ9YQHHrUzk24vV", Version="0.15.3"'
""")
with subtest("api login success"):
response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName",
data="""{"Username": "jellyfin", "Pw": "admin"}""",
extra=headers)
if response['code'] != 200:
raise Exception(f"Expected success, got: {response['code']}")
with subtest("api login failure"):
response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName",
data="""{"Username": "jellyfin", "Pw": "badpassword"}""",
extra=headers)
if response['code'] != 401:
raise Exception(f"Expected failure, got: {response['code']}")
'';
};
https = { config, ... }: {
shb.jellyfin = {
ssl = config.shb.certs.certs.selfsigned.n;
};
test = {
hasSSL = true;
};
};
ldap = { config, ... }: {
shb.jellyfin = {
ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminPassword.result = config.shb.hardcodedsecret.jellyfinLdapUserPassword.result;
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/jellyfin.nix
];
test = {
subdomain = "j";
};
};
shb.hardcodedsecret.jellyfinLdapUserPassword = {
request = config.shb.jellyfin.ldap.adminPassword.request;
settings.content = "ldapUserPassword";
};
};
sso = { config, ... }: {
shb.jellyfin = {
sso = {
shb.jellyfin = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result = config.shb.hardcodedsecret.jellyfinSSOPassword.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result;
inherit (config.test) subdomain domain;
inherit port;
admin = {
username = "jellyfin";
password.result = config.shb.hardcodedsecret.jellyfinAdminPassword.result;
};
debug = true;
};
};
shb.hardcodedsecret.jellyfinSSOPassword = {
request = config.shb.jellyfin.sso.sharedSecret.request;
settings.content = "ssoPassword";
};
shb.hardcodedsecret.jellyfinAdminPassword = {
request = config.shb.jellyfin.admin.password.request;
settings.content = "admin";
};
shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = {
request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request;
settings.content = "ssoPassword";
};
};
jellyfinTest = name: { nodes, testScript }: lib.shb.runNixOSTest {
name = "jellyfin_${name}";
interactive.nodes.server = {
environment.systemPackages = [
pkgs.sqlite
];
};
inherit nodes;
inherit testScript;
};
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "j";
};
test.login = {
browser = "firefox";
# I tried without the path part but it randomly selects either the wizard
# or the page that selects a server.
# startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/wizardstart.html";
# startUrl = "${config.test.proto}://${config.test.fqdn}";
startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/login.html";
usernameFieldLabelRegex = "[Uu]ser";
loginButtonNameRegex = "Sign In";
testLoginWith = [
# I just couldn't make this work. It's very flaky.
# Most of the time, the login jellyfin page doesn't even load
# and the playwright browser is stuck on the splash page.
# I resorted to test the API directly.
# { username = "jellyfin"; password = "badpassword"; nextPageExpect = [
# "expect(page).to_have_title(re.compile('Jellyfin'))"
# "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=30000)"
# ]; }
# { username = "jellyfin"; password = "admin"; nextPageExpect = [
# "expect(page).to_have_title(re.compile('Jellyfin'))"
# "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=30000)"
# "expect(page.get_by_role('label', re.compile('[Uu]ser'))).not_to_be_visible(timeout=30000)"
# "expect(page.get_by_text(re.compile('[Pp]assword'))).not_to_be_visible(timeout=30000)"
# ]; }
];
};
};
https =
{ config, ... }:
{
shb.jellyfin = {
ssl = config.shb.certs.certs.selfsigned.n;
};
test = {
hasSSL = true;
};
};
ldap =
{ config, ... }:
{
shb.jellyfin = {
ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminPassword.result = config.shb.hardcodedsecret.jellyfinLdapUserPassword.result;
};
};
shb.hardcodedsecret.jellyfinLdapUserPassword = {
request = config.shb.jellyfin.ldap.adminPassword.request;
settings.content = "ldapUserPassword";
};
};
sso =
{ config, ... }:
{
shb.jellyfin = {
sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sharedSecret.result = config.shb.hardcodedsecret.jellyfinSSOPassword.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result;
};
};
shb.hardcodedsecret.jellyfinSSOPassword = {
request = config.shb.jellyfin.sso.sharedSecret.request;
settings.content = "ssoPassword";
};
shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = {
request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request;
settings.content = "ssoPassword";
};
};
jellyfinTest =
name:
{ nodes, testScript }:
lib.shb.runNixOSTest {
name = "jellyfin_${name}";
interactive.nodes.server = {
environment.systemPackages = [
pkgs.sqlite
];
};
inherit nodes;
inherit testScript;
};
in
{
basic = jellyfinTest "basic" {
@ -174,20 +195,22 @@ in
# Client login does not work without SSL.
# At least, I couldn't make it work.
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
backup = jellyfinTest "backup" {
nodes.server = { config, ... }: {
imports = [
basic
(lib.shb.backup config.shb.jellyfin.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.jellyfin.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -201,12 +224,14 @@ in
];
};
nodes.client = { config, lib, ... }: {
imports = [
lib.shb.baseModule
clientLogin
];
};
nodes.client =
{ config, lib, ... }:
{
imports = [
lib.shb.baseModule
clientLogin
];
};
testScript = commonTestScript.access;
};
@ -219,25 +244,27 @@ in
ldap
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
sso = jellyfinTest "sso" {
nodes.server = { config, pkgs, ... }: {
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};

View file

@ -3,144 +3,182 @@ let
nextauthSecret = "nextauthSecret";
oidcSecret = "oidcSecret";
testLib = pkgs.callPackage ../common.nix {};
testLib = pkgs.callPackage ../common.nix { };
commonTestScript = testLib.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.karakeep.ssl);
waitForServices = { ... }: [
"karakeep-init.service"
"karakeep-browser.service"
"karakeep-web.service"
"karakeep-workers.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
node.config.shb.karakeep.port
];
};
basic = { config, ... }: {
imports = [
testLib.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/lldap.nix
../../modules/services/karakeep.nix
];
test = {
subdomain = "k";
};
shb.karakeep = {
enable = true;
inherit (config.test) subdomain domain;
nextauthSecret.result = config.shb.hardcodedsecret.nextauthSecret.result;
meilisearchMasterKey.result = config.shb.hardcodedsecret.meilisearchMasterKey.result;
};
shb.hardcodedsecret.nextauthSecret = {
request = config.shb.karakeep.nextauthSecret.request;
settings.content = nextauthSecret;
};
shb.hardcodedsecret.meilisearchMasterKey = {
request = config.shb.karakeep.meilisearchMasterKey.request;
settings.content = "meilisearch-master-key";
};
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
};
https = { config, ... }: {
shb.karakeep = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
ldap = { config, ... }: {
shb.karakeep = {
ldap = {
userGroup = "user_group";
};
};
};
clientLoginSso = { config, ... }: {
imports = [
testLib.baseModule
testLib.clientLoginModule
];
test = {
subdomain = "k";
};
test.login = {
startUrl = "https://${config.test.fqdn}";
beforeHook = ''
page.get_by_role("button", name="single sign-on").click()
'';
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('new item')).to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('new item')).to_be_visible()"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
# "page.get_by_role('button', name=re.compile('Accept')).click()" # I don't understand why this is not needed. Maybe it keeps somewhere the previous token?
"expect(page.get_by_text(re.compile('login failed'))).to_be_visible(timeout=10000)"
]; }
waitForServices =
{ ... }:
[
"karakeep-init.service"
"karakeep-browser.service"
"karakeep-web.service"
"karakeep-workers.service"
"nginx.service"
];
waitForPorts =
{ node, ... }:
[
node.config.shb.karakeep.port
];
};
};
sso = { config, ... }: {
shb.karakeep = {
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
clientID = "karakeep";
basic =
{ config, ... }:
{
imports = [
testLib.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/blocks/lldap.nix
../../modules/services/karakeep.nix
];
sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result;
test = {
subdomain = "k";
};
shb.karakeep = {
enable = true;
inherit (config.test) subdomain domain;
nextauthSecret.result = config.shb.hardcodedsecret.nextauthSecret.result;
meilisearchMasterKey.result = config.shb.hardcodedsecret.meilisearchMasterKey.result;
};
shb.hardcodedsecret.nextauthSecret = {
request = config.shb.karakeep.nextauthSecret.request;
settings.content = nextauthSecret;
};
shb.hardcodedsecret.meilisearchMasterKey = {
request = config.shb.karakeep.meilisearchMasterKey.request;
settings.content = "meilisearch-master-key";
};
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
};
shb.hardcodedsecret.oidcSecret = {
request = config.shb.karakeep.sso.sharedSecret.request;
settings.content = oidcSecret;
https =
{ config, ... }:
{
shb.karakeep = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
shb.hardcodedsecret.oidcAutheliaSecret = {
request = config.shb.karakeep.sso.sharedSecretForAuthelia.request;
settings.content = oidcSecret;
ldap =
{ config, ... }:
{
shb.karakeep = {
ldap = {
userGroup = "user_group";
};
};
};
clientLoginSso =
{ config, ... }:
{
imports = [
testLib.baseModule
testLib.clientLoginModule
];
test = {
subdomain = "k";
};
test.login = {
startUrl = "https://${config.test.fqdn}";
beforeHook = ''
page.get_by_role("button", name="single sign-on").click()
'';
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
];
}
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('new item')).to_be_visible()"
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('new item')).to_be_visible()"
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)"
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
# "page.get_by_role('button', name=re.compile('Accept')).click()" # I don't understand why this is not needed. Maybe it keeps somewhere the previous token?
"expect(page.get_by_text(re.compile('login failed'))).to_be_visible(timeout=10000)"
];
}
];
};
};
sso =
{ config, ... }:
{
shb.karakeep = {
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
clientID = "karakeep";
sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result;
};
};
shb.hardcodedsecret.oidcSecret = {
request = config.shb.karakeep.sso.sharedSecret.request;
settings.content = oidcSecret;
};
shb.hardcodedsecret.oidcAutheliaSecret = {
request = config.shb.karakeep.sso.sharedSecretForAuthelia.request;
settings.content = oidcSecret;
};
};
};
in
{
basic = pkgs.testers.runNixOSTest {
name = "karakeep_basic";
nodes.client = {};
nodes.client = { };
nodes.server = {
imports = [
basic
@ -153,14 +191,16 @@ in
backup = pkgs.testers.runNixOSTest {
name = "karakeep_backup";
nodes.server = { config, ... }: {
imports = [
basic
(testLib.backup config.shb.karakeep.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
(testLib.backup config.shb.karakeep.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -168,7 +208,7 @@ in
https = pkgs.testers.runNixOSTest {
name = "karakeep_https";
nodes.client = {};
nodes.client = { };
nodes.server = {
imports = [
basic
@ -191,19 +231,21 @@ in
virtualisation.memorySize = 4096;
};
nodes.server = { config, pkgs, ... }: {
imports = [
basic
testLib.certs
https
testLib.ldap
ldap
(testLib.sso config.shb.certs.certs.selfsigned.n)
sso
];
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
testLib.certs
https
testLib.ldap
ldap
(testLib.sso config.shb.certs.certs.selfsigned.n)
sso
];
virtualisation.memorySize = 4096;
};
virtualisation.memorySize = 4096;
};
testScript = commonTestScript.access;
};

View file

@ -4,43 +4,51 @@ let
commonTestScript = lib.shb.accessScript {
hasSSL = { node, ... }: !(isNull node.config.shb.monitoring.ssl);
waitForServices = { ... }: [
"grafana.service"
];
waitForPorts = { node, ... }: [
node.config.shb.monitoring.grafanaPort
];
waitForServices =
{ ... }:
[
"grafana.service"
];
waitForPorts =
{ node, ... }:
[
node.config.shb.monitoring.grafanaPort
];
};
basic = { config, ... }: {
test = {
subdomain = "g";
basic =
{ config, ... }:
{
test = {
subdomain = "g";
};
shb.monitoring = {
enable = true;
inherit (config.test) subdomain domain;
grafanaPort = 3000;
adminPassword.result = config.shb.hardcodedsecret."admin_password".result;
secretKey.result = config.shb.hardcodedsecret."secret_key".result;
};
shb.hardcodedsecret."admin_password" = {
request = config.shb.monitoring.adminPassword.request;
settings.content = password;
};
shb.hardcodedsecret."secret_key" = {
request = config.shb.monitoring.secretKey.request;
settings.content = "secret_key_pw";
};
};
shb.monitoring = {
enable = true;
inherit (config.test) subdomain domain;
grafanaPort = 3000;
adminPassword.result = config.shb.hardcodedsecret."admin_password".result;
secretKey.result = config.shb.hardcodedsecret."secret_key".result;
https =
{ config, ... }:
{
shb.monitoring = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
shb.hardcodedsecret."admin_password" = {
request = config.shb.monitoring.adminPassword.request;
settings.content = password;
};
shb.hardcodedsecret."secret_key" = {
request = config.shb.monitoring.secretKey.request;
settings.content = "secret_key_pw";
};
};
https = { config, ...}: {
shb.monitoring = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
in
{
basic = lib.shb.runNixOSTest {
@ -54,7 +62,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};
@ -72,7 +80,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript;
};

View file

@ -6,267 +6,339 @@ let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.nextcloud.ssl);
waitForServices = { ... }: [
"phpfpm-nextcloud.service"
"nginx.service"
];
waitForUnixSocket = { node, ... }: [
node.config.services.phpfpm.pools.nextcloud.socket
];
extraScript = { node, fqdn, proto_fqdn, ... }: ''
with subtest("fails with incorrect authentication"):
client.fail(
"curl -f -s --location -X PROPFIND"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:other """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/"
)
client.fail(
"curl -f -s --location -X PROPFIND"
+ """ -H "Depth: 1" """
+ """ -u root:rootpw """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ " ${proto_fqdn}/remote.php/dav/files/other/"
)
with subtest("fails with incorrect path"):
client.fail(
"curl -f -s --location -X PROPFIND"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ " ${proto_fqdn}/remote.php/dav/files/other/"
)
with subtest("can access webdav"):
client.succeed(
"curl -f -s --location -X PROPFIND"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/"
)
with subtest("can create and retrieve file"):
client.fail(
"curl -f -s --location -X GET"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ """ -T file """
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/file"
)
client.succeed("echo 'hello' > file")
client.succeed(
"curl -f -s --location -X PUT"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ """ -T file """
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/"
)
content = client.succeed(
"curl -f -s --location -X GET"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ """ -T file """
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/file"
)
if content != "hello\n":
raise Exception("Got incorrect content for file, expected 'hello\n' but got:\n{}".format(content))
'';
};
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/services/nextcloud-server.nix
];
test = {
subdomain = "n";
};
shb.nextcloud = {
enable = true;
inherit (config.test) subdomain domain;
dataDir = "/var/lib/nextcloud";
tracing = null;
defaultPhoneRegion = "US";
# This option is only needed because we do not access Nextcloud at the default port in the VM.
externalFqdn = "${config.test.fqdn}:8080";
adminUser = adminUser;
adminPass.result = config.shb.hardcodedsecret.adminPass.result;
debug = false; # Enable this if needed, but beware it is _very_ verbose.
};
shb.hardcodedsecret.adminPass = {
request = config.shb.nextcloud.adminPass.request;
settings.content = adminPass;
};
};
clientLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "n";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "Account name";
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{ username = adminUser; password = adminPass; nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
# Failure is after so we're not throttled too much.
{ username = adminUser; password = adminPass + "oops"; nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
]; }
waitForServices =
{ ... }:
[
"phpfpm-nextcloud.service"
"nginx.service"
];
};
};
clientLdapLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "n";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "Account name";
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
]; }
waitForUnixSocket =
{ node, ... }:
[
node.config.services.phpfpm.pools.nextcloud.socket
];
};
extraScript =
{
node,
fqdn,
proto_fqdn,
...
}:
''
with subtest("fails with incorrect authentication"):
client.fail(
"curl -f -s --location -X PROPFIND"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:other """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/"
)
client.fail(
"curl -f -s --location -X PROPFIND"
+ """ -H "Depth: 1" """
+ """ -u root:rootpw """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ " ${proto_fqdn}/remote.php/dav/files/other/"
)
with subtest("fails with incorrect path"):
client.fail(
"curl -f -s --location -X PROPFIND"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ " ${proto_fqdn}/remote.php/dav/files/other/"
)
with subtest("can access webdav"):
client.succeed(
"curl -f -s --location -X PROPFIND"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/"
)
with subtest("can create and retrieve file"):
client.fail(
"curl -f -s --location -X GET"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ """ -T file """
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/file"
)
client.succeed("echo 'hello' > file")
client.succeed(
"curl -f -s --location -X PUT"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ """ -T file """
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/"
)
content = client.succeed(
"curl -f -s --location -X GET"
+ """ -H "Depth: 1" """
+ """ -u ${adminUser}:${adminPass} """
+ " --connect-to ${fqdn}:443:server:443"
+ " --connect-to ${fqdn}:80:server:80"
+ """ -T file """
+ " ${proto_fqdn}/remote.php/dav/files/${adminUser}/file"
)
if content != "hello\n":
raise Exception("Got incorrect content for file, expected 'hello\n' but got:\n{}".format(content))
'';
};
clientSsoLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "n";
};
networking.hosts = {
"192.168.1.2" = [ "auth.example.com" ];
};
test.login = {
startUrl = "http://${config.test.fqdn}";
# No need since Nextcloud is auto-redirecting to the SSO sign in page.
# beforeHook = ''
# page.get_by_role("link", name="Sign in with SHB-Authelia").click()
# '';
usernameFieldLabelRegex = "Username";
passwordFieldSelector = "get_by_label(\"Password *\")";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page).to_have_title(re.compile('Dashboard'))"
"page.goto('https://${config.test.fqdn}/settings/admin')"
"expect(page.get_by_text('Access forbidden')).to_be_visible()"
]; }
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page).to_have_title(re.compile('Dashboard'))"
"page.goto('https://${config.test.fqdn}/settings/admin')"
"expect(page.get_by_text('Access forbidden')).not_to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text('not member of the allowed groups')).to_be_visible()"
]; }
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/services/nextcloud-server.nix
];
};
};
https = { config, ...}: {
shb.nextcloud = {
ssl = config.shb.certs.certs.selfsigned.n;
test = {
subdomain = "n";
};
externalFqdn = lib.mkForce null;
};
};
ldap = { config, ... }: {
shb.nextcloud = {
apps.ldap = {
shb.nextcloud = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminName = "admin";
adminPassword.result = config.shb.hardcodedsecret.nextcloudLdapUserPassword.result;
userGroup = "user_group";
inherit (config.test) subdomain domain;
dataDir = "/var/lib/nextcloud";
tracing = null;
defaultPhoneRegion = "US";
# This option is only needed because we do not access Nextcloud at the default port in the VM.
externalFqdn = "${config.test.fqdn}:8080";
adminUser = adminUser;
adminPass.result = config.shb.hardcodedsecret.adminPass.result;
debug = false; # Enable this if needed, but beware it is _very_ verbose.
};
shb.hardcodedsecret.adminPass = {
request = config.shb.nextcloud.adminPass.request;
settings.content = adminPass;
};
};
shb.hardcodedsecret.nextcloudLdapUserPassword = {
request = config.shb.nextcloud.apps.ldap.adminPassword.request;
settings = config.shb.hardcodedsecret.ldapUserPassword.settings;
};
};
sso = { config, ... }:
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "n";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "Account name";
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{
username = adminUser;
password = adminPass;
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
];
}
# Failure is after so we're not throttled too much.
{
username = adminUser;
password = adminPass + "oops";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
];
}
];
};
};
clientLdapLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "n";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
usernameFieldLabelRegex = "Account name";
passwordFieldLabelRegex = "^ *[Pp]assword";
loginButtonNameRegex = "[Ll]og [Ii]n";
testLoginWith = [
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
];
}
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
"expect(page).to_have_title(re.compile('Dashboard'))"
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
];
}
];
};
};
clientSsoLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "n";
};
networking.hosts = {
"192.168.1.2" = [ "auth.example.com" ];
};
test.login = {
startUrl = "http://${config.test.fqdn}";
# No need since Nextcloud is auto-redirecting to the SSO sign in page.
# beforeHook = ''
# page.get_by_role("link", name="Sign in with SHB-Authelia").click()
# '';
usernameFieldLabelRegex = "Username";
passwordFieldSelector = "get_by_label(\"Password *\")";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page).to_have_title(re.compile('Dashboard'))"
"page.goto('https://${config.test.fqdn}/settings/admin')"
"expect(page.get_by_text('Access forbidden')).to_be_visible()"
];
}
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page).to_have_title(re.compile('Dashboard'))"
"page.goto('https://${config.test.fqdn}/settings/admin')"
"expect(page.get_by_text('Access forbidden')).not_to_be_visible()"
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text('not member of the allowed groups')).to_be_visible()"
];
}
];
};
};
https =
{ config, ... }:
{
shb.nextcloud = {
ssl = config.shb.certs.certs.selfsigned.n;
externalFqdn = lib.mkForce null;
};
};
ldap =
{ config, ... }:
{
shb.nextcloud = {
apps.ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.lldap.ldapPort;
dcdomain = config.shb.lldap.dcdomain;
adminName = "admin";
adminPassword.result = config.shb.hardcodedsecret.nextcloudLdapUserPassword.result;
userGroup = "user_group";
};
};
shb.hardcodedsecret.nextcloudLdapUserPassword = {
request = config.shb.nextcloud.apps.ldap.adminPassword.request;
settings = config.shb.hardcodedsecret.ldapUserPassword.settings;
};
};
sso =
{ config, ... }:
{
shb.nextcloud = {
apps.ldap = {
@ -297,17 +369,19 @@ let
request = config.shb.nextcloud.apps.sso.secretForAuthelia.request;
settings.content = oidcSecret;
};
};
previewgenerator = { config, ...}: {
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
shb.nextcloud = {
apps.previewgenerator.enable = true;
};
};
previewgenerator =
{ config, ... }:
{
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
shb.nextcloud = {
apps.previewgenerator.enable = true;
};
};
externalstorage = {
systemd.tmpfiles.rules = [
@ -323,43 +397,50 @@ let
};
};
memories = { config, ...}: {
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
memories =
{ config, ... }:
{
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
shb.nextcloud = {
apps.memories.enable = true;
apps.memories.vaapi = true;
shb.nextcloud = {
apps.memories.enable = true;
apps.memories.vaapi = true;
};
};
};
recognize = { config, ...}: {
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
recognize =
{ config, ... }:
{
systemd.tmpfiles.rules = [
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
];
shb.nextcloud = {
apps.recognize.enable = true;
shb.nextcloud = {
apps.recognize.enable = true;
};
};
};
prometheus = { config, ... }: {
shb.nextcloud = {
phpFpmPrometheusExporter.enable = true;
prometheus =
{ config, ... }:
{
shb.nextcloud = {
phpFpmPrometheusExporter.enable = true;
};
};
};
prometheusTestScript = { nodes, ... }:
prometheusTestScript =
{ nodes, ... }:
''
server.wait_for_open_unix_socket("${nodes.server.services.phpfpm.pools.nextcloud.socket}")
server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.php-fpm.port})
with subtest("prometheus"):
response = server.succeed(
"curl -sSf "
+ " http://localhost:${toString nodes.server.services.prometheus.exporters.php-fpm.port}/metrics"
)
print(response)
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})
with subtest("prometheus"):
response = server.succeed(
"curl -sSf "
+ " http://localhost:${toString nodes.server.services.prometheus.exporters.php-fpm.port}/metrics"
)
print(response)
'';
in
{
@ -389,44 +470,53 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access.override {
extraScript = { node, fqdn, proto_fqdn, ... }: ''
import time
extraScript =
{
node,
fqdn,
proto_fqdn,
...
}:
''
import time
def find_in_logs(unit, text):
return server.systemctl("status {}".format(unit))[1].find(text) != -1
def find_in_logs(unit, text):
return server.systemctl("status {}".format(unit))[1].find(text) != -1
with subtest("cron job succeeds"):
# This call does not block until the service is done.
server.succeed("systemctl start nextcloud-cron.service&")
with subtest("cron job succeeds"):
# This call does not block until the service is done.
server.succeed("systemctl start nextcloud-cron.service&")
# If the service failed, then we're not happy.
status = "active"
while status == "active":
status = server.get_unit_info("nextcloud-cron")["ActiveState"]
time.sleep(5)
if status != "inactive":
raise Exception("Cron job did not finish correctly")
# If the service failed, then we're not happy.
status = "active"
while status == "active":
status = server.get_unit_info("nextcloud-cron")["ActiveState"]
time.sleep(5)
if status != "inactive":
raise Exception("Cron job did not finish correctly")
if not find_in_logs("nextcloud-cron", "nextcloud-cron.service: Deactivated successfully."):
raise Exception("Nextcloud cron job did not finish successfully.")
'';
if not find_in_logs("nextcloud-cron", "nextcloud-cron.service: Deactivated successfully."):
raise Exception("Nextcloud cron job did not finish successfully.")
'';
};
};
backup = lib.shb.runNixOSTest {
name = "nextcloud_backup";
nodes.server = { config, ... }: {
imports = [
basic
(lib.shb.backup config.shb.nextcloud.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.nextcloud.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -442,7 +532,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
# TODO: Test login
testScript = commonTestScript.access;
@ -460,7 +550,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -477,7 +567,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -514,77 +604,89 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
ldap = lib.shb.runNixOSTest {
name = "nextcloud_ldap";
nodes.server = { config, ... }: {
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
ldap
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
ldap
];
};
nodes.client = {
imports = [
clientLdapLogin
];
};
testScript = commonTestScript.access;
};
sso = lib.shb.runNixOSTest {
name = "nextcloud_sso";
nodes.server = { config, ... }: {
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
({ config, ... }: {
networking.hosts = {
"127.0.0.1" = [ config.test.fqdn ];
};
})
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
(
{ config, ... }:
{
networking.hosts = {
"127.0.0.1" = [ config.test.fqdn ];
};
}
)
];
};
nodes.client = {
imports = [
clientSsoLogin
({ config, ... }: {
networking.hosts = {
"192.168.1.2" = [ config.test.fqdn ];
};
})
(
{ config, ... }:
{
networking.hosts = {
"192.168.1.2" = [ config.test.fqdn ];
};
}
)
];
};
testScript = commonTestScript.access;
};
prometheus = lib.shb.runNixOSTest {
name = "nextcloud_prometheus";
nodes.server = { config, ... }: {
imports = [
basic
prometheus
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
prometheus
];
};
nodes.client = {};
nodes.client = { };
testScript = prometheusTestScript;
};

View file

@ -4,131 +4,169 @@ let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.open-webui.ssl);
waitForServices = { ... }: [
"open-webui.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
node.config.shb.open-webui.port
];
};
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/open-webui.nix
];
test = {
subdomain = "o";
};
shb.open-webui = {
enable = true;
inherit (config.test) subdomain domain;
};
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
};
https = { config, ... }: {
shb.open-webui = {
ssl = config.shb.certs.certs.selfsigned.n;
};
systemd.services.open-webui.environment = {
# Needed for open-webui to be able to talk to auth server.
SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
};
};
ldap = { config, ... }: {
shb.open-webui = {
ldap = {
userGroup = "user_group";
adminGroup = "admin_group";
};
};
};
clientLoginSso = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "o";
};
test.login = {
startUrl = "https://${config.test.fqdn}/auth";
beforeHook = ''
page.get_by_role("button", name="continue").click()
'';
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('logged in')).to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('logged in')).to_be_visible()"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text('unauthorized')).to_be_visible()"
]; }
waitForServices =
{ ... }:
[
"open-webui.service"
"nginx.service"
];
waitForPorts =
{ node, ... }:
[
node.config.shb.open-webui.port
];
};
};
sso = { config, ... }: {
shb.open-webui = {
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
clientID = "open-webui";
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/open-webui.nix
];
sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result;
test = {
subdomain = "o";
};
shb.open-webui = {
enable = true;
inherit (config.test) subdomain domain;
};
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
};
shb.hardcodedsecret.oidcSecret = {
request = config.shb.open-webui.sso.sharedSecret.request;
settings.content = oidcSecret;
https =
{ config, ... }:
{
shb.open-webui = {
ssl = config.shb.certs.certs.selfsigned.n;
};
systemd.services.open-webui.environment = {
# Needed for open-webui to be able to talk to auth server.
SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
};
};
shb.hardcodedsecret.oidcAutheliaSecret = {
request = config.shb.open-webui.sso.sharedSecretForAuthelia.request;
settings.content = oidcSecret;
ldap =
{ config, ... }:
{
shb.open-webui = {
ldap = {
userGroup = "user_group";
adminGroup = "admin_group";
};
};
};
clientLoginSso =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
virtualisation.memorySize = 4096;
test = {
subdomain = "o";
};
test.login = {
startUrl = "https://${config.test.fqdn}/auth";
beforeHook = ''
page.get_by_role("button", name="continue").click()
'';
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
];
}
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('logged in')).to_be_visible()"
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('logged in')).to_be_visible()"
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
"page.get_by_role('button', name=re.compile('Accept')).click()"
"expect(page.get_by_text('unauthorized')).to_be_visible()"
];
}
];
};
};
sso =
{ config, ... }:
{
shb.open-webui = {
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
clientID = "open-webui";
sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result;
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result;
};
};
shb.hardcodedsecret.oidcSecret = {
request = config.shb.open-webui.sso.sharedSecret.request;
settings.content = oidcSecret;
};
shb.hardcodedsecret.oidcAutheliaSecret = {
request = config.shb.open-webui.sso.sharedSecretForAuthelia.request;
settings.content = oidcSecret;
};
};
};
in
{
basic = lib.shb.runNixOSTest {
name = "open-webui_basic";
nodes.client = {};
nodes.client = { };
nodes.server = {
imports = [
basic
@ -141,14 +179,16 @@ in
backup = lib.shb.runNixOSTest {
name = "open-webui_backup";
nodes.server = { config, ... }: {
imports = [
basic
(lib.shb.backup config.shb.open-webui.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.open-webui.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -156,7 +196,7 @@ in
https = lib.shb.runNixOSTest {
name = "open-webui_https";
nodes.client = {};
nodes.client = { };
nodes.server = {
imports = [
basic
@ -176,17 +216,19 @@ in
clientLoginSso
];
};
nodes.server = { config, pkgs, ... }: {
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
testScript = commonTestScript.access;
};

View file

@ -2,134 +2,178 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.pinchflat.ssl);
waitForServices = { ... }: [
"pinchflat.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
node.config.shb.pinchflat.port
];
};
basic = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/pinchflat.nix
];
test = {
subdomain = "p";
};
shb.pinchflat = {
enable = true;
inherit (config.test) subdomain domain;
mediaDir = "/src/pinchflat";
timeZone = "America/Los_Angeles";
secretKeyBase.result = config.shb.hardcodedsecret.secretKeyBase.result;
};
systemd.tmpfiles.rules = [
"d '/src/pinchflat' 0750 pinchflat pinchflat - -"
];
# Needed for gitea-runner-local to be able to ping pinchflat.
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
shb.hardcodedsecret.secretKeyBase = {
request = config.shb.pinchflat.secretKeyBase.request;
settings.content = pkgs.lib.strings.replicate 64 "Z";
};
};
clientLogin = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = "p";
};
test.login = {
startUrl = "http://${config.test.fqdn}";
# There is no login without SSO integration.
testLoginWith = [
{ username = null; password = null; nextPageExpect = [
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
waitForServices =
{ ... }:
[
"pinchflat.service"
"nginx.service"
];
waitForPorts =
{ node, ... }:
[
node.config.shb.pinchflat.port
];
};
};
https = { config, ... }: {
shb.pinchflat = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
basic =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/pinchflat.nix
];
ldap = { config, ... }: {
shb.pinchflat = {
ldap = {
test = {
subdomain = "p";
};
shb.pinchflat = {
enable = true;
inherit (config.test) subdomain domain;
mediaDir = "/src/pinchflat";
timeZone = "America/Los_Angeles";
secretKeyBase.result = config.shb.hardcodedsecret.secretKeyBase.result;
};
userGroup = "user_group";
systemd.tmpfiles.rules = [
"d '/src/pinchflat' 0750 pinchflat pinchflat - -"
];
# Needed for gitea-runner-local to be able to ping pinchflat.
networking.hosts = {
"127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ];
};
shb.hardcodedsecret.secretKeyBase = {
request = config.shb.pinchflat.secretKeyBase.request;
settings.content = pkgs.lib.strings.replicate 64 "Z";
};
};
};
clientLoginSso = { config, ... }: {
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = "p";
};
test.login = {
startUrl = "https://${config.test.fqdn}";
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
]; }
{ username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
]; }
{ username = "charlie"; password = "CharliePassword"; nextPageExpect = [
"expect(page).to_have_url(re.compile('.*/authenticated'))"
]; }
clientLogin =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
};
};
test = {
subdomain = "p";
};
sso = { config, ... }: {
shb.pinchflat = {
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
test.login = {
startUrl = "http://${config.test.fqdn}";
# There is no login without SSO integration.
testLoginWith = [
{
username = null;
password = null;
nextPageExpect = [
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
];
}
];
};
};
https =
{ config, ... }:
{
shb.pinchflat = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
ldap =
{ config, ... }:
{
shb.pinchflat = {
ldap = {
enable = true;
userGroup = "user_group";
};
};
};
clientLoginSso =
{ config, ... }:
{
imports = [
lib.shb.baseModule
lib.shb.clientLoginModule
];
test = {
subdomain = "p";
};
test.login = {
startUrl = "https://${config.test.fqdn}";
usernameFieldLabelRegex = "Username";
passwordFieldLabelRegex = "Password";
loginButtonNameRegex = "[sS]ign [iI]n";
testLoginWith = [
{
username = "alice";
password = "NotAlicePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
];
}
{
username = "alice";
password = "AlicePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
];
}
{
username = "bob";
password = "NotBobPassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
];
}
{
username = "bob";
password = "BobPassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()"
"expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()"
"expect(page.get_by_text('Create a media profile')).to_be_visible()"
];
}
{
username = "charlie";
password = "NotCharliePassword";
nextPageExpect = [
"expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()"
];
}
{
username = "charlie";
password = "CharliePassword";
nextPageExpect = [
"expect(page).to_have_url(re.compile('.*/authenticated'))"
];
}
];
};
};
sso =
{ config, ... }:
{
shb.pinchflat = {
sso = {
enable = true;
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
};
};
};
in
{
basic = lib.shb.runNixOSTest {
@ -152,14 +196,16 @@ in
backup = lib.shb.runNixOSTest {
name = "pinchflat_backup";
nodes.server = { config, ... }: {
imports = [
basic
(lib.shb.backup config.shb.pinchflat.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
basic
(lib.shb.backup config.shb.pinchflat.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};
@ -191,17 +237,19 @@ in
clientLoginSso
];
};
nodes.server = { config, pkgs, ... }: {
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.server =
{ config, pkgs, ... }:
{
imports = [
basic
lib.shb.certs
https
lib.shb.ldap
ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
testScript = commonTestScript.access.override {
redirectSSO = true;

View file

@ -2,14 +2,18 @@
let
commonTestScript = lib.shb.mkScripts {
hasSSL = { node, ... }: !(isNull node.config.shb.vaultwarden.ssl);
waitForServices = { ... }: [
"vaultwarden.service"
"nginx.service"
];
waitForPorts = { node, ... }: [
8222
5432
];
waitForServices =
{ ... }:
[
"vaultwarden.service"
"nginx.service"
];
waitForPorts =
{ node, ... }:
[
8222
5432
];
# to get the get token test to succeed we need:
# 1. add group Vaultwarden_admin to LLDAP
# 2. add an Authelia user with to that group
@ -17,58 +21,64 @@ let
# 4. go to the Vaultwarden /admin endpoint
# 5. create a Vaultwarden user
# 6. now login with that new user to Vaultwarden
extraScript = { node, proto_fqdn, ... }: ''
with subtest("prelogin"):
response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """
{"email": "me@example.com"}
"""))
print(response)
if 'kdf' not in response:
raise Exception("Unrecognized response: {}".format(response))
extraScript =
{ node, proto_fqdn, ... }:
''
with subtest("prelogin"):
response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """
{"email": "me@example.com"}
"""))
print(response)
if 'kdf' not in response:
raise Exception("Unrecognized response: {}".format(response))
with subtest("get token"):
response = curl(client, "", "${proto_fqdn}/identity/connect/token", data=unline_with("", """
scope=api%20offline_access
&client_id=web
&deviceType=10
&deviceIdentifier=a60323bf-4686-4b4d-96e0-3c241fa5581c
&deviceName=firefox
&grant_type=password&username=me
&password=mypassword
"""))
print(response)
if response["message"] != "Username or password is incorrect. Try again":
raise Exception("Unrecognized response: {}".format(response))
'';
with subtest("get token"):
response = curl(client, "", "${proto_fqdn}/identity/connect/token", data=unline_with("", """
scope=api%20offline_access
&client_id=web
&deviceType=10
&deviceIdentifier=a60323bf-4686-4b4d-96e0-3c241fa5581c
&deviceName=firefox
&grant_type=password&username=me
&password=mypassword
"""))
print(response)
if response["message"] != "Username or password is incorrect. Try again":
raise Exception("Unrecognized response: {}".format(response))
'';
};
basic = { config, ... }: {
test = {
subdomain = "v";
basic =
{ config, ... }:
{
test = {
subdomain = "v";
};
shb.vaultwarden = {
enable = true;
inherit (config.test) subdomain domain;
port = 8222;
databasePassword.result = config.shb.hardcodedsecret.passphrase.result;
};
shb.hardcodedsecret.passphrase = {
request = config.shb.vaultwarden.databasePassword.request;
settings.content = "PassPhrase";
};
# networking.hosts = {
# "127.0.0.1" = [ fqdn ];
# };
};
shb.vaultwarden = {
enable = true;
inherit (config.test) subdomain domain;
port = 8222;
databasePassword.result = config.shb.hardcodedsecret.passphrase.result;
https =
{ config, ... }:
{
shb.vaultwarden = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
shb.hardcodedsecret.passphrase = {
request = config.shb.vaultwarden.databasePassword.request;
settings.content = "PassPhrase";
};
# networking.hosts = {
# "127.0.0.1" = [ fqdn ];
# };
};
https = { config, ... }: {
shb.vaultwarden = {
ssl = config.shb.certs.certs.selfsigned.n;
};
};
# Not yet supported
# ldap = { config, ... }: {
@ -78,11 +88,13 @@ let
# # };
# };
sso = { config, ... }: {
shb.vaultwarden = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
sso =
{ config, ... }:
{
shb.vaultwarden = {
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
};
};
};
in
{
basic = lib.shb.runNixOSTest {
@ -97,7 +109,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -116,7 +128,7 @@ in
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access;
};
@ -126,7 +138,7 @@ in
# ldap = lib.shb.runNixOSTest {
# name = "vaultwarden_ldap";
#
# nodes.server = lib.mkMerge [
# nodes.server = lib.mkMerge [
# lib.shb.baseModule
# ../../modules/blocks/hardcodedsecret.nix
# ../../modules/services/vaultwarden.nix
@ -142,56 +154,64 @@ in
sso = lib.shb.runNixOSTest {
name = "vaultwarden_sso";
nodes.server = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/vaultwarden.nix
lib.shb.certs
basic
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.server =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/vaultwarden.nix
lib.shb.certs
basic
https
lib.shb.ldap
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
sso
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.access.override {
waitForPorts = { node, ... }: [
8222
5432
9091
];
extraScript = { node, proto_fqdn, ... }: ''
with subtest("unauthenticated access is not granted to /admin"):
response = curl(client, """{"code":%{response_code},"auth_host":"%{urle.host}","auth_query":"%{urle.query}","all":%{json}}""", "${proto_fqdn}/admin")
waitForPorts =
{ node, ... }:
[
8222
5432
9091
];
extraScript =
{ node, proto_fqdn, ... }:
''
with subtest("unauthenticated access is not granted to /admin"):
response = curl(client, """{"code":%{response_code},"auth_host":"%{urle.host}","auth_query":"%{urle.query}","all":%{json}}""", "${proto_fqdn}/admin")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
if response['auth_host'] != "auth.${node.config.test.domain}":
raise Exception(f"auth host should be auth.${node.config.test.domain} but is {response['auth_host']}")
if response['auth_query'] != "rd=${proto_fqdn}/admin":
raise Exception(f"auth query should be rd=${proto_fqdn}/admin but is {response['auth_query']}")
'';
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
if response['auth_host'] != "auth.${node.config.test.domain}":
raise Exception(f"auth host should be auth.${node.config.test.domain} but is {response['auth_host']}")
if response['auth_query'] != "rd=${proto_fqdn}/admin":
raise Exception(f"auth query should be rd=${proto_fqdn}/admin but is {response['auth_query']}")
'';
};
};
backup = lib.shb.runNixOSTest {
name = "vaultwarden_backup";
nodes.server = { config, ... }: {
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/vaultwarden.nix
basic
(lib.shb.backup config.shb.vaultwarden.backup)
];
};
nodes.server =
{ config, ... }:
{
imports = [
lib.shb.baseModule
../../modules/blocks/hardcodedsecret.nix
../../modules/services/vaultwarden.nix
basic
(lib.shb.backup config.shb.vaultwarden.backup)
];
};
nodes.client = {};
nodes.client = { };
testScript = commonTestScript.backup;
};