chore: format all files
This commit is contained in:
parent
16fcb94897
commit
48802553de
77 changed files with 10265 additions and 8177 deletions
|
|
@ -5,17 +5,17 @@ let
|
||||||
targetPort = 2222;
|
targetPort = 2222;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports =
|
imports = [
|
||||||
[ # Include the results of the hardware scan.
|
# Include the results of the hardware scan.
|
||||||
./hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.loader.grub.enable = true;
|
boot.loader.grub.enable = true;
|
||||||
boot.kernelModules = [ "kvm-intel" ];
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
system.stateVersion = "22.11";
|
system.stateVersion = "22.11";
|
||||||
|
|
||||||
# Options above are generate by running nixos-generate-config on the VM.
|
# Options above are generate by running nixos-generate-config on the VM.
|
||||||
|
|
||||||
# Needed otherwise deploy will say system won't be able to boot.
|
# Needed otherwise deploy will say system won't be able to boot.
|
||||||
boot.loader.grub.device = "/dev/vdb";
|
boot.loader.grub.device = "/dev/vdb";
|
||||||
# Needed to avoid getting into not available disk space in /boot.
|
# 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.
|
# Options above are needed to deploy in a VM.
|
||||||
|
|
||||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
nix.settings.experimental-features = [
|
||||||
|
"nix-command"
|
||||||
|
"flakes"
|
||||||
|
];
|
||||||
|
|
||||||
# We need to create the user we will deploy with.
|
# We need to create the user we will deploy with.
|
||||||
users.users.${targetUser} = {
|
users.users.${targetUser} = {
|
||||||
|
|
@ -41,9 +44,11 @@ in
|
||||||
|
|
||||||
# The user we're deploying with must be able to run sudo without password.
|
# The user we're deploying with must be able to run sudo without password.
|
||||||
security.sudo.extraRules = [
|
security.sudo.extraRules = [
|
||||||
{ users = [ targetUser ];
|
{
|
||||||
|
users = [ targetUser ];
|
||||||
commands = [
|
commands = [
|
||||||
{ command = "ALL";
|
{
|
||||||
|
command = "ALL";
|
||||||
options = [ "NOPASSWD" ];
|
options = [ "NOPASSWD" ];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,148 +6,161 @@
|
||||||
sops-nix.url = "github:Mic92/sops-nix";
|
sops-nix.url = "github:Mic92/sops-nix";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs@{ self, selfhostblocks, sops-nix }:
|
outputs =
|
||||||
|
inputs@{
|
||||||
|
self,
|
||||||
|
selfhostblocks,
|
||||||
|
sops-nix,
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
|
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
|
||||||
inherit (selfhostblocks.lib.${system}) pkgs;
|
inherit (selfhostblocks.lib.${system}) pkgs;
|
||||||
|
|
||||||
basic = { config, ... }: {
|
basic =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
./configuration.nix
|
{
|
||||||
selfhostblocks.nixosModules.authelia
|
imports = [
|
||||||
selfhostblocks.nixosModules.home-assistant
|
./configuration.nix
|
||||||
selfhostblocks.nixosModules.sops
|
selfhostblocks.nixosModules.authelia
|
||||||
selfhostblocks.nixosModules.ssl
|
selfhostblocks.nixosModules.home-assistant
|
||||||
sops-nix.nixosModules.default
|
selfhostblocks.nixosModules.sops
|
||||||
];
|
selfhostblocks.nixosModules.ssl
|
||||||
|
sops-nix.nixosModules.default
|
||||||
|
];
|
||||||
|
|
||||||
sops.defaultSopsFile = ./secrets.yaml;
|
sops.defaultSopsFile = ./secrets.yaml;
|
||||||
|
|
||||||
shb.home-assistant = {
|
shb.home-assistant = {
|
||||||
enable = true;
|
enable = true;
|
||||||
domain = "example.com";
|
domain = "example.com";
|
||||||
subdomain = "ha";
|
subdomain = "ha";
|
||||||
config = {
|
config = {
|
||||||
name = "SHB Home Assistant";
|
name = "SHB Home Assistant";
|
||||||
country.source = config.shb.sops.secret."home-assistant/country".result.path;
|
country.source = config.shb.sops.secret."home-assistant/country".result.path;
|
||||||
latitude.source = config.shb.sops.secret."home-assistant/latitude".result.path;
|
latitude.source = config.shb.sops.secret."home-assistant/latitude".result.path;
|
||||||
longitude.source = config.shb.sops.secret."home-assistant/longitude".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;
|
time_zone.source = config.shb.sops.secret."home-assistant/time_zone".result.path;
|
||||||
unit_system = "metric";
|
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 = {
|
sopsConfig = {
|
||||||
sops.age.keyFile = "/etc/sops/my_key";
|
sops.age.keyFile = "/etc/sops/my_key";
|
||||||
environment.etc."sops/my_key".source = ./keys.txt;
|
environment.etc."sops/my_key".source = ./keys.txt;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
nixosConfigurations = {
|
nixosConfigurations = {
|
||||||
basic = pkgs.nixosSystem {
|
basic = pkgs.nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
modules = [
|
modules = [
|
||||||
basic
|
basic
|
||||||
sopsConfig
|
sopsConfig
|
||||||
];
|
];
|
||||||
};
|
|
||||||
|
|
||||||
ldap = pkgs.nixosSystem {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = [
|
|
||||||
basic
|
|
||||||
ldap
|
|
||||||
sopsConfig
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
colmena = {
|
ldap = pkgs.nixosSystem {
|
||||||
meta = {
|
system = "x86_64-linux";
|
||||||
nixpkgs = import nixpkgs' {
|
modules = [
|
||||||
system = "x86_64-linux";
|
basic
|
||||||
};
|
ldap
|
||||||
specialArgs = inputs;
|
sopsConfig
|
||||||
};
|
];
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,52 +3,65 @@
|
||||||
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
||||||
# and may be overwritten by future invocations. Please make changes
|
# and may be overwritten by future invocations. Please make changes
|
||||||
# to /etc/nixos/configuration.nix instead.
|
# to /etc/nixos/configuration.nix instead.
|
||||||
{ config, lib, pkgs, modulesPath, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
modulesPath,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
{
|
{
|
||||||
imports =
|
imports = [
|
||||||
[ (modulesPath + "/profiles/qemu-guest.nix")
|
(modulesPath + "/profiles/qemu-guest.nix")
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ];
|
boot.initrd.availableKernelModules = [
|
||||||
|
"ata_piix"
|
||||||
|
"uhci_hcd"
|
||||||
|
"virtio_pci"
|
||||||
|
"floppy"
|
||||||
|
"sr_mod"
|
||||||
|
"virtio_blk"
|
||||||
|
];
|
||||||
boot.initrd.kernelModules = [ ];
|
boot.initrd.kernelModules = [ ];
|
||||||
boot.kernelModules = [ "kvm-intel" ];
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
boot.extraModulePackages = [ ];
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
fileSystems."/" =
|
fileSystems."/" = {
|
||||||
{ device = "/dev/vda";
|
device = "/dev/vda";
|
||||||
fsType = "ext4";
|
fsType = "ext4";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/nix/.ro-store" =
|
fileSystems."/nix/.ro-store" = {
|
||||||
{ device = "nix-store";
|
device = "nix-store";
|
||||||
fsType = "9p";
|
fsType = "9p";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/nix/.rw-store" =
|
fileSystems."/nix/.rw-store" = {
|
||||||
{ device = "tmpfs";
|
device = "tmpfs";
|
||||||
fsType = "tmpfs";
|
fsType = "tmpfs";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/tmp/shared" =
|
fileSystems."/tmp/shared" = {
|
||||||
{ device = "shared";
|
device = "shared";
|
||||||
fsType = "9p";
|
fsType = "9p";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/tmp/xchg" =
|
fileSystems."/tmp/xchg" = {
|
||||||
{ device = "xchg";
|
device = "xchg";
|
||||||
fsType = "9p";
|
fsType = "9p";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/nix/store" =
|
fileSystems."/nix/store" = {
|
||||||
{ device = "overlay";
|
device = "overlay";
|
||||||
fsType = "overlay";
|
fsType = "overlay";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/boot" =
|
fileSystems."/boot" = {
|
||||||
{ device = "/dev/vdb2";
|
device = "/dev/vdb2";
|
||||||
fsType = "vfat";
|
fsType = "vfat";
|
||||||
};
|
};
|
||||||
|
|
||||||
swapDevices = [ ];
|
swapDevices = [ ];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ let
|
||||||
targetPort = 2222;
|
targetPort = 2222;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports =
|
imports = [
|
||||||
[ # Include the results of the hardware scan.
|
# Include the results of the hardware scan.
|
||||||
./hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.loader.grub.enable = true;
|
boot.loader.grub.enable = true;
|
||||||
boot.kernelModules = [ "kvm-intel" ];
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
system.stateVersion = "22.11";
|
system.stateVersion = "22.11";
|
||||||
|
|
||||||
# Options above are generate by running nixos-generate-config on the VM.
|
# Options above are generate by running nixos-generate-config on the VM.
|
||||||
|
|
||||||
# Needed otherwise deploy will say system won't be able to boot.
|
# Needed otherwise deploy will say system won't be able to boot.
|
||||||
boot.loader.grub.device = "/dev/vdb";
|
boot.loader.grub.device = "/dev/vdb";
|
||||||
# Needed to avoid getting into not available disk space in /boot.
|
# 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.
|
# Options above are needed to deploy in a VM.
|
||||||
|
|
||||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
nix.settings.experimental-features = [
|
||||||
|
"nix-command"
|
||||||
|
"flakes"
|
||||||
|
];
|
||||||
|
|
||||||
# We need to create the user we will deploy with.
|
# We need to create the user we will deploy with.
|
||||||
users.users.${targetUser} = {
|
users.users.${targetUser} = {
|
||||||
|
|
@ -42,9 +45,11 @@ in
|
||||||
|
|
||||||
# The user we're deploying with must be able to run sudo without password.
|
# The user we're deploying with must be able to run sudo without password.
|
||||||
security.sudo.extraRules = [
|
security.sudo.extraRules = [
|
||||||
{ users = [ targetUser ];
|
{
|
||||||
|
users = [ targetUser ];
|
||||||
commands = [
|
commands = [
|
||||||
{ command = "ALL";
|
{
|
||||||
|
command = "ALL";
|
||||||
options = [ "NOPASSWD" ];
|
options = [ "NOPASSWD" ];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -6,236 +6,256 @@
|
||||||
sops-nix.url = "github:Mic92/sops-nix";
|
sops-nix.url = "github:Mic92/sops-nix";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs@{ self, selfhostblocks, sops-nix }:
|
outputs =
|
||||||
|
inputs@{
|
||||||
|
self,
|
||||||
|
selfhostblocks,
|
||||||
|
sops-nix,
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
|
nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs;
|
||||||
inherit (selfhostblocks.lib.${system}) pkgs;
|
inherit (selfhostblocks.lib.${system}) pkgs;
|
||||||
|
|
||||||
basic = { config, ... }: {
|
basic =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
./configuration.nix
|
{
|
||||||
selfhostblocks.nixosModules.authelia
|
imports = [
|
||||||
selfhostblocks.nixosModules.nextcloud-server
|
./configuration.nix
|
||||||
selfhostblocks.nixosModules.nginx
|
selfhostblocks.nixosModules.authelia
|
||||||
selfhostblocks.nixosModules.sops
|
selfhostblocks.nixosModules.nextcloud-server
|
||||||
selfhostblocks.nixosModules.ssl
|
selfhostblocks.nixosModules.nginx
|
||||||
sops-nix.nixosModules.default
|
selfhostblocks.nixosModules.sops
|
||||||
];
|
selfhostblocks.nixosModules.ssl
|
||||||
|
sops-nix.nixosModules.default
|
||||||
|
];
|
||||||
|
|
||||||
sops.defaultSopsFile = ./secrets.yaml;
|
sops.defaultSopsFile = ./secrets.yaml;
|
||||||
|
|
||||||
shb.nextcloud = {
|
shb.nextcloud = {
|
||||||
enable = true;
|
enable = true;
|
||||||
domain = "example.com";
|
domain = "example.com";
|
||||||
subdomain = "n";
|
subdomain = "n";
|
||||||
dataDir = "/var/lib/nextcloud";
|
dataDir = "/var/lib/nextcloud";
|
||||||
tracing = null;
|
tracing = null;
|
||||||
defaultPhoneRegion = "US";
|
defaultPhoneRegion = "US";
|
||||||
|
|
||||||
# This option is only needed because we do not access Nextcloud at the default port in the VM.
|
# This option is only needed because we do not access Nextcloud at the default port in the VM.
|
||||||
port = 8080;
|
port = 8080;
|
||||||
|
|
||||||
adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result;
|
adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result;
|
||||||
|
|
||||||
apps = {
|
apps = {
|
||||||
previewgenerator.enable = true;
|
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";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request;
|
||||||
shb.nextcloud = {
|
|
||||||
port = lib.mkForce null;
|
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
|
||||||
};
|
|
||||||
shb.lldap.ssl = config.shb.certs.certs.selfsigned.n;
|
|
||||||
|
|
||||||
services.dnsmasq = {
|
# Set to true for more debug info with `journalctl -f -u nginx`.
|
||||||
enable = true;
|
shb.nginx.accessLog = true;
|
||||||
settings = {
|
shb.nginx.debugLog = false;
|
||||||
domain-needed = true;
|
};
|
||||||
# no-resolv = true;
|
|
||||||
bogus-priv = true;
|
ldap =
|
||||||
address =
|
{ config, ... }:
|
||||||
map (hostname: "/${hostname}/127.0.0.1") [
|
{
|
||||||
|
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"
|
"example.com"
|
||||||
"n.example.com"
|
"n.example.com"
|
||||||
"ldap.example.com"
|
"ldap.example.com"
|
||||||
"auth.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 = {
|
sopsConfig = {
|
||||||
sops.age.keyFile = "/etc/sops/my_key";
|
sops.age.keyFile = "/etc/sops/my_key";
|
||||||
environment.etc."sops/my_key".source = ./keys.txt;
|
environment.etc."sops/my_key".source = ./keys.txt;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
nixosConfigurations = {
|
nixosConfigurations = {
|
||||||
basic = pkgs.nixosSystem {
|
basic = pkgs.nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
modules = [
|
modules = [
|
||||||
sopsConfig
|
sopsConfig
|
||||||
basic
|
basic
|
||||||
];
|
];
|
||||||
};
|
|
||||||
ldap = pkgs.nixosSystem {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = [
|
|
||||||
sopsConfig
|
|
||||||
basic
|
|
||||||
ldap
|
|
||||||
];
|
|
||||||
};
|
|
||||||
sso = pkgs.nixosSystem {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = [
|
|
||||||
sopsConfig
|
|
||||||
basic
|
|
||||||
ldap
|
|
||||||
sso
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
ldap = pkgs.nixosSystem {
|
||||||
colmena = {
|
system = "x86_64-linux";
|
||||||
meta = {
|
modules = [
|
||||||
nixpkgs = import nixpkgs' {
|
sopsConfig
|
||||||
system = "x86_64-linux";
|
basic
|
||||||
};
|
ldap
|
||||||
specialArgs = inputs;
|
];
|
||||||
};
|
};
|
||||||
|
sso = pkgs.nixosSystem {
|
||||||
basic = { config, ... }: {
|
system = "x86_64-linux";
|
||||||
imports = [
|
modules = [
|
||||||
basic
|
sopsConfig
|
||||||
];
|
basic
|
||||||
|
ldap
|
||||||
deployment = {
|
sso
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,52 +3,65 @@
|
||||||
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
# Do not modify this file! It was generated by ‘nixos-generate-config’
|
||||||
# and may be overwritten by future invocations. Please make changes
|
# and may be overwritten by future invocations. Please make changes
|
||||||
# to /etc/nixos/configuration.nix instead.
|
# to /etc/nixos/configuration.nix instead.
|
||||||
{ config, lib, pkgs, modulesPath, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
modulesPath,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
{
|
{
|
||||||
imports =
|
imports = [
|
||||||
[ (modulesPath + "/profiles/qemu-guest.nix")
|
(modulesPath + "/profiles/qemu-guest.nix")
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ];
|
boot.initrd.availableKernelModules = [
|
||||||
|
"ata_piix"
|
||||||
|
"uhci_hcd"
|
||||||
|
"virtio_pci"
|
||||||
|
"floppy"
|
||||||
|
"sr_mod"
|
||||||
|
"virtio_blk"
|
||||||
|
];
|
||||||
boot.initrd.kernelModules = [ ];
|
boot.initrd.kernelModules = [ ];
|
||||||
boot.kernelModules = [ "kvm-intel" ];
|
boot.kernelModules = [ "kvm-intel" ];
|
||||||
boot.extraModulePackages = [ ];
|
boot.extraModulePackages = [ ];
|
||||||
|
|
||||||
fileSystems."/" =
|
fileSystems."/" = {
|
||||||
{ device = "/dev/vda";
|
device = "/dev/vda";
|
||||||
fsType = "ext4";
|
fsType = "ext4";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/nix/.ro-store" =
|
fileSystems."/nix/.ro-store" = {
|
||||||
{ device = "nix-store";
|
device = "nix-store";
|
||||||
fsType = "9p";
|
fsType = "9p";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/nix/.rw-store" =
|
fileSystems."/nix/.rw-store" = {
|
||||||
{ device = "tmpfs";
|
device = "tmpfs";
|
||||||
fsType = "tmpfs";
|
fsType = "tmpfs";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/tmp/shared" =
|
fileSystems."/tmp/shared" = {
|
||||||
{ device = "shared";
|
device = "shared";
|
||||||
fsType = "9p";
|
fsType = "9p";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/tmp/xchg" =
|
fileSystems."/tmp/xchg" = {
|
||||||
{ device = "xchg";
|
device = "xchg";
|
||||||
fsType = "9p";
|
fsType = "9p";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/nix/store" =
|
fileSystems."/nix/store" = {
|
||||||
{ device = "overlay";
|
device = "overlay";
|
||||||
fsType = "overlay";
|
fsType = "overlay";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems."/boot" =
|
fileSystems."/boot" = {
|
||||||
{ device = "/dev/vdb2";
|
device = "/dev/vdb2";
|
||||||
fsType = "vfat";
|
fsType = "vfat";
|
||||||
};
|
};
|
||||||
|
|
||||||
swapDevices = [ ];
|
swapDevices = [ ];
|
||||||
|
|
||||||
|
|
|
||||||
167
docs/default.nix
167
docs/default.nix
|
|
@ -1,40 +1,49 @@
|
||||||
# Taken nearly verbatim from https://github.com/nix-community/home-manager/pull/4673
|
# Taken nearly verbatim from https://github.com/nix-community/home-manager/pull/4673
|
||||||
# Read these docs online at https://shb.skarabox.com.
|
# Read these docs online at https://shb.skarabox.com.
|
||||||
{ pkgs
|
{
|
||||||
, buildPackages
|
pkgs,
|
||||||
, lib
|
buildPackages,
|
||||||
, nmdsrc
|
lib,
|
||||||
, stdenv
|
nmdsrc,
|
||||||
, documentation-highlighter
|
stdenv,
|
||||||
, nixos-render-docs
|
documentation-highlighter,
|
||||||
|
nixos-render-docs,
|
||||||
|
|
||||||
, release
|
release,
|
||||||
, allModules
|
allModules,
|
||||||
|
|
||||||
, version ? builtins.readFile ../VERSION
|
version ? builtins.readFile ../VERSION,
|
||||||
, substituteVersionIn
|
substituteVersionIn,
|
||||||
|
|
||||||
, modules
|
modules,
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
shbPath = toString ./..;
|
shbPath = toString ./..;
|
||||||
|
|
||||||
gitHubDeclaration = user: repo: subpath:
|
gitHubDeclaration =
|
||||||
let urlRef = "main";
|
user: repo: subpath:
|
||||||
end = if subpath == "" then "" else "/" + subpath;
|
let
|
||||||
in {
|
urlRef = "main";
|
||||||
|
end = if subpath == "" then "" else "/" + subpath;
|
||||||
|
in
|
||||||
|
{
|
||||||
url = "https://github.com/${user}/${repo}/blob/${urlRef}${end}";
|
url = "https://github.com/${user}/${repo}/blob/${urlRef}${end}";
|
||||||
name = "<${repo}${end}>";
|
name = "<${repo}${end}>";
|
||||||
};
|
};
|
||||||
|
|
||||||
ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url;
|
ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url;
|
||||||
|
|
||||||
buildOptionsDocs = { modules, filterOptionPath ? null }: args:
|
buildOptionsDocs =
|
||||||
|
{
|
||||||
|
modules,
|
||||||
|
filterOptionPath ? null,
|
||||||
|
}:
|
||||||
|
args:
|
||||||
let
|
let
|
||||||
config = {
|
config = {
|
||||||
_module.check = false;
|
_module.check = false;
|
||||||
_module.args = {};
|
_module.args = { };
|
||||||
system.stateVersion = "22.11";
|
system.stateVersion = "22.11";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -52,41 +61,56 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
options = lib.setAttrByPath filterOptionPath (lib.getAttrFromPath filterOptionPath eval.options);
|
options = lib.setAttrByPath filterOptionPath (lib.getAttrFromPath filterOptionPath eval.options);
|
||||||
in buildPackages.nixosOptionsDoc ({
|
in
|
||||||
inherit options;
|
buildPackages.nixosOptionsDoc (
|
||||||
|
{
|
||||||
|
inherit options;
|
||||||
|
|
||||||
transformOptions = opt:
|
transformOptions =
|
||||||
opt // {
|
opt:
|
||||||
# Clean up declaration sites to not refer to the Home Manager
|
opt
|
||||||
# source tree.
|
// {
|
||||||
declarations = map (decl:
|
# Clean up declaration sites to not refer to the Home Manager
|
||||||
gitHubDeclaration "ibizaman" "selfhostblocks"
|
# source tree.
|
||||||
(lib.removePrefix "/" (lib.removePrefix shbPath (toString decl)))) opt.declarations;
|
declarations = map (
|
||||||
};
|
decl:
|
||||||
} // builtins.removeAttrs args [ "includeModuleSystemOptions" ]);
|
gitHubDeclaration "ibizaman" "selfhostblocks" (
|
||||||
|
lib.removePrefix "/" (lib.removePrefix shbPath (toString decl))
|
||||||
|
)
|
||||||
|
) opt.declarations;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// builtins.removeAttrs args [ "includeModuleSystemOptions" ]
|
||||||
|
);
|
||||||
|
|
||||||
scrubbedModule = {
|
scrubbedModule = {
|
||||||
_module.args.pkgs = lib.mkForce (nmd.scrubDerivations "pkgs" pkgs);
|
_module.args.pkgs = lib.mkForce (nmd.scrubDerivations "pkgs" pkgs);
|
||||||
_module.check = false;
|
_module.check = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
allOptionsDocs = paths: (buildOptionsDocs
|
allOptionsDocs =
|
||||||
{
|
paths:
|
||||||
modules = paths ++ allModules ++ [ scrubbedModule ];
|
(buildOptionsDocs
|
||||||
filterOptionPath = [ "shb" ];
|
{
|
||||||
}
|
modules = paths ++ allModules ++ [ scrubbedModule ];
|
||||||
{
|
filterOptionPath = [ "shb" ];
|
||||||
variablelistId = "selfhostblocks-options";
|
}
|
||||||
}).optionsJSON;
|
{
|
||||||
|
variablelistId = "selfhostblocks-options";
|
||||||
|
}
|
||||||
|
).optionsJSON;
|
||||||
|
|
||||||
individualModuleOptionsDocs = filterOptionPath: paths: (buildOptionsDocs
|
individualModuleOptionsDocs =
|
||||||
{
|
filterOptionPath: paths:
|
||||||
modules = paths ++ [ scrubbedModule ];
|
(buildOptionsDocs
|
||||||
inherit filterOptionPath;
|
{
|
||||||
}
|
modules = paths ++ [ scrubbedModule ];
|
||||||
{
|
inherit filterOptionPath;
|
||||||
variablelistId = "selfhostblocks-options";
|
}
|
||||||
}).optionsJSON;
|
{
|
||||||
|
variablelistId = "selfhostblocks-options";
|
||||||
|
}
|
||||||
|
).optionsJSON;
|
||||||
|
|
||||||
nmd = import nmdsrc {
|
nmd = import nmdsrc {
|
||||||
inherit lib;
|
inherit lib;
|
||||||
|
|
@ -94,15 +118,15 @@ let
|
||||||
# `nmd` uses to work around the broken stylesheets in
|
# `nmd` uses to work around the broken stylesheets in
|
||||||
# `docbook-xsl-ns`, so we restore the patched version here.
|
# `docbook-xsl-ns`, so we restore the patched version here.
|
||||||
pkgs = pkgs // {
|
pkgs = pkgs // {
|
||||||
docbook-xsl-ns =
|
docbook-xsl-ns = pkgs.docbook-xsl-ns.override { withManOptDedupPatch = true; };
|
||||||
pkgs.docbook-xsl-ns.override { withManOptDedupPatch = true; };
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputPath = "share/doc/selfhostblocks";
|
outputPath = "share/doc/selfhostblocks";
|
||||||
|
|
||||||
manpage-urls = pkgs.writeText "manpage-urls.json" ''{}'';
|
manpage-urls = pkgs.writeText "manpage-urls.json" ''{}'';
|
||||||
in stdenv.mkDerivation {
|
in
|
||||||
|
stdenv.mkDerivation {
|
||||||
name = "self-host-blocks-manual";
|
name = "self-host-blocks-manual";
|
||||||
|
|
||||||
nativeBuildInputs = [ nixos-render-docs ];
|
nativeBuildInputs = [ nixos-render-docs ];
|
||||||
|
|
@ -135,28 +159,41 @@ in stdenv.mkDerivation {
|
||||||
${nmdsrc}/static/highlightjs/highlight.load.js
|
${nmdsrc}/static/highlightjs/highlight.load.js
|
||||||
|
|
||||||
''
|
''
|
||||||
+ lib.concatStringsSep "\n" (map (m: ''
|
+ lib.concatStringsSep "\n" (
|
||||||
substituteInPlace ${m} --replace '@VERSION@' ${version}
|
map (m: ''
|
||||||
'') substituteVersionIn)
|
substituteInPlace ${m} --replace '@VERSION@' ${version}
|
||||||
|
'') substituteVersionIn
|
||||||
|
)
|
||||||
+ ''
|
+ ''
|
||||||
substituteInPlace ./options.md \
|
substituteInPlace ./options.md \
|
||||||
--replace \
|
--replace \
|
||||||
'@OPTIONS_JSON@' \
|
'@OPTIONS_JSON@' \
|
||||||
${allOptionsDocs [
|
${
|
||||||
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix")
|
allOptionsDocs [
|
||||||
]}/share/doc/nixos/options.json
|
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix")
|
||||||
|
]
|
||||||
|
}/share/doc/nixos/options.json
|
||||||
''
|
''
|
||||||
+ lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cfg':
|
+ lib.concatStringsSep "\n" (
|
||||||
let
|
lib.mapAttrsToList (
|
||||||
cfg = if builtins.isAttrs cfg' then cfg' else { module = cfg'; };
|
name: cfg':
|
||||||
module = if builtins.isList cfg.module then cfg.module else [ cfg.module ];
|
let
|
||||||
optionRoot = cfg.optionRoot or [ "shb" (lib.last (lib.splitString "/" name)) ];
|
cfg = if builtins.isAttrs cfg' then cfg' else { module = cfg'; };
|
||||||
in ''
|
module = if builtins.isList cfg.module then cfg.module else [ cfg.module ];
|
||||||
substituteInPlace ./modules/${name}/docs/default.md \
|
optionRoot =
|
||||||
--replace-fail \
|
cfg.optionRoot or [
|
||||||
'@OPTIONS_JSON@' \
|
"shb"
|
||||||
${individualModuleOptionsDocs optionRoot module}/share/doc/nixos/options.json
|
(lib.last (lib.splitString "/" name))
|
||||||
'') modules)
|
];
|
||||||
|
in
|
||||||
|
''
|
||||||
|
substituteInPlace ./modules/${name}/docs/default.md \
|
||||||
|
--replace-fail \
|
||||||
|
'@OPTIONS_JSON@' \
|
||||||
|
${individualModuleOptionsDocs optionRoot module}/share/doc/nixos/options.json
|
||||||
|
''
|
||||||
|
) modules
|
||||||
|
)
|
||||||
+ ''
|
+ ''
|
||||||
find . -name "*.md" -print0 | \
|
find . -name "*.md" -print0 | \
|
||||||
while IFS= read -r -d ''' f; do
|
while IFS= read -r -d ''' f; do
|
||||||
|
|
|
||||||
555
flake.nix
555
flake.nix
|
|
@ -11,66 +11,89 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = inputs@{ self, nixpkgs, nix-flake-tests, flake-utils, nmdsrc, ... }: flake-utils.lib.eachDefaultSystem (system:
|
outputs =
|
||||||
let
|
inputs@{
|
||||||
originPkgs = nixpkgs.legacyPackages.${system};
|
self,
|
||||||
shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [
|
nixpkgs,
|
||||||
# Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged.
|
nix-flake-tests,
|
||||||
./patches/lldap.patch
|
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.
|
# Leaving commented out as an example.
|
||||||
# (originPkgs.fetchpatch {
|
# (originPkgs.fetchpatch {
|
||||||
# url = "https://github.com/NixOS/nixpkgs/pull/317107.patch";
|
# url = "https://github.com/NixOS/nixpkgs/pull/317107.patch";
|
||||||
# hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk=";
|
# 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;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
};
|
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.
|
# The contract dummies are used to show options for contracts.
|
||||||
contractDummyModules = [
|
contractDummyModules = [
|
||||||
modules/contracts/backup/dummyModule.nix
|
modules/contracts/backup/dummyModule.nix
|
||||||
modules/contracts/ssl/dummyModule.nix
|
modules/contracts/ssl/dummyModule.nix
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
formatter = pkgs.nixfmt-tree;
|
formatter = pkgs.nixfmt-tree;
|
||||||
|
|
||||||
packages.manualHtml = pkgs.callPackage ./docs {
|
packages.manualHtml = pkgs.callPackage ./docs {
|
||||||
inherit nmdsrc;
|
inherit nmdsrc;
|
||||||
allModules = self.nixosModules.default.imports
|
allModules =
|
||||||
++ [
|
self.nixosModules.default.imports
|
||||||
self.nixosModules.sops
|
++ [
|
||||||
] ++ contractDummyModules;
|
self.nixosModules.sops
|
||||||
|
]
|
||||||
|
++ contractDummyModules;
|
||||||
release = builtins.readFile ./VERSION;
|
release = builtins.readFile ./VERSION;
|
||||||
|
|
||||||
substituteVersionIn = [
|
substituteVersionIn = [
|
||||||
|
|
@ -82,7 +105,10 @@
|
||||||
"blocks/lldap" = ./modules/blocks/lldap.nix;
|
"blocks/lldap" = ./modules/blocks/lldap.nix;
|
||||||
"blocks/ssl" = {
|
"blocks/ssl" = {
|
||||||
module = ./modules/blocks/ssl.nix;
|
module = ./modules/blocks/ssl.nix;
|
||||||
optionRoot = [ "shb" "certs" ];
|
optionRoot = [
|
||||||
|
"shb"
|
||||||
|
"certs"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"blocks/mitmdump" = ./modules/blocks/mitmdump.nix;
|
"blocks/mitmdump" = ./modules/blocks/mitmdump.nix;
|
||||||
"blocks/monitoring" = ./modules/blocks/monitoring.nix;
|
"blocks/monitoring" = ./modules/blocks/monitoring.nix;
|
||||||
|
|
@ -99,154 +125,196 @@
|
||||||
"services/karakeep" = ./modules/services/karakeep.nix;
|
"services/karakeep" = ./modules/services/karakeep.nix;
|
||||||
"services/nextcloud-server" = {
|
"services/nextcloud-server" = {
|
||||||
module = ./modules/services/nextcloud-server.nix;
|
module = ./modules/services/nextcloud-server.nix;
|
||||||
optionRoot = [ "shb" "nextcloud" ];
|
optionRoot = [
|
||||||
|
"shb"
|
||||||
|
"nextcloud"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"services/open-webui" = ./modules/services/open-webui.nix;
|
"services/open-webui" = ./modules/services/open-webui.nix;
|
||||||
"services/pinchflat" = ./modules/services/pinchflat.nix;
|
"services/pinchflat" = ./modules/services/pinchflat.nix;
|
||||||
"services/vaultwarden" = ./modules/services/vaultwarden.nix;
|
"services/vaultwarden" = ./modules/services/vaultwarden.nix;
|
||||||
"contracts/backup" = {
|
"contracts/backup" = {
|
||||||
module = ./modules/contracts/backup/dummyModule.nix;
|
module = ./modules/contracts/backup/dummyModule.nix;
|
||||||
optionRoot = [ "shb" "contracts" "backup" ];
|
optionRoot = [
|
||||||
|
"shb"
|
||||||
|
"contracts"
|
||||||
|
"backup"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"contracts/databasebackup" = {
|
"contracts/databasebackup" = {
|
||||||
module = ./modules/contracts/databasebackup/dummyModule.nix;
|
module = ./modules/contracts/databasebackup/dummyModule.nix;
|
||||||
optionRoot = [ "shb" "contracts" "databasebackup" ];
|
optionRoot = [
|
||||||
|
"shb"
|
||||||
|
"contracts"
|
||||||
|
"databasebackup"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"contracts/secret" = {
|
"contracts/secret" = {
|
||||||
module = ./modules/contracts/secret/dummyModule.nix;
|
module = ./modules/contracts/secret/dummyModule.nix;
|
||||||
optionRoot = [ "shb" "contracts" "secret" ];
|
optionRoot = [
|
||||||
|
"shb"
|
||||||
|
"contracts"
|
||||||
|
"secret"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
"contracts/ssl" = {
|
"contracts/ssl" = {
|
||||||
module = ./modules/contracts/ssl/dummyModule.nix;
|
module = ./modules/contracts/ssl/dummyModule.nix;
|
||||||
optionRoot = [ "shb" "contracts" "ssl" ];
|
optionRoot = [
|
||||||
|
"shb"
|
||||||
|
"contracts"
|
||||||
|
"ssl"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Documentation redirect generation tool - scans HTML files for anchor mappings
|
# Documentation redirect generation tool - scans HTML files for anchor mappings
|
||||||
packages.generateRedirects =
|
packages.generateRedirects =
|
||||||
let
|
let
|
||||||
# Python patch to inject redirect collector
|
# Python patch to inject redirect collector
|
||||||
pythonPatch = pkgs.writeText "nixos-render-docs-patch.py" ''
|
pythonPatch = pkgs.writeText "nixos-render-docs-patch.py" ''
|
||||||
# Load redirect collector patch
|
# Load redirect collector patch
|
||||||
try:
|
try:
|
||||||
import sys, os
|
import sys, os
|
||||||
sys.path.insert(0, os.path.dirname(__file__) + '/..')
|
sys.path.insert(0, os.path.dirname(__file__) + '/..')
|
||||||
import missing_refs_collector
|
import missing_refs_collector
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Failed to load redirect collector: {e}", file=sys.stderr)
|
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[@]}"
|
|
||||||
'';
|
'';
|
||||||
};
|
|
||||||
in
|
# Patched nixos-render-docs that collects redirects during HTML generation
|
||||||
(self.packages.${system}.manualHtml.override {
|
nixos-render-docs-patched = pkgs.writeShellApplication {
|
||||||
nixos-render-docs = nixos-render-docs-patched;
|
name = "nixos-render-docs";
|
||||||
}).overrideAttrs (old: {
|
runtimeInputs = [ pkgs.nixos-render-docs ];
|
||||||
installPhase = ''
|
text = ''
|
||||||
${old.installPhase}
|
TEMP_DIR=$(mktemp -d); trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||||
ln -sf share/doc/selfhostblocks/redirects.json $out/redirects.json
|
|
||||||
'';
|
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 =
|
lib =
|
||||||
(pkgs.callPackage ./lib {})
|
(pkgs.callPackage ./lib { })
|
||||||
// (pkgs.callPackage ./test/common.nix {})
|
// (pkgs.callPackage ./test/common.nix { })
|
||||||
// {
|
// {
|
||||||
contracts = pkgs.callPackage ./modules/contracts {};
|
contracts = pkgs.callPackage ./modules/contracts { };
|
||||||
patches = shbPatches;
|
patches = shbPatches;
|
||||||
inherit patchNixpkgs patchedNixpkgs pkgs;
|
inherit patchNixpkgs patchedNixpkgs pkgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
checks =
|
checks =
|
||||||
let
|
let
|
||||||
inherit (pkgs.lib) foldl foldlAttrs removeAttrs mergeAttrs optionalAttrs;
|
inherit (pkgs.lib)
|
||||||
|
foldl
|
||||||
|
foldlAttrs
|
||||||
|
removeAttrs
|
||||||
|
mergeAttrs
|
||||||
|
optionalAttrs
|
||||||
|
;
|
||||||
|
|
||||||
importFiles = files:
|
importFiles = files: map (m: pkgs.callPackage m { }) files;
|
||||||
map (m: pkgs.callPackage m {}) files;
|
|
||||||
|
|
||||||
mergeTests = foldl mergeAttrs {};
|
mergeTests = foldl mergeAttrs { };
|
||||||
|
|
||||||
flattenAttrs = root: attrset: foldlAttrs (acc: name: value: acc // {
|
flattenAttrs =
|
||||||
"${root}_${name}" = value;
|
root: attrset:
|
||||||
}) {} attrset;
|
foldlAttrs (
|
||||||
|
acc: name: value:
|
||||||
|
acc
|
||||||
|
// {
|
||||||
|
"${root}_${name}" = value;
|
||||||
|
}
|
||||||
|
) { } attrset;
|
||||||
|
|
||||||
vm_test = name: path: flattenAttrs "vm_${name}" (
|
vm_test =
|
||||||
removeAttrs (pkgs.callPackage path {}) [ "override" "overrideDerivation" ]
|
name: path:
|
||||||
);
|
flattenAttrs "vm_${name}" (
|
||||||
in (optionalAttrs (system == "x86_64-linux") ({
|
removeAttrs (pkgs.callPackage path { }) [
|
||||||
modules = pkgs.lib.shb.check {
|
"override"
|
||||||
inherit pkgs;
|
"overrideDerivation"
|
||||||
tests =
|
]
|
||||||
mergeTests (importFiles [
|
);
|
||||||
|
in
|
||||||
|
(optionalAttrs (system == "x86_64-linux") (
|
||||||
|
{
|
||||||
|
modules = pkgs.lib.shb.check {
|
||||||
|
inherit pkgs;
|
||||||
|
tests = mergeTests (importFiles [
|
||||||
./test/modules/davfs.nix
|
./test/modules/davfs.nix
|
||||||
# TODO: Make this not use IFD
|
# TODO: Make this not use IFD
|
||||||
./test/modules/lib.nix
|
./test/modules/lib.nix
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
# TODO: Make this not use IFD
|
# TODO: Make this not use IFD
|
||||||
lib = nix-flake-tests.lib.check {
|
lib = nix-flake-tests.lib.check {
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
tests = pkgs.callPackage ./test/modules/lib.nix {};
|
tests = pkgs.callPackage ./test/modules/lib.nix { };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// (vm_test "arr" ./test/services/arr.nix)
|
// (vm_test "arr" ./test/services/arr.nix)
|
||||||
// (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix)
|
// (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix)
|
||||||
// (vm_test "deluge" ./test/services/deluge.nix)
|
// (vm_test "deluge" ./test/services/deluge.nix)
|
||||||
// (vm_test "forgejo" ./test/services/forgejo.nix)
|
// (vm_test "forgejo" ./test/services/forgejo.nix)
|
||||||
// (vm_test "grocy" ./test/services/grocy.nix)
|
// (vm_test "grocy" ./test/services/grocy.nix)
|
||||||
// (vm_test "hledger" ./test/services/hledger.nix)
|
// (vm_test "hledger" ./test/services/hledger.nix)
|
||||||
// (vm_test "immich" ./test/services/immich.nix)
|
// (vm_test "immich" ./test/services/immich.nix)
|
||||||
// (vm_test "homeassistant" ./test/services/home-assistant.nix)
|
// (vm_test "homeassistant" ./test/services/home-assistant.nix)
|
||||||
// (vm_test "jellyfin" ./test/services/jellyfin.nix)
|
// (vm_test "jellyfin" ./test/services/jellyfin.nix)
|
||||||
// (vm_test "karakeep" ./test/services/karakeep.nix)
|
// (vm_test "karakeep" ./test/services/karakeep.nix)
|
||||||
// (vm_test "monitoring" ./test/services/monitoring.nix)
|
// (vm_test "monitoring" ./test/services/monitoring.nix)
|
||||||
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
|
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
|
||||||
// (vm_test "open-webui" ./test/services/open-webui.nix)
|
// (vm_test "open-webui" ./test/services/open-webui.nix)
|
||||||
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
|
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
|
||||||
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
|
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
|
||||||
|
|
||||||
// (vm_test "authelia" ./test/blocks/authelia.nix)
|
// (vm_test "authelia" ./test/blocks/authelia.nix)
|
||||||
// (vm_test "lldap" ./test/blocks/lldap.nix)
|
// (vm_test "lldap" ./test/blocks/lldap.nix)
|
||||||
// (vm_test "lib" ./test/blocks/lib.nix)
|
// (vm_test "lib" ./test/blocks/lib.nix)
|
||||||
// (vm_test "mitmdump" ./test/blocks/mitmdump.nix)
|
// (vm_test "mitmdump" ./test/blocks/mitmdump.nix)
|
||||||
// (vm_test "postgresql" ./test/blocks/postgresql.nix)
|
// (vm_test "postgresql" ./test/blocks/postgresql.nix)
|
||||||
// (vm_test "restic" ./test/blocks/restic.nix)
|
// (vm_test "restic" ./test/blocks/restic.nix)
|
||||||
// (vm_test "ssl" ./test/blocks/ssl.nix)
|
// (vm_test "ssl" ./test/blocks/ssl.nix)
|
||||||
|
|
||||||
// (vm_test "contracts-backup" ./test/contracts/backup.nix)
|
// (vm_test "contracts-backup" ./test/contracts/backup.nix)
|
||||||
// (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix)
|
// (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix)
|
||||||
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
|
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
|
||||||
));
|
));
|
||||||
|
|
||||||
# To see the traces, run:
|
# To see the traces, run:
|
||||||
# nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
|
# nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
|
||||||
packages.playwright =
|
packages.playwright = pkgs.callPackage (
|
||||||
pkgs.callPackage ({ stdenvNoCC, makeWrapper, playwright }: stdenvNoCC.mkDerivation {
|
{
|
||||||
|
stdenvNoCC,
|
||||||
|
makeWrapper,
|
||||||
|
playwright,
|
||||||
|
}:
|
||||||
|
stdenvNoCC.mkDerivation {
|
||||||
name = "playwright";
|
name = "playwright";
|
||||||
|
|
||||||
src = playwright;
|
src = playwright;
|
||||||
|
|
@ -261,97 +329,104 @@
|
||||||
--set PLAYWRIGHT_BROWSERS_PATH ${pkgs.playwright-driver.browsers} \
|
--set PLAYWRIGHT_BROWSERS_PATH ${pkgs.playwright-driver.browsers} \
|
||||||
--set PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS true
|
--set PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS true
|
||||||
'';
|
'';
|
||||||
}) {};
|
}
|
||||||
|
) { };
|
||||||
|
|
||||||
# Run "nix run .#update-redirects" to regenerate docs/redirects.json
|
# Run "nix run .#update-redirects" to regenerate docs/redirects.json
|
||||||
apps.update-redirects = {
|
apps.update-redirects = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${pkgs.writeShellApplication {
|
program = "${
|
||||||
name = "update-redirects";
|
pkgs.writeShellApplication {
|
||||||
runtimeInputs = [ pkgs.nix pkgs.jq ];
|
name = "update-redirects";
|
||||||
text = ''
|
runtimeInputs = [
|
||||||
echo "=== SelfHostBlocks Redirects Updater ==="
|
pkgs.nix
|
||||||
echo "Generating fresh ./docs/redirects.json..."
|
pkgs.jq
|
||||||
|
];
|
||||||
nix build .#generateRedirects || { echo "Error: Failed to generate redirects" >&2; exit 1; }
|
text = ''
|
||||||
[[ -f result/redirects.json ]] || { echo "Error: Generated redirects file not found" >&2; exit 1; }
|
echo "=== SelfHostBlocks Redirects Updater ==="
|
||||||
|
echo "Generating fresh ./docs/redirects.json..."
|
||||||
echo "Generated $(jq 'keys | length' result/redirects.json) redirects"
|
|
||||||
|
nix build .#generateRedirects || { echo "Error: Failed to generate redirects" >&2; exit 1; }
|
||||||
[[ -f docs/redirects.json ]] && cp docs/redirects.json docs/redirects.json.backup && echo "Created backup"
|
[[ -f result/redirects.json ]] || { echo "Error: Generated redirects file not found" >&2; exit 1; }
|
||||||
cp result/redirects.json docs/redirects.json
|
|
||||||
echo " Updated docs/redirects.json"
|
echo "Generated $(jq 'keys | length' result/redirects.json) redirects"
|
||||||
echo "To verify: nix build .#manualHtml"
|
|
||||||
'';
|
[[ -f docs/redirects.json ]] && cp docs/redirects.json docs/redirects.json.backup && echo "Created backup"
|
||||||
}}/bin/update-redirects";
|
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 = {
|
nixosModules.default = {
|
||||||
imports = [
|
imports = [
|
||||||
# blocks
|
# blocks
|
||||||
self.nixosModules.authelia
|
self.nixosModules.authelia
|
||||||
self.nixosModules.davfs
|
self.nixosModules.davfs
|
||||||
self.nixosModules.hardcodedsecret
|
self.nixosModules.hardcodedsecret
|
||||||
self.nixosModules.lldap
|
self.nixosModules.lldap
|
||||||
self.nixosModules.mitmdump
|
self.nixosModules.mitmdump
|
||||||
self.nixosModules.monitoring
|
self.nixosModules.monitoring
|
||||||
self.nixosModules.nginx
|
self.nixosModules.nginx
|
||||||
self.nixosModules.postgresql
|
self.nixosModules.postgresql
|
||||||
self.nixosModules.restic
|
self.nixosModules.restic
|
||||||
self.nixosModules.ssl
|
self.nixosModules.ssl
|
||||||
self.nixosModules.tinyproxy
|
self.nixosModules.tinyproxy
|
||||||
self.nixosModules.vpn
|
self.nixosModules.vpn
|
||||||
self.nixosModules.zfs
|
self.nixosModules.zfs
|
||||||
|
|
||||||
# services
|
# services
|
||||||
self.nixosModules.arr
|
self.nixosModules.arr
|
||||||
self.nixosModules.audiobookshelf
|
self.nixosModules.audiobookshelf
|
||||||
self.nixosModules.deluge
|
self.nixosModules.deluge
|
||||||
self.nixosModules.forgejo
|
self.nixosModules.forgejo
|
||||||
self.nixosModules.grocy
|
self.nixosModules.grocy
|
||||||
self.nixosModules.hledger
|
self.nixosModules.hledger
|
||||||
self.nixosModules.immich
|
self.nixosModules.immich
|
||||||
self.nixosModules.home-assistant
|
self.nixosModules.home-assistant
|
||||||
self.nixosModules.jellyfin
|
self.nixosModules.jellyfin
|
||||||
self.nixosModules.karakeep
|
self.nixosModules.karakeep
|
||||||
self.nixosModules.nextcloud-server
|
self.nixosModules.nextcloud-server
|
||||||
self.nixosModules.open-webui
|
self.nixosModules.open-webui
|
||||||
self.nixosModules.pinchflat
|
self.nixosModules.pinchflat
|
||||||
self.nixosModules.vaultwarden
|
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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
396
lib/default.nix
396
lib/default.nix
|
|
@ -9,7 +9,14 @@ rec {
|
||||||
# - resultPath is the location the config file should have on the filesystem.
|
# - resultPath is the location the config file should have on the filesystem.
|
||||||
# - generator is a function taking two arguments name and value and returning path in the nix
|
# - generator is a function taking two arguments name and value and returning path in the nix
|
||||||
# nix store where the
|
# nix store where the
|
||||||
replaceSecrets = { userConfig, resultPath, generator, user ? null, permissions ? "u=r,g=r,o=" }:
|
replaceSecrets =
|
||||||
|
{
|
||||||
|
userConfig,
|
||||||
|
resultPath,
|
||||||
|
generator,
|
||||||
|
user ? null,
|
||||||
|
permissions ? "u=r,g=r,o=",
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
configWithTemplates = withReplacements userConfig;
|
configWithTemplates = withReplacements userConfig;
|
||||||
|
|
||||||
|
|
@ -17,28 +24,47 @@ rec {
|
||||||
|
|
||||||
replacements = getReplacements userConfig;
|
replacements = getReplacements userConfig;
|
||||||
in
|
in
|
||||||
replaceSecretsScript {
|
replaceSecretsScript {
|
||||||
file = nonSecretConfigFile;
|
file = nonSecretConfigFile;
|
||||||
inherit resultPath replacements;
|
inherit resultPath replacements;
|
||||||
inherit user permissions;
|
inherit user permissions;
|
||||||
};
|
};
|
||||||
|
|
||||||
replaceSecretsFormatAdapter = format: format.generate;
|
replaceSecretsFormatAdapter = format: format.generate;
|
||||||
replaceSecretsGeneratorAdapter = generator: name: value: pkgs.writeText "generator " (generator value);
|
replaceSecretsGeneratorAdapter =
|
||||||
toEnvVar = replaceSecretsGeneratorAdapter (v: (lib.generators.toINIWithGlobalSection {} { globalSection = v; }));
|
generator: name: value:
|
||||||
|
pkgs.writeText "generator " (generator value);
|
||||||
|
toEnvVar = replaceSecretsGeneratorAdapter (
|
||||||
|
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
|
||||||
|
);
|
||||||
|
|
||||||
template = file: newPath: replacements: replaceSecretsScript {
|
template =
|
||||||
inherit file replacements;
|
file: newPath: replacements:
|
||||||
resultPath = newPath;
|
replaceSecretsScript {
|
||||||
};
|
inherit file replacements;
|
||||||
|
resultPath = newPath;
|
||||||
|
};
|
||||||
|
|
||||||
genReplacement = secret:
|
genReplacement =
|
||||||
|
secret:
|
||||||
let
|
let
|
||||||
t = { transform ? null, ... }: if isNull transform then x: x else transform;
|
t =
|
||||||
|
{
|
||||||
|
transform ? null,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
if isNull transform then x: x else transform;
|
||||||
in
|
in
|
||||||
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
|
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
|
||||||
|
|
||||||
replaceSecretsScript = { file, resultPath, replacements, user ? null, permissions ? "u=r,g=r,o=" }:
|
replaceSecretsScript =
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
resultPath,
|
||||||
|
replacements,
|
||||||
|
user ? null,
|
||||||
|
permissions ? "u=r,g=r,o=",
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
templatePath = resultPath + ".template";
|
templatePath = resultPath + ".template";
|
||||||
|
|
||||||
|
|
@ -47,15 +73,17 @@ rec {
|
||||||
# step. Otherwise, the $(cat ...) commands inside the sed
|
# step. Otherwise, the $(cat ...) commands inside the sed
|
||||||
# replacements could fail but not fail individually but
|
# replacements could fail but not fail individually but
|
||||||
# not fail the whole script.
|
# not fail the whole script.
|
||||||
checkPermissions = concatMapStringsSep "\n" (pattern: "cat ${pattern.source} > /dev/null") replacements;
|
checkPermissions = concatMapStringsSep "\n" (
|
||||||
|
pattern: "cat ${pattern.source} > /dev/null"
|
||||||
|
) replacements;
|
||||||
|
|
||||||
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (map genReplacement replacements);
|
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (
|
||||||
|
map genReplacement replacements
|
||||||
|
);
|
||||||
|
|
||||||
sedCmd = if replacements == []
|
sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}";
|
||||||
then "cat"
|
|
||||||
else "${pkgs.gnused}/bin/sed ${sedPatterns}";
|
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
${checkPermissions}
|
${checkPermissions}
|
||||||
|
|
@ -64,12 +92,14 @@ rec {
|
||||||
ln -fs ${file} ${templatePath}
|
ln -fs ${file} ${templatePath}
|
||||||
rm -f ${resultPath}
|
rm -f ${resultPath}
|
||||||
touch ${resultPath}
|
touch ${resultPath}
|
||||||
'' + (lib.optionalString (user != null) ''
|
''
|
||||||
|
+ (lib.optionalString (user != null) ''
|
||||||
chown ${user} ${resultPath}
|
chown ${user} ${resultPath}
|
||||||
'') + ''
|
'')
|
||||||
|
+ ''
|
||||||
${sedCmd} ${templatePath} > ${resultPath}
|
${sedCmd} ${templatePath} > ${resultPath}
|
||||||
chmod ${permissions} ${resultPath}
|
chmod ${permissions} ${resultPath}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
secretFileType = lib.types.submodule {
|
secretFileType = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
|
|
@ -83,35 +113,34 @@ rec {
|
||||||
description = "An optional function to transform the secret.";
|
description = "An optional function to transform the secret.";
|
||||||
default = null;
|
default = null;
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
v: "prefix-$${v}-suffix"
|
v: "prefix-$${v}-suffix"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
secretName = names:
|
secretName =
|
||||||
"%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
|
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
|
||||||
|
|
||||||
withReplacements = attrs:
|
withReplacements =
|
||||||
|
attrs:
|
||||||
let
|
let
|
||||||
valueOrReplacement = name: value:
|
valueOrReplacement =
|
||||||
if !(builtins.isAttrs value && value ? "source")
|
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
|
||||||
then value
|
|
||||||
else secretName name;
|
|
||||||
in
|
in
|
||||||
mapAttrsRecursiveCond (v: ! v ? "source") valueOrReplacement attrs;
|
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
|
||||||
|
|
||||||
getReplacements = attrs:
|
getReplacements =
|
||||||
|
attrs:
|
||||||
let
|
let
|
||||||
addNameField = name: value:
|
addNameField =
|
||||||
if !(builtins.isAttrs value && value ? "source")
|
name: value:
|
||||||
then value
|
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
|
||||||
else value // { name = name; };
|
|
||||||
|
|
||||||
secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs;
|
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
|
||||||
in
|
in
|
||||||
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.
|
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
|
||||||
mapAttrsRecursiveCond =
|
mapAttrsRecursiveCond =
|
||||||
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
|
# 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.
|
# Attribute set or list to recursively map over.
|
||||||
set:
|
set:
|
||||||
let
|
let
|
||||||
recurse = path: val:
|
recurse =
|
||||||
if builtins.isAttrs val && cond val
|
path: val:
|
||||||
then lib.attrsets.mapAttrs (n: v: recurse (path ++ [n]) v) val
|
if builtins.isAttrs val && cond val then
|
||||||
else if builtins.isList val && cond val
|
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
|
||||||
then lib.lists.imap0 (i: v: recurse (path ++ [(builtins.toString i)]) v) val
|
else if builtins.isList val && cond val then
|
||||||
else f path val;
|
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
|
||||||
in recurse [] set;
|
else
|
||||||
|
f path val;
|
||||||
|
in
|
||||||
|
recurse [ ] set;
|
||||||
|
|
||||||
# Like lib.attrsets.collect but also recurses on lists.
|
# Like lib.attrsets.collect but also recurses on lists.
|
||||||
collect =
|
collect =
|
||||||
# Given an attribute's value, determine if recursion should stop.
|
# Given an attribute's value, determine if recursion should stop.
|
||||||
pred:
|
pred:
|
||||||
# The attribute set to recursively collect.
|
# The attribute set to recursively collect.
|
||||||
attrs:
|
attrs:
|
||||||
if pred attrs then
|
if pred attrs then
|
||||||
[ attrs ]
|
[ attrs ]
|
||||||
else if builtins.isAttrs attrs then
|
else if builtins.isAttrs attrs then
|
||||||
|
|
@ -142,111 +174,158 @@ rec {
|
||||||
else if builtins.isList attrs then
|
else if builtins.isList attrs then
|
||||||
lib.lists.concatMap (collect pred) attrs
|
lib.lists.concatMap (collect pred) attrs
|
||||||
else
|
else
|
||||||
[];
|
[ ];
|
||||||
|
|
||||||
indent = i: str: lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
|
indent =
|
||||||
|
i: str:
|
||||||
|
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
|
||||||
|
|
||||||
# Generator for XML
|
# Generator for XML
|
||||||
formatXML = {
|
formatXML =
|
||||||
enclosingRoot ? null
|
{
|
||||||
}: {
|
enclosingRoot ? null,
|
||||||
type = with lib.types; let
|
}:
|
||||||
valueType = nullOr (oneOf [
|
{
|
||||||
bool
|
type =
|
||||||
int
|
with lib.types;
|
||||||
float
|
let
|
||||||
str
|
valueType =
|
||||||
path
|
nullOr (oneOf [
|
||||||
(attrsOf valueType)
|
bool
|
||||||
(listOf valueType)
|
int
|
||||||
]) // {
|
float
|
||||||
description = "XML value";
|
str
|
||||||
};
|
path
|
||||||
in valueType;
|
(attrsOf valueType)
|
||||||
|
(listOf valueType)
|
||||||
|
])
|
||||||
|
// {
|
||||||
|
description = "XML value";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
valueType;
|
||||||
|
|
||||||
generate = name: value: pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
|
generate =
|
||||||
value = builtins.toJSON (
|
name: value:
|
||||||
if enclosingRoot == null then
|
pkgs.callPackage (
|
||||||
value
|
{ runCommand, python3 }:
|
||||||
else
|
runCommand "config"
|
||||||
{ ${enclosingRoot} = value; });
|
{
|
||||||
passAsFile = [ "value" ];
|
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
|
||||||
} (pkgs.writers.writePython3 "dict2xml" {
|
passAsFile = [ "value" ];
|
||||||
libraries = with python3.pkgs; [ python dict2xml ];
|
}
|
||||||
} ''
|
(
|
||||||
import os
|
pkgs.writers.writePython3 "dict2xml"
|
||||||
import json
|
{
|
||||||
from dict2xml import dict2xml
|
libraries = with python3.pkgs; [
|
||||||
|
python
|
||||||
|
dict2xml
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from dict2xml import dict2xml
|
||||||
|
|
||||||
with open(os.environ["valuePath"]) as f:
|
with open(os.environ["valuePath"]) as f:
|
||||||
content = json.loads(f.read())
|
content = json.loads(f.read())
|
||||||
if content is None:
|
if content is None:
|
||||||
print("Could not parse env var valuePath as json")
|
print("Could not parse env var valuePath as json")
|
||||||
os.exit(2)
|
os.exit(2)
|
||||||
with open(os.environ["out"], "w") as out:
|
with open(os.environ["out"], "w") as out:
|
||||||
out.write(dict2xml(content))
|
out.write(dict2xml(content))
|
||||||
'')) {};
|
''
|
||||||
|
)
|
||||||
|
) { };
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
parseXML = xml:
|
parseXML =
|
||||||
|
xml:
|
||||||
let
|
let
|
||||||
xmlToJsonFile = pkgs.callPackage ({ runCommand, python3 }: runCommand "config" {
|
xmlToJsonFile = pkgs.callPackage (
|
||||||
inherit xml;
|
{ runCommand, python3 }:
|
||||||
passAsFile = [ "xml" ];
|
runCommand "config"
|
||||||
} (pkgs.writers.writePython3 "xml2json" {
|
{
|
||||||
libraries = with python3.pkgs; [ python ];
|
inherit xml;
|
||||||
} ''
|
passAsFile = [ "xml" ];
|
||||||
import os
|
}
|
||||||
import json
|
(
|
||||||
from collections import ChainMap
|
pkgs.writers.writePython3 "xml2json"
|
||||||
from xml.etree import ElementTree
|
{
|
||||||
|
libraries = with python3.pkgs; [ python ];
|
||||||
|
}
|
||||||
def xml_to_dict_recursive(root):
|
''
|
||||||
all_descendants = list(root)
|
import os
|
||||||
if len(all_descendants) == 0:
|
import json
|
||||||
return {root.tag: root.text}
|
from collections import ChainMap
|
||||||
else:
|
from xml.etree import ElementTree
|
||||||
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
|
|
||||||
return {root.tag: dict(merged_dict)}
|
|
||||||
|
|
||||||
|
|
||||||
with open(os.environ["xmlPath"]) as f:
|
def xml_to_dict_recursive(root):
|
||||||
root = ElementTree.XML(f.read())
|
all_descendants = list(root)
|
||||||
xml = xml_to_dict_recursive(root)
|
if len(all_descendants) == 0:
|
||||||
j = json.dumps(xml)
|
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
|
in
|
||||||
builtins.fromJSON (builtins.readFile xmlToJsonFile);
|
builtins.fromJSON (builtins.readFile xmlToJsonFile);
|
||||||
|
|
||||||
renameAttrName = attrset: from: to:
|
renameAttrName =
|
||||||
(lib.attrsets.filterAttrs (name: v: name == from) attrset) // {
|
attrset: from: to:
|
||||||
|
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
|
||||||
|
// {
|
||||||
${to} = attrset.${from};
|
${to} = attrset.${from};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
|
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
|
||||||
# with a nicer diff display function.
|
# with a nicer diff display function.
|
||||||
check = { pkgs, tests }:
|
check =
|
||||||
|
{ pkgs, tests }:
|
||||||
let
|
let
|
||||||
formatValue = val:
|
formatValue =
|
||||||
if (builtins.isList val || builtins.isAttrs val) then builtins.toJSON val
|
val:
|
||||||
else builtins.toString val;
|
if (builtins.isList val || builtins.isAttrs val) then
|
||||||
|
builtins.toJSON val
|
||||||
|
else
|
||||||
|
builtins.toString val;
|
||||||
|
|
||||||
resultToString = { name, expected, result }:
|
resultToString =
|
||||||
builtins.readFile (pkgs.runCommand "nix-flake-tests-error" {
|
{
|
||||||
expected = formatValue expected;
|
name,
|
||||||
result = formatValue result;
|
expected,
|
||||||
passAsFile = [ "expected" "result" ];
|
result,
|
||||||
} ''
|
}:
|
||||||
echo "${name} failed (- expected, + result)" > $out
|
builtins.readFile (
|
||||||
cp ''${expectedPath} ''${expectedPath}.json
|
pkgs.runCommand "nix-flake-tests-error"
|
||||||
cp ''${resultPath} ''${resultPath}.json
|
{
|
||||||
${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
|
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;
|
results = pkgs.lib.runTests tests;
|
||||||
in
|
in
|
||||||
|
|
@ -255,8 +334,14 @@ rec {
|
||||||
else
|
else
|
||||||
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
|
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
|
||||||
|
|
||||||
|
genConfigOutOfBandSystemd =
|
||||||
genConfigOutOfBandSystemd = { config, configLocation, generator, user ? null, permissions ? "u=r,g=r,o=" }:
|
{
|
||||||
|
config,
|
||||||
|
configLocation,
|
||||||
|
generator,
|
||||||
|
user ? null,
|
||||||
|
permissions ? "u=r,g=r,o=",
|
||||||
|
}:
|
||||||
{
|
{
|
||||||
loadCredentials = getLoadCredentials "source" config;
|
loadCredentials = getLoadCredentials "source" config;
|
||||||
preStart = lib.mkBefore (replaceSecrets {
|
preStart = lib.mkBefore (replaceSecrets {
|
||||||
|
|
@ -267,34 +352,35 @@ rec {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
updateToLoadCredentials = sourceField: rootDir: attrs:
|
updateToLoadCredentials =
|
||||||
|
sourceField: rootDir: attrs:
|
||||||
let
|
let
|
||||||
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
||||||
|
|
||||||
valueOrLoadCredential = path: value:
|
valueOrLoadCredential =
|
||||||
if ! (hasPlaceholderField value)
|
path: value:
|
||||||
then value
|
if !(hasPlaceholderField value) then
|
||||||
else value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
|
value
|
||||||
|
else
|
||||||
|
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
|
||||||
in
|
in
|
||||||
mapAttrsRecursiveCond (v: ! (hasPlaceholderField v)) valueOrLoadCredential attrs;
|
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
|
||||||
|
|
||||||
getLoadCredentials = sourceField: attrs:
|
getLoadCredentials =
|
||||||
|
sourceField: attrs:
|
||||||
let
|
let
|
||||||
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
||||||
|
|
||||||
addPathField = path: value:
|
addPathField =
|
||||||
if ! (hasPlaceholderField value)
|
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
|
||||||
then value
|
|
||||||
else value // { inherit path; };
|
|
||||||
|
|
||||||
secretsWithPath = mapAttrsRecursiveCond (v: ! (hasPlaceholderField v)) addPathField attrs;
|
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
|
||||||
|
|
||||||
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
|
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
|
||||||
|
|
||||||
genLoadCredentials = secret:
|
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
|
||||||
"${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
|
|
||||||
in
|
in
|
||||||
map genLoadCredentials allSecrets;
|
map genLoadCredentials allSecrets;
|
||||||
|
|
||||||
anyNotNull = any (x: x != null);
|
anyNotNull = any (x: x != null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
{ config, options, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.authelia;
|
cfg = config.shb.authelia;
|
||||||
opt = options.shb.authelia;
|
opt = options.shb.authelia;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}";
|
fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}";
|
||||||
|
|
@ -148,25 +154,25 @@ in
|
||||||
extraOidcClaimsPolicies = lib.mkOption {
|
extraOidcClaimsPolicies = lib.mkOption {
|
||||||
description = "Extra OIDC claims policies.";
|
description = "Extra OIDC claims policies.";
|
||||||
type = lib.types.attrsOf lib.types.attrs;
|
type = lib.types.attrsOf lib.types.attrs;
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
extraOidcScopes = lib.mkOption {
|
extraOidcScopes = lib.mkOption {
|
||||||
description = "Extra OIDC scopes.";
|
description = "Extra OIDC scopes.";
|
||||||
type = lib.types.attrsOf lib.types.attrs;
|
type = lib.types.attrsOf lib.types.attrs;
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
extraOidcAuthorizationPolicies = lib.mkOption {
|
extraOidcAuthorizationPolicies = lib.mkOption {
|
||||||
description = "Extra OIDC authorization policies.";
|
description = "Extra OIDC authorization policies.";
|
||||||
type = lib.types.attrsOf lib.types.attrs;
|
type = lib.types.attrsOf lib.types.attrs;
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
extraDefinitions = lib.mkOption {
|
extraDefinitions = lib.mkOption {
|
||||||
description = "Extra definitions.";
|
description = "Extra definitions.";
|
||||||
type = lib.types.attrsOf lib.types.attrs;
|
type = lib.types.attrsOf lib.types.attrs;
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
oidcClients = lib.mkOption {
|
oidcClients = lib.mkOption {
|
||||||
|
|
@ -178,79 +184,92 @@ in
|
||||||
client_secret.source = pkgs.writeText "dummy.secret" "dummy_client_secret";
|
client_secret.source = pkgs.writeText "dummy.secret" "dummy_client_secret";
|
||||||
public = false;
|
public = false;
|
||||||
authorization_policy = "one_factor";
|
authorization_policy = "one_factor";
|
||||||
redirect_uris = [];
|
redirect_uris = [ ];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
type = lib.types.listOf (lib.types.submodule {
|
type = lib.types.listOf (
|
||||||
freeformType = lib.types.attrsOf lib.types.anything;
|
lib.types.submodule {
|
||||||
|
freeformType = lib.types.attrsOf lib.types.anything;
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
client_id = lib.mkOption {
|
client_id = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Unique identifier of the OIDC client.";
|
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 {
|
smtp = lib.mkOption {
|
||||||
|
|
@ -263,50 +282,52 @@ in
|
||||||
default = "/tmp/authelia-notifications";
|
default = "/tmp/authelia-notifications";
|
||||||
type = lib.types.oneOf [
|
type = lib.types.oneOf [
|
||||||
lib.types.str
|
lib.types.str
|
||||||
(lib.types.nullOr (lib.types.submodule {
|
(lib.types.nullOr (
|
||||||
options = {
|
lib.types.submodule {
|
||||||
from_address = lib.mkOption {
|
options = {
|
||||||
type = lib.types.str;
|
from_address = lib.mkOption {
|
||||||
description = "SMTP address from which the emails originate.";
|
type = lib.types.str;
|
||||||
example = "authelia@mydomain.com";
|
description = "SMTP address from which the emails originate.";
|
||||||
};
|
example = "authelia@mydomain.com";
|
||||||
from_name = lib.mkOption {
|
};
|
||||||
type = lib.types.str;
|
from_name = lib.mkOption {
|
||||||
description = "SMTP name from which the emails originate.";
|
type = lib.types.str;
|
||||||
default = "Authelia";
|
description = "SMTP name from which the emails originate.";
|
||||||
};
|
default = "Authelia";
|
||||||
host = lib.mkOption {
|
};
|
||||||
type = lib.types.str;
|
host = lib.mkOption {
|
||||||
description = "SMTP host to send the emails to.";
|
type = lib.types.str;
|
||||||
};
|
description = "SMTP host to send the emails to.";
|
||||||
port = lib.mkOption {
|
};
|
||||||
type = lib.types.port;
|
port = lib.mkOption {
|
||||||
description = "SMTP port to send the emails to.";
|
type = lib.types.port;
|
||||||
default = 25;
|
description = "SMTP port to send the emails to.";
|
||||||
};
|
default = 25;
|
||||||
username = lib.mkOption {
|
};
|
||||||
type = lib.types.str;
|
username = lib.mkOption {
|
||||||
description = "Username to connect to the SMTP host.";
|
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.";
|
password = lib.mkOption {
|
||||||
type = lib.types.submodule {
|
description = "File containing the password to connect to the SMTP host.";
|
||||||
options = contracts.secret.mkRequester {
|
type = lib.types.submodule {
|
||||||
mode = "0400";
|
options = contracts.secret.mkRequester {
|
||||||
owner = cfg.autheliaUser;
|
mode = "0400";
|
||||||
restartUnits = [ "authelia-${fqdn}" ];
|
owner = cfg.autheliaUser;
|
||||||
|
restartUnits = [ "authelia-${fqdn}" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
}))
|
))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
rules = lib.mkOption {
|
rules = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.anything;
|
type = lib.types.listOf lib.types.anything;
|
||||||
description = "Rule based clients";
|
description = "Rule based clients";
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
mount = lib.mkOption {
|
mount = lib.mkOption {
|
||||||
|
|
@ -324,8 +345,12 @@ in
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
default = { path = "/var/lib/authelia-authelia.${cfg.domain}"; };
|
default = {
|
||||||
defaultText = { path = "/var/lib/authelia-authelia.example.com"; };
|
path = "/var/lib/authelia-authelia.${cfg.domain}";
|
||||||
|
};
|
||||||
|
defaultText = {
|
||||||
|
path = "/var/lib/authelia-authelia.example.com";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
mountRedis = lib.mkOption {
|
mountRedis = lib.mkOption {
|
||||||
|
|
@ -343,7 +368,9 @@ in
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
default = { path = "/var/lib/redis-authelia"; };
|
default = {
|
||||||
|
path = "/var/lib/redis-authelia";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
debug = lib.mkOption {
|
debug = lib.mkOption {
|
||||||
|
|
@ -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 '.'.
|
# Overriding the user name so we don't allow any weird characters anywhere. For example, postgres users do not accept the '.'.
|
||||||
users = {
|
users = {
|
||||||
groups.${autheliaCfg.user} = {};
|
groups.${autheliaCfg.user} = { };
|
||||||
users.${autheliaCfg.user} = {
|
users.${autheliaCfg.user} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = autheliaCfg.user;
|
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_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
|
||||||
AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
|
AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
|
||||||
|
|
||||||
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.password.result.path);
|
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (
|
||||||
|
toString cfg.smtp.password.result.path
|
||||||
|
);
|
||||||
};
|
};
|
||||||
settings = {
|
settings = {
|
||||||
server.address = "tcp://127.0.0.1:${toString listenPort}";
|
server.address = "tcp://127.0.0.1:${toString listenPort}";
|
||||||
|
|
@ -428,10 +457,12 @@ in
|
||||||
# Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis
|
# Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis
|
||||||
session = {
|
session = {
|
||||||
name = "authelia_session";
|
name = "authelia_session";
|
||||||
cookies = [{
|
cookies = [
|
||||||
domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}";
|
{
|
||||||
authelia_url = "https://${cfg.subdomain}.${cfg.domain}";
|
domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}";
|
||||||
}];
|
authelia_url = "https://${cfg.subdomain}.${cfg.domain}";
|
||||||
|
}
|
||||||
|
];
|
||||||
same_site = "lax";
|
same_site = "lax";
|
||||||
expiration = "1h";
|
expiration = "1h";
|
||||||
inactivity = "5m";
|
inactivity = "5m";
|
||||||
|
|
@ -468,7 +499,11 @@ in
|
||||||
networks = [
|
networks = [
|
||||||
{
|
{
|
||||||
name = "internal";
|
name = "internal";
|
||||||
networks = [ "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/18" ];
|
networks = [
|
||||||
|
"10.0.0.0/8"
|
||||||
|
"172.16.0.0/12"
|
||||||
|
"192.168.0.0/18"
|
||||||
|
];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
rules = [
|
rules = [
|
||||||
|
|
@ -479,7 +514,8 @@ in
|
||||||
"^/api/.*"
|
"^/api/.*"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
] ++ cfg.rules;
|
]
|
||||||
|
++ cfg.rules;
|
||||||
};
|
};
|
||||||
telemetry = {
|
telemetry = {
|
||||||
metrics = {
|
metrics = {
|
||||||
|
|
@ -489,17 +525,25 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
log.level = if cfg.debug then "debug" else "info";
|
log.level = if cfg.debug then "debug" else "info";
|
||||||
} // {
|
}
|
||||||
|
// {
|
||||||
identity_providers.oidc = {
|
identity_providers.oidc = {
|
||||||
claims_policies = {
|
claims_policies = {
|
||||||
# This default claim should go away at some point.
|
# This default claim should go away at some point.
|
||||||
# https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter
|
# https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter
|
||||||
default.id_token = [ "email" "preferred_username" "name" "groups" ];
|
default.id_token = [
|
||||||
} // cfg.extraOidcClaimsPolicies;
|
"email"
|
||||||
|
"preferred_username"
|
||||||
|
"name"
|
||||||
|
"groups"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// cfg.extraOidcClaimsPolicies;
|
||||||
scopes = cfg.extraOidcScopes;
|
scopes = cfg.extraOidcScopes;
|
||||||
authorization_policies = cfg.extraOidcAuthorizationPolicies;
|
authorization_policies = cfg.extraOidcAuthorizationPolicies;
|
||||||
};
|
};
|
||||||
} // lib.optionalAttrs (cfg.extraDefinitions != {}) {
|
}
|
||||||
|
// lib.optionalAttrs (cfg.extraDefinitions != { }) {
|
||||||
definitions = cfg.extraDefinitions;
|
definitions = cfg.extraDefinitions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -508,18 +552,22 @@ in
|
||||||
|
|
||||||
systemd.services."authelia-${fqdn}".preStart =
|
systemd.services."authelia-${fqdn}".preStart =
|
||||||
let
|
let
|
||||||
mkCfg = clients:
|
mkCfg =
|
||||||
|
clients:
|
||||||
lib.shb.replaceSecrets {
|
lib.shb.replaceSecrets {
|
||||||
userConfig = {
|
userConfig = {
|
||||||
identity_providers.oidc.clients = clients;
|
identity_providers.oidc.clients = clients;
|
||||||
};
|
};
|
||||||
resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
|
resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
|
||||||
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML {});
|
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { });
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.mkBefore (mkCfg cfg.oidcClients + ''
|
lib.mkBefore (
|
||||||
${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'
|
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} = {
|
services.nginx.virtualHosts.${fqdn} = {
|
||||||
forceSSL = !(isNull cfg.ssl);
|
forceSSL = !(isNull cfg.ssl);
|
||||||
|
|
@ -554,7 +602,7 @@ in
|
||||||
error_page 403 = /error/403;
|
error_page 403 = /error/403;
|
||||||
error_page 404 = /error/404;
|
error_page 404 = /error/404;
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
locations."/api/verify".extraConfig = ''
|
locations."/api/verify".extraConfig = ''
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
|
@ -567,7 +615,7 @@ in
|
||||||
|
|
||||||
proxy_set_header Host $http_x_forwarded_host;
|
proxy_set_header Host $http_x_forwarded_host;
|
||||||
proxy_pass http://127.0.0.1:9091;
|
proxy_pass http://127.0.0.1:9091;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# I would like this to live outside of the Authelia module.
|
# I would like this to live outside of the Authelia module.
|
||||||
|
|
@ -579,7 +627,8 @@ in
|
||||||
after = [ "authelia-${fqdn}.service" ];
|
after = [ "authelia-${fqdn}.service" ];
|
||||||
enabledAddons = [ config.shb.mitmdump.addons.logger ];
|
enabledAddons = [ config.shb.mitmdump.addons.logger ];
|
||||||
extraArgs = [
|
extraArgs = [
|
||||||
"--set" "verbose_pattern=/api"
|
"--set"
|
||||||
|
"verbose_pattern=/api"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -600,7 +649,7 @@ in
|
||||||
job_name = "authelia";
|
job_name = "authelia";
|
||||||
static_configs = [
|
static_configs = [
|
||||||
{
|
{
|
||||||
targets = ["127.0.0.1:9959"];
|
targets = [ "127.0.0.1:9959" ];
|
||||||
labels = {
|
labels = {
|
||||||
"hostname" = config.networking.hostName;
|
"hostname" = config.networking.hostName;
|
||||||
"domain" = cfg.domain;
|
"domain" = cfg.domain;
|
||||||
|
|
@ -610,17 +659,20 @@ in
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.targets."authelia-${fqdn}" = let
|
systemd.targets."authelia-${fqdn}" =
|
||||||
services = [
|
let
|
||||||
"authelia-${fqdn}.service"
|
services = [
|
||||||
] ++ lib.optionals cfg.debug [
|
"authelia-${fqdn}.service"
|
||||||
config.shb.mitmdump.instances."authelia-${fqdn}".serviceName
|
]
|
||||||
];
|
++ lib.optionals cfg.debug [
|
||||||
in {
|
config.shb.mitmdump.instances."authelia-${fqdn}".serviceName
|
||||||
after = services;
|
];
|
||||||
requires = services;
|
in
|
||||||
|
{
|
||||||
|
after = services;
|
||||||
|
requires = services;
|
||||||
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
{ config, pkgs, lib, utils, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
utils,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.borgbackup;
|
cfg = config.shb.borgbackup;
|
||||||
|
|
@ -17,7 +23,9 @@ let
|
||||||
type = lib.types.path;
|
type = lib.types.path;
|
||||||
};
|
};
|
||||||
|
|
||||||
encryption_passcommand = "cat /run/secrets/borgmatic/passphrases/${if isNull instance.secretName then name else instance.secretName}";
|
encryption_passcommand = "cat /run/secrets/borgmatic/passphrases/${
|
||||||
|
if isNull instance.secretName then name else instance.secretName
|
||||||
|
}";
|
||||||
borg_keys_directory = "/run/secrets/borgmatic/keys";
|
borg_keys_directory = "/run/secrets/borgmatic/keys";
|
||||||
|
|
||||||
sourceDirectories = lib.mkOption {
|
sourceDirectories = lib.mkOption {
|
||||||
|
|
@ -28,7 +36,7 @@ let
|
||||||
excludePatterns = lib.mkOption {
|
excludePatterns = lib.mkOption {
|
||||||
description = "Exclude patterns.";
|
description = "Exclude patterns.";
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
secretName = lib.mkOption {
|
secretName = lib.mkOption {
|
||||||
|
|
@ -39,33 +47,40 @@ let
|
||||||
|
|
||||||
repositories = lib.mkOption {
|
repositories = lib.mkOption {
|
||||||
description = "Repositories to back this instance to.";
|
description = "Repositories to back this instance to.";
|
||||||
type = lib.types.nonEmptyListOf (lib.types.submodule {
|
type = lib.types.nonEmptyListOf (
|
||||||
options = {
|
lib.types.submodule {
|
||||||
path = lib.mkOption {
|
options = {
|
||||||
type = lib.types.str;
|
path = lib.mkOption {
|
||||||
description = "Repository location";
|
type = lib.types.str;
|
||||||
};
|
description = "Repository location";
|
||||||
|
};
|
||||||
|
|
||||||
timerConfig = lib.mkOption {
|
timerConfig = lib.mkOption {
|
||||||
type = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption;
|
type = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption;
|
||||||
default = {
|
default = {
|
||||||
OnCalendar = "daily";
|
OnCalendar = "daily";
|
||||||
Persistent = true;
|
Persistent = true;
|
||||||
};
|
};
|
||||||
description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.'';
|
description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.'';
|
||||||
example = {
|
example = {
|
||||||
OnCalendar = "00:05";
|
OnCalendar = "00:05";
|
||||||
RandomizedDelaySec = "5h";
|
RandomizedDelaySec = "5h";
|
||||||
Persistent = true;
|
Persistent = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
retention = lib.mkOption {
|
retention = lib.mkOption {
|
||||||
description = "Retention options.";
|
description = "Retention options.";
|
||||||
type = lib.types.attrsOf (lib.types.oneOf [ lib.types.int lib.types.nonEmptyStr ]);
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.oneOf [
|
||||||
|
lib.types.int
|
||||||
|
lib.types.nonEmptyStr
|
||||||
|
]
|
||||||
|
);
|
||||||
default = {
|
default = {
|
||||||
keep_within = "1d";
|
keep_within = "1d";
|
||||||
keep_hourly = 24;
|
keep_hourly = 24;
|
||||||
|
|
@ -78,7 +93,7 @@ let
|
||||||
consistency = lib.mkOption {
|
consistency = lib.mkOption {
|
||||||
description = "Consistency frequency options.";
|
description = "Consistency frequency options.";
|
||||||
type = lib.types.attrsOf lib.types.nonEmptyStr;
|
type = lib.types.attrsOf lib.types.nonEmptyStr;
|
||||||
default = {};
|
default = { };
|
||||||
example = {
|
example = {
|
||||||
repository = "2 weeks";
|
repository = "2 weeks";
|
||||||
archives = "1 month";
|
archives = "1 month";
|
||||||
|
|
@ -87,19 +102,19 @@ let
|
||||||
|
|
||||||
hooks = lib.mkOption {
|
hooks = lib.mkOption {
|
||||||
description = "Hooks to run before or after the backup.";
|
description = "Hooks to run before or after the backup.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
beforeBackup = lib.mkOption {
|
beforeBackup = lib.mkOption {
|
||||||
description = "Hooks to run before backup";
|
description = "Hooks to run before backup";
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
afterBackup = lib.mkOption {
|
afterBackup = lib.mkOption {
|
||||||
description = "Hooks to run after backup";
|
description = "Hooks to run after backup";
|
||||||
type = lib.types.listOf lib.types.str;
|
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
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -132,10 +148,12 @@ in
|
||||||
|
|
||||||
instances = lib.mkOption {
|
instances = lib.mkOption {
|
||||||
description = "Each instance is a backup setting";
|
description = "Each instance is a backup setting";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.attrsOf (lib.types.submodule {
|
type = lib.types.attrsOf (
|
||||||
options = instanceOptions;
|
lib.types.submodule {
|
||||||
});
|
options = instanceOptions;
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
borgServer = lib.mkOption {
|
borgServer = lib.mkOption {
|
||||||
|
|
@ -148,7 +166,7 @@ in
|
||||||
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
|
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
|
||||||
performance = lib.mkOption {
|
performance = lib.mkOption {
|
||||||
description = "Reduce performance impact of backup jobs.";
|
description = "Reduce performance impact of backup jobs.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
niceness = lib.mkOption {
|
niceness = lib.mkOption {
|
||||||
|
|
@ -157,7 +175,11 @@ in
|
||||||
default = 15;
|
default = 15;
|
||||||
};
|
};
|
||||||
ioSchedulingClass = lib.mkOption {
|
ioSchedulingClass = lib.mkOption {
|
||||||
type = lib.types.enum [ "idle" "best-effort" "realtime" ];
|
type = lib.types.enum [
|
||||||
|
"idle"
|
||||||
|
"best-effort"
|
||||||
|
"realtime"
|
||||||
|
];
|
||||||
description = "ionice scheduling class, defaults to best-effort IO.";
|
description = "ionice scheduling class, defaults to best-effort IO.";
|
||||||
default = "best-effort";
|
default = "best-effort";
|
||||||
};
|
};
|
||||||
|
|
@ -171,10 +193,11 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf (cfg.instances != {}) (
|
config = lib.mkIf (cfg.instances != { }) (
|
||||||
let
|
let
|
||||||
enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances;
|
enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances;
|
||||||
in lib.mkMerge [
|
in
|
||||||
|
lib.mkMerge [
|
||||||
# Secrets configuration
|
# Secrets configuration
|
||||||
{
|
{
|
||||||
users.users = {
|
users.users = {
|
||||||
|
|
@ -195,74 +218,101 @@ in
|
||||||
|
|
||||||
sops.secrets =
|
sops.secrets =
|
||||||
let
|
let
|
||||||
mkSopsSecret = name: instance: (
|
mkSopsSecret =
|
||||||
[
|
name: instance:
|
||||||
{
|
(
|
||||||
"${instance.backend}/passphrases/${if isNull instance.secretName then name else instance.secretName}" = {
|
[
|
||||||
sopsFile = instance.keySopsFile;
|
{
|
||||||
mode = "0440";
|
"${instance.backend}/passphrases/${
|
||||||
owner = cfg.user;
|
if isNull instance.secretName then name else instance.secretName
|
||||||
group = cfg.group;
|
}" =
|
||||||
};
|
{
|
||||||
}
|
sopsFile = instance.keySopsFile;
|
||||||
] ++ lib.optional ((lib.filter ({path, ...}: lib.strings.hasPrefix "s3" path) instance.repositories) != []) {
|
mode = "0440";
|
||||||
"${instance.backend}/environmentfiles/${if isNull instance.secretName then name else instance.secretName}" = {
|
owner = cfg.user;
|
||||||
sopsFile = instance.keySopsFile;
|
group = cfg.group;
|
||||||
mode = "0440";
|
};
|
||||||
owner = cfg.user;
|
}
|
||||||
group = cfg.group;
|
]
|
||||||
};
|
++
|
||||||
} ++ lib.optionals (instance.backend == "borgmatic") (lib.flatten (map ({path, ...}: {
|
lib.optional
|
||||||
"${instance.backend}/keys/${repoSlugName path}" = {
|
((lib.filter ({ path, ... }: lib.strings.hasPrefix "s3" path) instance.repositories) != [ ])
|
||||||
key = "${instance.backend}/keys/${if isNull instance.secretName then name else instance.secretName}";
|
{
|
||||||
sopsFile = instance.keySopsFile;
|
"${instance.backend}/environmentfiles/${
|
||||||
mode = "0440";
|
if isNull instance.secretName then name else instance.secretName
|
||||||
owner = cfg.user;
|
}" =
|
||||||
group = cfg.group;
|
{
|
||||||
};
|
sopsFile = instance.keySopsFile;
|
||||||
}) instance.repositories))
|
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
|
in
|
||||||
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances));
|
lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances));
|
||||||
}
|
}
|
||||||
# Borgmatic configuration
|
# Borgmatic configuration
|
||||||
{
|
{
|
||||||
systemd.timers.borgmatic = lib.mkIf (enabledInstances != {}) {
|
systemd.timers.borgmatic = lib.mkIf (enabledInstances != { }) {
|
||||||
timerConfig = {
|
timerConfig = {
|
||||||
OnCalendar = "hourly";
|
OnCalendar = "hourly";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.borgmatic = lib.mkIf (enabledInstances != {}) {
|
systemd.services.borgmatic = lib.mkIf (enabledInstances != { }) {
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
User = cfg.user;
|
User = cfg.user;
|
||||||
Group = cfg.group;
|
Group = cfg.group;
|
||||||
ExecStartPre = [ "" ]; # Do not sleep before starting.
|
ExecStartPre = [ "" ]; # Do not sleep before starting.
|
||||||
ExecStart = [ "" "${pkgs.borgmatic}/bin/borgmatic --verbosity -1 --syslog-verbosity 1" ];
|
ExecStart = [
|
||||||
|
""
|
||||||
|
"${pkgs.borgmatic}/bin/borgmatic --verbosity -1 --syslog-verbosity 1"
|
||||||
|
];
|
||||||
# For borgmatic, since we have only one service, we need to merge all environmentFile
|
# For borgmatic, since we have only one service, we need to merge all environmentFile
|
||||||
# from all instances.
|
# from all instances.
|
||||||
EnvironmentFile = lib.mapAttrsToList (name: value: value.environmentFile) enabledInstances;
|
EnvironmentFile = lib.mapAttrsToList (name: value: value.environmentFile) enabledInstances;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.packages = lib.mkIf (enabledInstances != {}) [ pkgs.borgmatic ];
|
systemd.packages = lib.mkIf (enabledInstances != { }) [ pkgs.borgmatic ];
|
||||||
environment.systemPackages = (
|
environment.systemPackages = (
|
||||||
lib.optionals cfg.borgServer [ pkgs.borgbackup ]
|
lib.optionals cfg.borgServer [ pkgs.borgbackup ]
|
||||||
++ lib.optionals (enabledInstances != {}) [ pkgs.borgbackup pkgs.borgmatic ]
|
++ lib.optionals (enabledInstances != { }) [
|
||||||
|
pkgs.borgbackup
|
||||||
|
pkgs.borgmatic
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
environment.etc =
|
environment.etc =
|
||||||
let
|
let
|
||||||
mkSettings = name: instance: {
|
mkSettings = name: instance: {
|
||||||
"borgmatic.d/${name}.yaml".text = lib.generators.toYAML {} {
|
"borgmatic.d/${name}.yaml".text = lib.generators.toYAML { } {
|
||||||
location =
|
location = {
|
||||||
{
|
source_directories = instance.sourceDirectories;
|
||||||
source_directories = instance.sourceDirectories;
|
repositories = map ({ path, ... }: path) instance.repositories;
|
||||||
repositories = map ({path, ...}: path) instance.repositories;
|
}
|
||||||
}
|
// (lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) {
|
||||||
// (lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) {
|
excludePatterns = instance.excludePatterns;
|
||||||
excludePatterns = instance.excludePatterns;
|
});
|
||||||
});
|
|
||||||
|
|
||||||
storage = {
|
storage = {
|
||||||
encryption_passcommand = "cat ${instance.encryptionKeyFile}";
|
encryption_passcommand = "cat ${instance.encryptionKeyFile}";
|
||||||
|
|
@ -276,7 +326,7 @@ in
|
||||||
inherit name frequency;
|
inherit name frequency;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.attrsets.mapAttrsToList mkCheck instance.consistency;
|
lib.attrsets.mapAttrsToList mkCheck instance.consistency;
|
||||||
|
|
||||||
# hooks = lib.mkMerge [
|
# hooks = lib.mkMerge [
|
||||||
# lib.optionalAttrs (builtins.length instance.hooks.beforeBackup > 0) {
|
# lib.optionalAttrs (builtins.length instance.hooks.beforeBackup > 0) {
|
||||||
|
|
@ -289,7 +339,8 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings enabledInstances);
|
lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings enabledInstances);
|
||||||
}
|
}
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,66 +7,68 @@ in
|
||||||
options.shb.davfs = {
|
options.shb.davfs = {
|
||||||
mounts = lib.mkOption {
|
mounts = lib.mkOption {
|
||||||
description = "List of mounts.";
|
description = "List of mounts.";
|
||||||
default = [];
|
default = [ ];
|
||||||
type = lib.types.listOf (lib.types.submodule {
|
type = lib.types.listOf (
|
||||||
options = {
|
lib.types.submodule {
|
||||||
remoteUrl = lib.mkOption {
|
options = {
|
||||||
type = lib.types.str;
|
remoteUrl = lib.mkOption {
|
||||||
description = "Webdav endpoint to connect to.";
|
type = lib.types.str;
|
||||||
example = "https://my.domain.com/dav";
|
description = "Webdav endpoint to connect to.";
|
||||||
};
|
example = "https://my.domain.com/dav";
|
||||||
|
};
|
||||||
|
|
||||||
mountPoint = lib.mkOption {
|
mountPoint = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Mount point to mount the webdav endpoint on.";
|
description = "Mount point to mount the webdav endpoint on.";
|
||||||
example = "/mnt";
|
example = "/mnt";
|
||||||
};
|
};
|
||||||
|
|
||||||
username = lib.mkOption {
|
username = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Username to connect to the webdav endpoint.";
|
description = "Username to connect to the webdav endpoint.";
|
||||||
};
|
};
|
||||||
|
|
||||||
passwordFile = lib.mkOption {
|
passwordFile = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Password to connect to the webdav endpoint.";
|
description = "Password to connect to the webdav endpoint.";
|
||||||
};
|
};
|
||||||
|
|
||||||
uid = lib.mkOption {
|
uid = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.int;
|
type = lib.types.nullOr lib.types.int;
|
||||||
description = "User owner of the mount point.";
|
description = "User owner of the mount point.";
|
||||||
example = 1000;
|
example = 1000;
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
gid = lib.mkOption {
|
gid = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.int;
|
type = lib.types.nullOr lib.types.int;
|
||||||
description = "Group owner of the mount point.";
|
description = "Group owner of the mount point.";
|
||||||
example = 1000;
|
example = 1000;
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
fileMode = lib.mkOption {
|
fileMode = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
description = "File creation mode";
|
description = "File creation mode";
|
||||||
example = "0664";
|
example = "0664";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
directoryMode = lib.mkOption {
|
directoryMode = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
description = "Directory creation mode";
|
description = "Directory creation mode";
|
||||||
example = "2775";
|
example = "2775";
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
automount = lib.mkOption {
|
automount = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Create a systemd automount unit";
|
description = "Create a systemd automount unit";
|
||||||
default = true;
|
default = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -93,6 +95,6 @@ in
|
||||||
mountConfig.TimeoutSec = 15;
|
mountConfig.TimeoutSec = 15;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
map mkMountCfg cfg.mounts;
|
map mkMountCfg cfg.mounts;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,102 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.shb.hardcodedsecret;
|
cfg = config.shb.hardcodedsecret;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
inherit (lib) mapAttrs' mkOption nameValuePair;
|
inherit (lib) mapAttrs' mkOption nameValuePair;
|
||||||
inherit (lib.types) attrsOf nullOr str submodule;
|
inherit (lib.types)
|
||||||
|
attrsOf
|
||||||
|
nullOr
|
||||||
|
str
|
||||||
|
submodule
|
||||||
|
;
|
||||||
inherit (pkgs) writeText;
|
inherit (pkgs) writeText;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.hardcodedsecret = mkOption {
|
options.shb.hardcodedsecret = mkOption {
|
||||||
default = {};
|
default = { };
|
||||||
description = ''
|
description = ''
|
||||||
Hardcoded secrets. These should only be used in tests.
|
Hardcoded secrets. These should only be used in tests.
|
||||||
'';
|
'';
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
{
|
{
|
||||||
mySecret = {
|
mySecret = {
|
||||||
request = {
|
request = {
|
||||||
user = "me";
|
user = "me";
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
restartUnits = [ "myservice.service" ];
|
restartUnits = [ "myservice.service" ];
|
||||||
|
};
|
||||||
|
settings.content = "My Secret";
|
||||||
};
|
};
|
||||||
settings.content = "My Secret";
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
'';
|
'';
|
||||||
type = attrsOf (submodule ({ name, ... }: {
|
type = attrsOf (
|
||||||
options = contracts.secret.mkProvider {
|
submodule (
|
||||||
settings = mkOption {
|
{ name, ... }:
|
||||||
description = ''
|
{
|
||||||
Settings specific to the hardcoded secret module.
|
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 {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
content = mkOption {
|
content = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = ''
|
description = ''
|
||||||
Content of the secret as a string.
|
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.
|
This will be stored in the nix store and should only be used for testing or maybe in dev.
|
||||||
'';
|
'';
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
source = mkOption {
|
source = mkOption {
|
||||||
type = nullOr str;
|
type = nullOr str;
|
||||||
description = ''
|
description = ''
|
||||||
Source of the content of the secret as a path in the nix store.
|
Source of the content of the secret as a path in the nix store.
|
||||||
'';
|
'';
|
||||||
default = null;
|
default = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
resultCfg = {
|
resultCfg = {
|
||||||
path = "/run/hardcodedsecrets/hardcodedsecret_${name}";
|
path = "/run/hardcodedsecrets/hardcodedsecret_${name}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
system.activationScripts = mapAttrs' (n: cfg':
|
system.activationScripts = mapAttrs' (
|
||||||
|
n: cfg':
|
||||||
let
|
let
|
||||||
source = if cfg'.settings.source != null
|
source =
|
||||||
then cfg'.settings.source
|
if cfg'.settings.source != null then
|
||||||
else writeText "hardcodedsecret_${n}_content" cfg'.settings.content;
|
cfg'.settings.source
|
||||||
|
else
|
||||||
|
writeText "hardcodedsecret_${n}_content" cfg'.settings.content;
|
||||||
in
|
in
|
||||||
nameValuePair "hardcodedsecret_${n}" ''
|
nameValuePair "hardcodedsecret_${n}" ''
|
||||||
mkdir -p "$(dirname "${cfg'.result.path}")"
|
mkdir -p "$(dirname "${cfg'.result.path}")"
|
||||||
touch "${cfg'.result.path}"
|
touch "${cfg'.result.path}"
|
||||||
chmod ${cfg'.request.mode} "${cfg'.result.path}"
|
chmod ${cfg'.request.mode} "${cfg'.result.path}"
|
||||||
chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}"
|
chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}"
|
||||||
cp ${source} "${cfg'.result.path}"
|
cp ${source} "${cfg'.result.path}"
|
||||||
''
|
''
|
||||||
) cfg;
|
) cfg;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.lldap;
|
cfg = config.shb.lldap;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
|
|
@ -145,7 +150,9 @@ in
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
default = { path = "/var/lib/lldap"; };
|
default = {
|
||||||
|
path = "/var/lib/lldap";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
backup = lib.mkOption {
|
backup = lib.mkOption {
|
||||||
|
|
@ -327,7 +334,7 @@ in
|
||||||
default = true;
|
default = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
|
|
@ -340,10 +347,16 @@ in
|
||||||
locations."/" = {
|
locations."/" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
'' + (if isNull cfg.restrictAccessIPRange then "" else ''
|
''
|
||||||
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}/";
|
proxyPass = "http://${toString config.services.lldap.settings.http_host}:${toString config.shb.lldap.webUIListenPort}/";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -354,7 +367,7 @@ in
|
||||||
group = "lldap";
|
group = "lldap";
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
};
|
};
|
||||||
users.groups.lldap = {};
|
users.groups.lldap = { };
|
||||||
|
|
||||||
services.lldap = {
|
services.lldap = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
@ -382,9 +395,13 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
inherit (cfg) ensureGroups ensureUserFields ensureGroupFields;
|
inherit (cfg) ensureGroups ensureUserFields ensureGroupFields;
|
||||||
ensureUsers = lib.mapAttrs (n: v: (lib.removeAttrs v [ "password" ]) // {
|
ensureUsers = lib.mapAttrs (
|
||||||
"password_file" = toString v.password.result.path;
|
n: v:
|
||||||
}) cfg.ensureUsers;
|
(lib.removeAttrs v [ "password" ])
|
||||||
|
// {
|
||||||
|
"password_file" = toString v.password.result.path;
|
||||||
|
}
|
||||||
|
) cfg.ensureUsers;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.mitmdump.instances."lldap-web" = lib.mkIf cfg.debug {
|
shb.mitmdump.instances."lldap-web" = lib.mkIf cfg.debug {
|
||||||
|
|
@ -393,7 +410,8 @@ in
|
||||||
after = [ "lldap.service" ];
|
after = [ "lldap.service" ];
|
||||||
enabledAddons = [ config.shb.mitmdump.addons.logger ];
|
enabledAddons = [ config.shb.mitmdump.addons.logger ];
|
||||||
extraArgs = [
|
extraArgs = [
|
||||||
"--set" "verbose_pattern=/api"
|
"--set"
|
||||||
|
"verbose_pattern=/api"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,163 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib) mapAttrs' mkOption nameValuePair types;
|
inherit (lib)
|
||||||
inherit (types) attrsOf listOf port submodule str;
|
mapAttrs'
|
||||||
|
mkOption
|
||||||
|
nameValuePair
|
||||||
|
types
|
||||||
|
;
|
||||||
|
inherit (types)
|
||||||
|
attrsOf
|
||||||
|
listOf
|
||||||
|
port
|
||||||
|
submodule
|
||||||
|
str
|
||||||
|
;
|
||||||
|
|
||||||
cfg = config.shb.mitmdump;
|
cfg = config.shb.mitmdump;
|
||||||
|
|
||||||
mitmdumpScript = pkgs.writers.writePython3Bin "mitmdump"
|
mitmdumpScript =
|
||||||
{
|
pkgs.writers.writePython3Bin "mitmdump"
|
||||||
libraries = let
|
{
|
||||||
p = pkgs.python3Packages;
|
libraries =
|
||||||
in [
|
let
|
||||||
p.systemd
|
p = pkgs.python3Packages;
|
||||||
p.mitmproxy
|
in
|
||||||
];
|
|
||||||
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(
|
|
||||||
[
|
[
|
||||||
MITMDUMP_BIN,
|
p.systemd
|
||||||
"--listen-host", args.listen_host,
|
p.mitmproxy
|
||||||
"-p", args.listen_port,
|
];
|
||||||
"--mode", f"reverse:{args.upstream_host}:{args.upstream_port}",
|
flakeIgnore = [ "E501" ];
|
||||||
] + rest,
|
}
|
||||||
stdout=sys.stdout,
|
''
|
||||||
stderr=sys.stderr,
|
from systemd.daemon import notify
|
||||||
)
|
import argparse
|
||||||
|
import logging
|
||||||
logging.info(f"Waiting for mitmdump instance to start on port {args.listen_port}.")
|
import os
|
||||||
if wait_for_port("127.0.0.1", args.listen_port, timeout=10):
|
import subprocess
|
||||||
logging.info(f"Mitmdump is started on port {args.listen_port}.")
|
import socket
|
||||||
notify("READY=1")
|
import sys
|
||||||
else:
|
import time
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
|
||||||
|
|
||||||
class RegexLogger:
|
def wait_for_port(host, port, timeout=10):
|
||||||
def __init__(self):
|
deadline = time.time() + timeout
|
||||||
self.verbose_patterns = None
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
def load(self, loader):
|
with socket.create_connection((host, port), timeout=0.5):
|
||||||
loader.add_option(
|
return True
|
||||||
name="verbose_pattern",
|
except Exception:
|
||||||
typespec=Sequence[str],
|
time.sleep(0.1)
|
||||||
default=[],
|
return False
|
||||||
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:
|
def flatten(xss):
|
||||||
return (
|
return [x for xs in xss for x in xs]
|
||||||
"\n"
|
|
||||||
"RequestHeaders:\n"
|
|
||||||
f" {format_headers(flow.request.headers.items())}\n"
|
parser = argparse.ArgumentParser()
|
||||||
f"RequestBody: {flow.request.get_text()}\n"
|
parser.add_argument("--listen_host", default="127.0.0.1", help="Host mitmdump will listen on")
|
||||||
f"Status: {flow.response.data.status_code}\n"
|
parser.add_argument("--listen_port", required=True, help="Port mitmdump will listen on")
|
||||||
"ResponseHeaders:\n"
|
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")
|
||||||
f" {format_headers(flow.response.headers.items())}\n"
|
parser.add_argument("--upstream_port", required=True, help="Port mitmdump will connect to for upstream")
|
||||||
f"ResponseBody: {flow.response.get_text()}\n"
|
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:
|
proc.wait()
|
||||||
return "\n ".join(k + ": " + v for k, v in headers)
|
'';
|
||||||
|
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
options.shb.mitmdump = {
|
options.shb.mitmdump = {
|
||||||
addons = mkOption {
|
addons = mkOption {
|
||||||
type = attrsOf str;
|
type = attrsOf str;
|
||||||
default = [];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Addons available to the be added to the mitmdump instance.
|
Addons available to the be added to the mitmdump instance.
|
||||||
|
|
||||||
|
|
@ -146,120 +166,129 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
instances = mkOption {
|
instances = mkOption {
|
||||||
default = {};
|
default = { };
|
||||||
description = "Mitmdump instance.";
|
description = "Mitmdump instance.";
|
||||||
type = attrsOf (submodule ({ name, ... }: {
|
type = attrsOf (
|
||||||
options = {
|
submodule (
|
||||||
package = lib.mkPackageOption pkgs "mitmproxy" {};
|
{ name, ... }:
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
package = lib.mkPackageOption pkgs "mitmproxy" { };
|
||||||
|
|
||||||
serviceName = mkOption {
|
serviceName = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = ''
|
description = ''
|
||||||
Name of the mitmdump system service.
|
Name of the mitmdump system service.
|
||||||
'';
|
'';
|
||||||
default = "mitmdump-${name}.service";
|
default = "mitmdump-${name}.service";
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
listenHost = mkOption {
|
listenHost = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
default = "127.0.0.1";
|
default = "127.0.0.1";
|
||||||
description = ''
|
description = ''
|
||||||
Host the mitmdump instance will connect on.
|
Host the mitmdump instance will connect on.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
listenPort = mkOption {
|
listenPort = mkOption {
|
||||||
type = port;
|
type = port;
|
||||||
description = ''
|
description = ''
|
||||||
Port the mitmdump instance will listen on.
|
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 {
|
upstreamHost = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
default = "http://127.0.0.1";
|
default = "http://127.0.0.1";
|
||||||
description = ''
|
description = ''
|
||||||
Host the mitmdump instance will connect to.
|
Host the mitmdump instance will connect to.
|
||||||
|
|
||||||
If only an IP or domain is provided,
|
If only an IP or domain is provided,
|
||||||
mitmdump will default to connect using HTTPS.
|
mitmdump will default to connect using HTTPS.
|
||||||
If this is not wanted, prefix the IP or domain with the 'http://' protocol.
|
If this is not wanted, prefix the IP or domain with the 'http://' protocol.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
upstreamPort = mkOption {
|
upstreamPort = mkOption {
|
||||||
type = port;
|
type = port;
|
||||||
description = ''
|
description = ''
|
||||||
Port the mitmdump instance will connect to.
|
Port the mitmdump instance will connect to.
|
||||||
|
|
||||||
The port the server is listening on.
|
The port the server is listening on.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
after = mkOption {
|
after = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
default = [];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Systemd services that must be started before this mitmdump proxy instance.
|
Systemd services that must be started before this mitmdump proxy instance.
|
||||||
|
|
||||||
You are guaranteed the mitmdump is listening on the `listenPort`
|
You are guaranteed the mitmdump is listening on the `listenPort`
|
||||||
when its systemd service has started.
|
when its systemd service has started.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
enabledAddons = mkOption {
|
enabledAddons = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
default = [];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Addons to enable on this mitmdump instance.
|
Addons to enable on this mitmdump instance.
|
||||||
'';
|
'';
|
||||||
example = lib.literalExpression ''[ config.shb.mitmdump.addons.logger ]'';
|
example = lib.literalExpression ''[ config.shb.mitmdump.addons.logger ]'';
|
||||||
};
|
};
|
||||||
|
|
||||||
extraArgs = mkOption {
|
extraArgs = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
default = [];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
Extra arguments to pass to the mitmdump instance.
|
Extra arguments to pass to the mitmdump instance.
|
||||||
|
|
||||||
See upstream [manual](https://docs.mitmproxy.org/stable/concepts/options/#flow_detail) for all possible options.
|
See upstream [manual](https://docs.mitmproxy.org/stable/concepts/options/#flow_detail) for all possible options.
|
||||||
'';
|
'';
|
||||||
example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]'';
|
example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
systemd.services = mapAttrs' (name: cfg': nameValuePair "mitmdump-${name}" {
|
systemd.services = mapAttrs' (
|
||||||
environment = {
|
name: cfg':
|
||||||
"HOME" = "/var/lib/private/mitmdump-${name}";
|
nameValuePair "mitmdump-${name}" {
|
||||||
"MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump";
|
environment = {
|
||||||
};
|
"HOME" = "/var/lib/private/mitmdump-${name}";
|
||||||
serviceConfig = {
|
"MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump";
|
||||||
Type = "notify";
|
};
|
||||||
Restart = "on-failure";
|
serviceConfig = {
|
||||||
StandardOutput = "journal";
|
Type = "notify";
|
||||||
StandardError = "journal";
|
Restart = "on-failure";
|
||||||
|
StandardOutput = "journal";
|
||||||
|
StandardError = "journal";
|
||||||
|
|
||||||
DynamicUser = true;
|
DynamicUser = true;
|
||||||
WorkingDirectory = "/var/lib/mitmdump-${name}";
|
WorkingDirectory = "/var/lib/mitmdump-${name}";
|
||||||
StateDirectory = "mitmdump-${name}";
|
StateDirectory = "mitmdump-${name}";
|
||||||
|
|
||||||
ExecStart = let
|
ExecStart =
|
||||||
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
|
let
|
||||||
extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
|
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
|
||||||
in
|
extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
|
||||||
"${lib.getExe mitmdumpScript} --listen_host ${cfg'.listenHost} --listen_port ${toString cfg'.listenPort} --upstream_host ${cfg'.upstreamHost} --upstream_port ${toString cfg'.upstreamPort} ${addons} ${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;
|
requires = cfg'.after;
|
||||||
wantedBy = [ "multi-user.target" ];
|
after = cfg'.after;
|
||||||
}) cfg.instances;
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
}
|
||||||
|
) cfg.instances;
|
||||||
|
|
||||||
shb.mitmdump.addons = {
|
shb.mitmdump.addons = {
|
||||||
inherit logger;
|
inherit logger;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.monitoring;
|
cfg = config.shb.monitoring;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
|
|
@ -53,7 +58,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
lokiMajorVersion = lib.mkOption {
|
lokiMajorVersion = lib.mkOption {
|
||||||
type = lib.types.enum [ 2 3 ];
|
type = lib.types.enum [
|
||||||
|
2
|
||||||
|
3
|
||||||
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Switching from version 2 to 3 requires manual intervention
|
Switching from version 2 to 3 requires manual intervention
|
||||||
https://grafana.com/docs/loki/latest/setup/upgrade/#main--unreleased. So this let's the user
|
https://grafana.com/docs/loki/latest/setup/upgrade/#main--unreleased. So this let's the user
|
||||||
|
|
@ -84,7 +92,7 @@ in
|
||||||
contactPoints = lib.mkOption {
|
contactPoints = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
description = "List of email addresses to send alerts to";
|
description = "List of email addresses to send alerts to";
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
adminPassword = lib.mkOption {
|
adminPassword = lib.mkOption {
|
||||||
|
|
@ -114,37 +122,39 @@ in
|
||||||
smtp = lib.mkOption {
|
smtp = lib.mkOption {
|
||||||
description = "SMTP options.";
|
description = "SMTP options.";
|
||||||
default = null;
|
default = null;
|
||||||
type = lib.types.nullOr (lib.types.submodule {
|
type = lib.types.nullOr (
|
||||||
options = {
|
lib.types.submodule {
|
||||||
from_address = lib.mkOption {
|
options = {
|
||||||
type = lib.types.str;
|
from_address = lib.mkOption {
|
||||||
description = "SMTP address from which the emails originate.";
|
type = lib.types.str;
|
||||||
example = "vaultwarden@mydomain.com";
|
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 = {
|
services.grafana.provision = {
|
||||||
dashboards.settings = lib.mkIf cfg.provisionDashboards {
|
dashboards.settings = lib.mkIf cfg.provisionDashboards {
|
||||||
apiVersion = 1;
|
apiVersion = 1;
|
||||||
providers = [{
|
providers = [
|
||||||
folder = "Self Host Blocks";
|
{
|
||||||
options.path = ./monitoring/dashboards;
|
folder = "Self Host Blocks";
|
||||||
allowUiUpdates = true;
|
options.path = ./monitoring/dashboards;
|
||||||
disableDeletion = true;
|
allowUiUpdates = true;
|
||||||
}];
|
disableDeletion = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
datasources.settings = {
|
datasources.settings = {
|
||||||
apiVersion = 1;
|
apiVersion = 1;
|
||||||
|
|
@ -245,26 +257,35 @@ in
|
||||||
};
|
};
|
||||||
alerting.contactPoints.settings = {
|
alerting.contactPoints.settings = {
|
||||||
apiVersion = 1;
|
apiVersion = 1;
|
||||||
contactPoints = [{
|
contactPoints = [
|
||||||
inherit (cfg) orgId;
|
{
|
||||||
name = "grafana-default-email";
|
inherit (cfg) orgId;
|
||||||
receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [{
|
name = "grafana-default-email";
|
||||||
uid = "sysadmin";
|
receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [
|
||||||
type = "email";
|
{
|
||||||
settings.addresses = lib.concatStringsSep ";" cfg.contactPoints;
|
uid = "sysadmin";
|
||||||
}];
|
type = "email";
|
||||||
}];
|
settings.addresses = lib.concatStringsSep ";" cfg.contactPoints;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
alerting.policies.settings = {
|
alerting.policies.settings = {
|
||||||
apiVersion = 1;
|
apiVersion = 1;
|
||||||
policies = [{
|
policies = [
|
||||||
inherit (cfg) orgId;
|
{
|
||||||
receiver = "grafana-default-email";
|
inherit (cfg) orgId;
|
||||||
group_by = [ "grafana_folder" "alertname" ];
|
receiver = "grafana-default-email";
|
||||||
group_wait = "30s";
|
group_by = [
|
||||||
group_interval = "5m";
|
"grafana_folder"
|
||||||
repeat_interval = "4h";
|
"alertname"
|
||||||
}];
|
];
|
||||||
|
group_wait = "30s";
|
||||||
|
group_interval = "5m";
|
||||||
|
repeat_interval = "4h";
|
||||||
|
}
|
||||||
|
];
|
||||||
# resetPolicies seems to happen after setting the above policies, effectively rolling back
|
# resetPolicies seems to happen after setting the above policies, effectively rolling back
|
||||||
# any updates.
|
# any updates.
|
||||||
};
|
};
|
||||||
|
|
@ -273,18 +294,20 @@ in
|
||||||
rules = builtins.fromJSON (builtins.readFile ./monitoring/rules.json);
|
rules = builtins.fromJSON (builtins.readFile ./monitoring/rules.json);
|
||||||
ruleIds = map (r: r.uid) rules;
|
ruleIds = map (r: r.uid) rules;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
apiVersion = 1;
|
apiVersion = 1;
|
||||||
groups = [{
|
groups = [
|
||||||
|
{
|
||||||
inherit (cfg) orgId;
|
inherit (cfg) orgId;
|
||||||
name = "SysAdmin";
|
name = "SysAdmin";
|
||||||
folder = "Self Host Blocks";
|
folder = "Self Host Blocks";
|
||||||
interval = "10m";
|
interval = "10m";
|
||||||
inherit rules;
|
inherit rules;
|
||||||
}];
|
}
|
||||||
# deleteRules seems to happen after creating the above rules, effectively rolling back
|
];
|
||||||
# any updates.
|
# deleteRules seems to happen after creating the above rules, effectively rolling back
|
||||||
};
|
# any updates.
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
services.prometheus = {
|
services.prometheus = {
|
||||||
|
|
@ -295,36 +318,45 @@ in
|
||||||
services.loki = {
|
services.loki = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/loki";
|
dataDir = "/var/lib/loki";
|
||||||
package = if cfg.lokiMajorVersion == 3 then pkgs.grafana-loki else
|
package =
|
||||||
# Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4
|
if cfg.lokiMajorVersion == 3 then
|
||||||
(pkgs.grafana-loki.overrideAttrs (finalAttrs: previousAttrs: rec {
|
pkgs.grafana-loki
|
||||||
version = "2.9.6";
|
else
|
||||||
|
# Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4
|
||||||
|
(pkgs.grafana-loki.overrideAttrs (
|
||||||
|
finalAttrs: previousAttrs: rec {
|
||||||
|
version = "2.9.6";
|
||||||
|
|
||||||
src = pkgs.fetchFromGitHub {
|
src = pkgs.fetchFromGitHub {
|
||||||
owner = "grafana";
|
owner = "grafana";
|
||||||
repo = "loki";
|
repo = "loki";
|
||||||
rev = "v${version}";
|
rev = "v${version}";
|
||||||
hash = "sha256-79hK7axHf6soku5DvdXkE/0K4WKc4pnS9VMbVc1FS2I=";
|
hash = "sha256-79hK7axHf6soku5DvdXkE/0K4WKc4pnS9VMbVc1FS2I=";
|
||||||
};
|
};
|
||||||
|
|
||||||
subPackages = [
|
subPackages = [
|
||||||
"cmd/loki"
|
"cmd/loki"
|
||||||
"cmd/loki-canary"
|
"cmd/loki-canary"
|
||||||
"clients/cmd/promtail"
|
"clients/cmd/promtail"
|
||||||
"cmd/logcli"
|
"cmd/logcli"
|
||||||
# Removes "cmd/lokitool"
|
# Removes "cmd/lokitool"
|
||||||
];
|
];
|
||||||
|
|
||||||
ldflags = let t = "github.com/grafana/loki/pkg/util/build"; in [
|
ldflags =
|
||||||
"-s"
|
let
|
||||||
"-w"
|
t = "github.com/grafana/loki/pkg/util/build";
|
||||||
"-X ${t}.Version=${version}"
|
in
|
||||||
"-X ${t}.BuildUser=nix@nixpkgs"
|
[
|
||||||
"-X ${t}.BuildDate=unknown"
|
"-s"
|
||||||
"-X ${t}.Branch=unknown"
|
"-w"
|
||||||
"-X ${t}.Revision=unknown"
|
"-X ${t}.Version=${version}"
|
||||||
];
|
"-X ${t}.BuildUser=nix@nixpkgs"
|
||||||
}));
|
"-X ${t}.BuildDate=unknown"
|
||||||
|
"-X ${t}.Branch=unknown"
|
||||||
|
"-X ${t}.Revision=unknown"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
));
|
||||||
configuration = {
|
configuration = {
|
||||||
auth_enabled = false;
|
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}";
|
proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -448,61 +480,75 @@ in
|
||||||
services.prometheus.scrapeConfigs = [
|
services.prometheus.scrapeConfigs = [
|
||||||
{
|
{
|
||||||
job_name = "node";
|
job_name = "node";
|
||||||
static_configs = [{
|
static_configs = [
|
||||||
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.node.port}"];
|
{
|
||||||
labels = commonLabels;
|
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ];
|
||||||
}];
|
labels = commonLabels;
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
job_name = "netdata";
|
job_name = "netdata";
|
||||||
metrics_path = "/api/v1/allmetrics";
|
metrics_path = "/api/v1/allmetrics";
|
||||||
params.format = [ "prometheus" ];
|
params.format = [ "prometheus" ];
|
||||||
honor_labels = true;
|
honor_labels = true;
|
||||||
static_configs = [{
|
static_configs = [
|
||||||
targets = [ "127.0.0.1:19999" ];
|
{
|
||||||
labels = commonLabels;
|
targets = [ "127.0.0.1:19999" ];
|
||||||
}];
|
labels = commonLabels;
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
job_name = "smartctl";
|
job_name = "smartctl";
|
||||||
static_configs = [{
|
static_configs = [
|
||||||
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}"];
|
{
|
||||||
labels = commonLabels;
|
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}" ];
|
||||||
}];
|
labels = commonLabels;
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
job_name = "prometheus_internal";
|
job_name = "prometheus_internal";
|
||||||
static_configs = [{
|
static_configs = [
|
||||||
targets = ["127.0.0.1:${toString config.services.prometheus.port}"];
|
{
|
||||||
labels = commonLabels;
|
targets = [ "127.0.0.1:${toString config.services.prometheus.port}" ];
|
||||||
}];
|
labels = commonLabels;
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
] ++ (lib.lists.optional config.services.nginx.enable {
|
]
|
||||||
job_name = "nginx";
|
++ (lib.lists.optional config.services.nginx.enable {
|
||||||
static_configs = [{
|
job_name = "nginx";
|
||||||
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}"];
|
static_configs = [
|
||||||
|
{
|
||||||
|
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}" ];
|
||||||
labels = commonLabels;
|
labels = commonLabels;
|
||||||
}];
|
}
|
||||||
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) {
|
];
|
||||||
# job_name = "redis";
|
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) {
|
||||||
# static_configs = [
|
# job_name = "redis";
|
||||||
# {
|
# static_configs = [
|
||||||
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.redis.port}"];
|
# {
|
||||||
# }
|
# 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";
|
# }) ++ (lib.optional (builtins.length (lib.attrNames config.services.openvpn.servers) > 0) {
|
||||||
# static_configs = [
|
# job_name = "openvpn";
|
||||||
# {
|
# static_configs = [
|
||||||
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"];
|
# {
|
||||||
# }
|
# targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"];
|
||||||
# ];
|
# }
|
||||||
}) ++ (lib.optional config.services.dnsmasq.enable {
|
# ];
|
||||||
job_name = "dnsmasq";
|
})
|
||||||
static_configs = [{
|
++ (lib.optional config.services.dnsmasq.enable {
|
||||||
targets = ["127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}"];
|
job_name = "dnsmasq";
|
||||||
|
static_configs = [
|
||||||
|
{
|
||||||
|
targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}" ];
|
||||||
labels = commonLabels;
|
labels = commonLabels;
|
||||||
}];
|
}
|
||||||
|
];
|
||||||
});
|
});
|
||||||
services.prometheus.exporters.nginx = lib.mkIf config.services.nginx.enable {
|
services.prometheus.exporters.nginx = lib.mkIf config.services.nginx.enable {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
@ -513,7 +559,7 @@ in
|
||||||
services.prometheus.exporters.node = {
|
services.prometheus.exporters.node = {
|
||||||
enable = true;
|
enable = true;
|
||||||
# https://github.com/prometheus/node_exporter#collectors
|
# https://github.com/prometheus/node_exporter#collectors
|
||||||
enabledCollectors = ["ethtool"];
|
enabledCollectors = [ "ethtool" ];
|
||||||
port = 9112;
|
port = 9112;
|
||||||
listenAddress = "127.0.0.1";
|
listenAddress = "127.0.0.1";
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.nginx;
|
cfg = config.shb.nginx;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = c: "${c.subdomain}.${c.domain}";
|
fqdn = c: "${c.subdomain}.${c.domain}";
|
||||||
|
|
||||||
|
|
@ -42,12 +47,13 @@ let
|
||||||
|
|
||||||
autheliaRules = lib.mkOption {
|
autheliaRules = lib.mkOption {
|
||||||
type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
|
type = lib.types.listOf (lib.types.attrsOf lib.types.anything);
|
||||||
default = [];
|
default = [ ];
|
||||||
description = "Authelia rule configuration";
|
description = "Authelia rule configuration";
|
||||||
example = lib.literalExpression ''[{
|
example = lib.literalExpression ''
|
||||||
policy = "two_factor";
|
[{
|
||||||
subject = ["group:service_user"];
|
policy = "two_factor";
|
||||||
}]'';
|
subject = ["group:service_user"];
|
||||||
|
}]'';
|
||||||
};
|
};
|
||||||
|
|
||||||
extraConfig = lib.mkOption {
|
extraConfig = lib.mkOption {
|
||||||
|
|
@ -77,41 +83,44 @@ in
|
||||||
vhosts = lib.mkOption {
|
vhosts = lib.mkOption {
|
||||||
description = "Endpoints to be protected by authelia.";
|
description = "Endpoints to be protected by authelia.";
|
||||||
type = lib.types.listOf vhostConfig;
|
type = lib.types.listOf vhostConfig;
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
|
||||||
services.nginx.enable = true;
|
services.nginx.enable = true;
|
||||||
services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn";
|
services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn";
|
||||||
services.nginx.appendHttpConfig = lib.mkIf cfg.accessLog ''
|
services.nginx.appendHttpConfig = lib.mkIf cfg.accessLog ''
|
||||||
log_format apm
|
log_format apm
|
||||||
'{'
|
'{'
|
||||||
'"remote_addr":"$remote_addr",'
|
'"remote_addr":"$remote_addr",'
|
||||||
'"remote_user":"$remote_user",'
|
'"remote_user":"$remote_user",'
|
||||||
'"time_local":"$time_local",'
|
'"time_local":"$time_local",'
|
||||||
'"request":"$request",'
|
'"request":"$request",'
|
||||||
'"request_length":"$request_length",'
|
'"request_length":"$request_length",'
|
||||||
'"server_name":"$server_name",'
|
'"server_name":"$server_name",'
|
||||||
'"status":"$status",'
|
'"status":"$status",'
|
||||||
'"bytes_sent":"$bytes_sent",'
|
'"bytes_sent":"$bytes_sent",'
|
||||||
'"body_bytes_sent":"$body_bytes_sent",'
|
'"body_bytes_sent":"$body_bytes_sent",'
|
||||||
'"referrer":"$http_referrer",'
|
'"referrer":"$http_referrer",'
|
||||||
'"user_agent":"$http_user_agent",'
|
'"user_agent":"$http_user_agent",'
|
||||||
'"gzip_ration":"$gzip_ratio",'
|
'"gzip_ration":"$gzip_ratio",'
|
||||||
'"post":"$request_body",'
|
'"post":"$request_body",'
|
||||||
'"upstream_addr":"$upstream_addr",'
|
'"upstream_addr":"$upstream_addr",'
|
||||||
'"upstream_status":"$upstream_status",'
|
'"upstream_status":"$upstream_status",'
|
||||||
'"request_time":"$request_time",'
|
'"request_time":"$request_time",'
|
||||||
'"upstream_response_time":"$upstream_response_time",'
|
'"upstream_response_time":"$upstream_response_time",'
|
||||||
'"upstream_connect_time":"$upstream_connect_time",'
|
'"upstream_connect_time":"$upstream_connect_time",'
|
||||||
'"upstream_header_time":"$upstream_header_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 =
|
services.nginx.virtualHosts =
|
||||||
let
|
let
|
||||||
|
|
@ -189,13 +198,13 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.mkMerge (map vhostCfg cfg.vhosts);
|
lib.mkMerge (map vhostCfg cfg.vhosts);
|
||||||
|
|
||||||
shb.authelia.rules =
|
shb.authelia.rules =
|
||||||
let
|
let
|
||||||
authConfig = c: map (r: r // { domain = fqdn c; }) c.autheliaRules;
|
authConfig = c: map (r: r // { domain = fqdn c; }) c.autheliaRules;
|
||||||
in
|
in
|
||||||
lib.flatten (map authConfig cfg.vhosts);
|
lib.flatten (map authConfig cfg.vhosts);
|
||||||
|
|
||||||
security.acme.defaults.reloadServices = [
|
security.acme.defaults.reloadServices = [
|
||||||
"nginx.service"
|
"nginx.service"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.shb.postgresql;
|
cfg = config.shb.postgresql;
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
upgrade-script = old: new:
|
upgrade-script =
|
||||||
|
old: new:
|
||||||
let
|
let
|
||||||
oldStr = builtins.toString old;
|
oldStr = builtins.toString old;
|
||||||
newStr = builtins.toString new;
|
newStr = builtins.toString new;
|
||||||
|
|
@ -11,37 +17,37 @@ let
|
||||||
oldPkg = pkgs.${"postgresql_${oldStr}"};
|
oldPkg = pkgs.${"postgresql_${oldStr}"};
|
||||||
newPkg = pkgs.${"postgresql_${newStr}"};
|
newPkg = pkgs.${"postgresql_${newStr}"};
|
||||||
in
|
in
|
||||||
pkgs.writeScriptBin "upgrade-pg-cluster-${oldStr}-${newStr}" ''
|
pkgs.writeScriptBin "upgrade-pg-cluster-${oldStr}-${newStr}" ''
|
||||||
set -eux
|
set -eux
|
||||||
# XXX it's perhaps advisable to stop all services that depend on postgresql
|
# XXX it's perhaps advisable to stop all services that depend on postgresql
|
||||||
systemctl stop postgresql
|
systemctl stop postgresql
|
||||||
|
|
||||||
export NEWDATA="/var/lib/postgresql/${newPkg.psqlSchema}"
|
export NEWDATA="/var/lib/postgresql/${newPkg.psqlSchema}"
|
||||||
export NEWBIN="${newPkg}/bin"
|
export NEWBIN="${newPkg}/bin"
|
||||||
|
|
||||||
export OLDDATA="/var/lib/postgresql/${oldPkg.psqlSchema}"
|
export OLDDATA="/var/lib/postgresql/${oldPkg.psqlSchema}"
|
||||||
export OLDBIN="${oldPkg}/bin"
|
export OLDBIN="${oldPkg}/bin"
|
||||||
|
|
||||||
install -d -m 0700 -o postgres -g postgres "$NEWDATA"
|
install -d -m 0700 -o postgres -g postgres "$NEWDATA"
|
||||||
cd "$NEWDATA"
|
cd "$NEWDATA"
|
||||||
sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
|
sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
|
||||||
|
|
||||||
sudo -u postgres $NEWBIN/pg_upgrade \
|
sudo -u postgres $NEWBIN/pg_upgrade \
|
||||||
--old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
|
--old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
|
||||||
--old-bindir $OLDBIN --new-bindir $NEWBIN \
|
--old-bindir $OLDBIN --new-bindir $NEWBIN \
|
||||||
"$@"
|
"$@"
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.postgresql = {
|
options.shb.postgresql = {
|
||||||
debug = lib.mkOption {
|
debug = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = ''
|
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;
|
default = false;
|
||||||
};
|
};
|
||||||
enableTCPIP = lib.mkOption {
|
enableTCPIP = lib.mkOption {
|
||||||
|
|
@ -55,7 +61,7 @@ in
|
||||||
Backup configuration.
|
Backup configuration.
|
||||||
'';
|
'';
|
||||||
|
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.databasebackup.mkRequester {
|
options = contracts.databasebackup.mkRequester {
|
||||||
user = "postgres";
|
user = "postgres";
|
||||||
|
|
@ -75,27 +81,29 @@ in
|
||||||
|
|
||||||
ensures = lib.mkOption {
|
ensures = lib.mkOption {
|
||||||
description = "List of username, database and/or passwords that should be created.";
|
description = "List of username, database and/or passwords that should be created.";
|
||||||
type = lib.types.listOf (lib.types.submodule {
|
type = lib.types.listOf (
|
||||||
options = {
|
lib.types.submodule {
|
||||||
username = lib.mkOption {
|
options = {
|
||||||
type = lib.types.str;
|
username = lib.mkOption {
|
||||||
description = "Postgres user name.";
|
type = lib.types.str;
|
||||||
};
|
description = "Postgres user name.";
|
||||||
|
};
|
||||||
|
|
||||||
database = lib.mkOption {
|
database = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Postgres database.";
|
description = "Postgres database.";
|
||||||
};
|
};
|
||||||
|
|
||||||
passwordFile = lib.mkOption {
|
passwordFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
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.";
|
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;
|
default = null;
|
||||||
example = "/run/secrets/postgresql/password";
|
example = "/run/secrets/postgresql/password";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
});
|
);
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -123,51 +131,61 @@ in
|
||||||
dbConfig = ensureCfgs: {
|
dbConfig = ensureCfgs: {
|
||||||
services.postgresql.enable = lib.mkDefault ((builtins.length ensureCfgs) > 0);
|
services.postgresql.enable = lib.mkDefault ((builtins.length ensureCfgs) > 0);
|
||||||
services.postgresql.ensureDatabases = map ({ database, ... }: database) ensureCfgs;
|
services.postgresql.ensureDatabases = map ({ database, ... }: database) ensureCfgs;
|
||||||
services.postgresql.ensureUsers = map ({ username, database, ... }: {
|
services.postgresql.ensureUsers = map (
|
||||||
name = username;
|
{ username, database, ... }:
|
||||||
ensureDBOwnership = true;
|
{
|
||||||
ensureClauses.login = true;
|
name = username;
|
||||||
}) ensureCfgs;
|
ensureDBOwnership = true;
|
||||||
|
ensureClauses.login = true;
|
||||||
|
}
|
||||||
|
) ensureCfgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
pwdConfig = ensureCfgs: {
|
pwdConfig = ensureCfgs: {
|
||||||
systemd.services.postgresql-setup.script = lib.mkAfter
|
systemd.services.postgresql-setup.script = lib.mkAfter (
|
||||||
(let
|
let
|
||||||
prefix = ''
|
prefix = ''
|
||||||
psql -tA <<'EOF'
|
psql -tA <<'EOF'
|
||||||
DO $$
|
DO $$
|
||||||
DECLARE password TEXT;
|
DECLARE password TEXT;
|
||||||
BEGIN
|
BEGIN
|
||||||
'';
|
'';
|
||||||
suffix = ''
|
suffix = ''
|
||||||
END $$;
|
END $$;
|
||||||
EOF
|
EOF
|
||||||
'';
|
|
||||||
exec = { username, passwordFile, ... }: ''
|
|
||||||
password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', '''));
|
|
||||||
EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password);
|
|
||||||
'';
|
'';
|
||||||
|
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;
|
cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs;
|
||||||
in
|
in
|
||||||
if (builtins.length cfgsWithPasswords) == 0 then "" else
|
if (builtins.length cfgsWithPasswords) == 0 then
|
||||||
prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix);
|
""
|
||||||
|
else
|
||||||
|
prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
debugConfig = enableDebug: lib.mkIf enableDebug {
|
debugConfig =
|
||||||
services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements";
|
enableDebug:
|
||||||
};
|
lib.mkIf enableDebug {
|
||||||
|
services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements";
|
||||||
|
};
|
||||||
in
|
in
|
||||||
lib.mkMerge ([
|
lib.mkMerge ([
|
||||||
commonConfig
|
commonConfig
|
||||||
(dbConfig cfg.ensures)
|
(dbConfig cfg.ensures)
|
||||||
(pwdConfig cfg.ensures)
|
(pwdConfig cfg.ensures)
|
||||||
(lib.mkIf cfg.enableTCPIP tcpConfig)
|
(lib.mkIf cfg.enableTCPIP tcpConfig)
|
||||||
(debugConfig cfg.debug)
|
(debugConfig cfg.debug)
|
||||||
{
|
{
|
||||||
environment.systemPackages = lib.mkIf config.services.postgresql.enable [
|
environment.systemPackages = lib.mkIf config.services.postgresql.enable [
|
||||||
(upgrade-script 15 16)
|
(upgrade-script 15 16)
|
||||||
(upgrade-script 16 17)
|
(upgrade-script 16 17)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,167 +1,226 @@
|
||||||
{ config, pkgs, lib, utils, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
utils,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.restic;
|
cfg = config.shb.restic;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
inherit (lib) concatStringsSep filterAttrs flatten literalExpression optionals listToAttrs mapAttrsToList mkEnableOption mkOption mkMerge;
|
inherit (lib)
|
||||||
inherit (lib) hasPrefix mkIf nameValuePair optionalAttrs removePrefix;
|
concatStringsSep
|
||||||
inherit (lib.types) attrsOf enum int ints oneOf nonEmptyStr nullOr str submodule;
|
filterAttrs
|
||||||
|
flatten
|
||||||
|
literalExpression
|
||||||
|
optionals
|
||||||
|
listToAttrs
|
||||||
|
mapAttrsToList
|
||||||
|
mkEnableOption
|
||||||
|
mkOption
|
||||||
|
mkMerge
|
||||||
|
;
|
||||||
|
inherit (lib)
|
||||||
|
hasPrefix
|
||||||
|
mkIf
|
||||||
|
nameValuePair
|
||||||
|
optionalAttrs
|
||||||
|
removePrefix
|
||||||
|
;
|
||||||
|
inherit (lib.types)
|
||||||
|
attrsOf
|
||||||
|
enum
|
||||||
|
int
|
||||||
|
ints
|
||||||
|
oneOf
|
||||||
|
nonEmptyStr
|
||||||
|
nullOr
|
||||||
|
str
|
||||||
|
submodule
|
||||||
|
;
|
||||||
|
|
||||||
commonOptions = { name, prefix, config, ... }: {
|
commonOptions =
|
||||||
enable = mkEnableOption ''
|
{
|
||||||
this backup intance.
|
name,
|
||||||
|
prefix,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
enable = mkEnableOption ''
|
||||||
|
this backup intance.
|
||||||
|
|
||||||
A disabled instance will not backup data anymore
|
A disabled instance will not backup data anymore
|
||||||
but still provides the helper tool to restore snapshots
|
but still provides the helper tool to restore snapshots
|
||||||
'';
|
'';
|
||||||
|
|
||||||
passphrase = lib.mkOption {
|
passphrase = lib.mkOption {
|
||||||
description = "Encryption key for the backup repository.";
|
description = "Encryption key for the backup repository.";
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.secret.mkRequester {
|
options = contracts.secret.mkRequester {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = config.request.user;
|
owner = config.request.user;
|
||||||
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
|
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
|
||||||
restartUnits = [ (fullName name config.settings.repository) ];
|
restartUnits = [ (fullName name config.settings.repository) ];
|
||||||
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
|
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
repository = mkOption {
|
repository = mkOption {
|
||||||
description = "Repositories to back this instance to.";
|
description = "Repositories to back this instance to.";
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
path = mkOption {
|
path = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Repository location";
|
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;
|
|
||||||
};
|
};
|
||||||
description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.'';
|
|
||||||
example = {
|
secrets = mkOption {
|
||||||
OnCalendar = "00:05";
|
type = attrsOf lib.shb.secretFileType;
|
||||||
RandomizedDelaySec = "5h";
|
default = { };
|
||||||
Persistent = true;
|
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 {
|
retention = mkOption {
|
||||||
description = "For how long to keep backup files.";
|
description = "For how long to keep backup files.";
|
||||||
type = attrsOf (oneOf [ int nonEmptyStr ]);
|
type = attrsOf (oneOf [
|
||||||
default = {
|
int
|
||||||
keep_within = "1d";
|
nonEmptyStr
|
||||||
keep_hourly = 24;
|
]);
|
||||||
keep_daily = 7;
|
default = {
|
||||||
keep_weekly = 4;
|
keep_within = "1d";
|
||||||
keep_monthly = 6;
|
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 {
|
repoSlugName = name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (removePrefix "/" name);
|
||||||
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);
|
|
||||||
fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}";
|
fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.restic = {
|
options.shb.restic = {
|
||||||
instances = mkOption {
|
instances = mkOption {
|
||||||
description = "Files to backup following the [backup contract](./contracts-backup.html).";
|
description = "Files to backup following the [backup contract](./contracts-backup.html).";
|
||||||
default = {};
|
default = { };
|
||||||
type = attrsOf (submodule ({ name, config, ... }: {
|
type = attrsOf (
|
||||||
options = contracts.backup.mkProvider {
|
submodule (
|
||||||
settings = mkOption {
|
{ name, config, ... }:
|
||||||
description = ''
|
{
|
||||||
Settings specific to the Restic provider.
|
options = contracts.backup.mkProvider {
|
||||||
'';
|
settings = mkOption {
|
||||||
|
description = ''
|
||||||
|
Settings specific to the Restic provider.
|
||||||
|
'';
|
||||||
|
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = commonOptions { inherit name config; prefix = "instances"; };
|
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 {
|
databases = mkOption {
|
||||||
description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html).";
|
description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html).";
|
||||||
default = {};
|
default = { };
|
||||||
type = attrsOf (submodule ({ name, config, ... }: {
|
type = attrsOf (
|
||||||
options = contracts.databasebackup.mkProvider {
|
submodule (
|
||||||
settings = mkOption {
|
{ name, config, ... }:
|
||||||
description = ''
|
{
|
||||||
Settings specific to the Restic provider.
|
options = contracts.databasebackup.mkProvider {
|
||||||
'';
|
settings = mkOption {
|
||||||
|
description = ''
|
||||||
|
Settings specific to the Restic provider.
|
||||||
|
'';
|
||||||
|
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = commonOptions { inherit name config; prefix = "databases"; };
|
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
|
# Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
|
||||||
performance = mkOption {
|
performance = mkOption {
|
||||||
description = "Reduce performance impact of backup jobs.";
|
description = "Reduce performance impact of backup jobs.";
|
||||||
default = {};
|
default = { };
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
niceness = mkOption {
|
niceness = mkOption {
|
||||||
|
|
@ -170,7 +229,11 @@ in
|
||||||
default = 15;
|
default = 15;
|
||||||
};
|
};
|
||||||
ioSchedulingClass = mkOption {
|
ioSchedulingClass = mkOption {
|
||||||
type = enum [ "idle" "best-effort" "realtime" ];
|
type = enum [
|
||||||
|
"idle"
|
||||||
|
"best-effort"
|
||||||
|
"realtime"
|
||||||
|
];
|
||||||
description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
|
description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands.";
|
||||||
default = "best-effort";
|
default = "best-effort";
|
||||||
};
|
};
|
||||||
|
|
@ -184,23 +247,28 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf (cfg.instances != {} || cfg.databases != {}) (
|
config = mkIf (cfg.instances != { } || cfg.databases != { }) (
|
||||||
let
|
let
|
||||||
enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances;
|
enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances;
|
||||||
enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases;
|
enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases;
|
||||||
in mkMerge [
|
in
|
||||||
|
mkMerge [
|
||||||
{
|
{
|
||||||
environment.systemPackages = optionals (enabledInstances != {} || enabledDatabases != {}) [ pkgs.restic ];
|
environment.systemPackages = optionals (enabledInstances != { } || enabledDatabases != { }) [
|
||||||
|
pkgs.restic
|
||||||
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
# Create repository if it is a local path.
|
# Create repository if it is a local path.
|
||||||
systemd.tmpfiles.rules =
|
systemd.tmpfiles.rules =
|
||||||
let
|
let
|
||||||
mkSettings = name: instance: optionals (hasPrefix "/" instance.settings.repository.path) [
|
mkSettings =
|
||||||
"d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -"
|
name: instance:
|
||||||
];
|
optionals (hasPrefix "/" instance.settings.repository.path) [
|
||||||
|
"d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -"
|
||||||
|
];
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases));
|
flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
services.restic.backups =
|
services.restic.backups =
|
||||||
|
|
@ -219,8 +287,8 @@ in
|
||||||
|
|
||||||
inherit (instance.settings.repository) timerConfig;
|
inherit (instance.settings.repository) timerConfig;
|
||||||
|
|
||||||
pruneOpts = mapAttrsToList (name: value:
|
pruneOpts = mapAttrsToList (
|
||||||
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
|
name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}"
|
||||||
) instance.settings.retention;
|
) instance.settings.retention;
|
||||||
|
|
||||||
backupPrepareCommand = concatStringsSep "\n" instance.request.hooks.beforeBackup;
|
backupPrepareCommand = concatStringsSep "\n" instance.request.hooks.beforeBackup;
|
||||||
|
|
@ -234,12 +302,13 @@ in
|
||||||
++ (optionals (instance.settings.limitDownloadKiBs != null) [
|
++ (optionals (instance.settings.limitDownloadKiBs != null) [
|
||||||
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
|
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
|
||||||
]);
|
]);
|
||||||
} // optionalAttrs (builtins.length instance.request.excludePatterns > 0) {
|
}
|
||||||
|
// optionalAttrs (builtins.length instance.request.excludePatterns > 0) {
|
||||||
exclude = instance.request.excludePatterns;
|
exclude = instance.request.excludePatterns;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
|
mkMerge (flatten (mapAttrsToList mkSettings enabledInstances));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
services.restic.backups =
|
services.restic.backups =
|
||||||
|
|
@ -258,8 +327,8 @@ in
|
||||||
|
|
||||||
inherit (instance.settings.repository) timerConfig;
|
inherit (instance.settings.repository) timerConfig;
|
||||||
|
|
||||||
pruneOpts = mapAttrsToList (name: value:
|
pruneOpts = mapAttrsToList (
|
||||||
"--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}"
|
name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}"
|
||||||
) instance.settings.retention;
|
) instance.settings.retention;
|
||||||
|
|
||||||
extraBackupArgs =
|
extraBackupArgs =
|
||||||
|
|
@ -269,120 +338,131 @@ in
|
||||||
++ (optionals (instance.settings.limitDownloadKiBs != null) [
|
++ (optionals (instance.settings.limitDownloadKiBs != null) [
|
||||||
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
|
"--limit-download=${toString instance.settings.limitDownloadKiBs}"
|
||||||
])
|
])
|
||||||
++
|
++ (
|
||||||
(let
|
let
|
||||||
cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd;
|
cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd;
|
||||||
in
|
in
|
||||||
[
|
[
|
||||||
"--stdin-filename ${instance.request.backupName} --stdin-from-command -- ${cmd}/bin/dump.sh"
|
"--stdin-filename ${instance.request.backupName} --stdin-from-command -- ${cmd}/bin/dump.sh"
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases));
|
mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
systemd.services =
|
systemd.services =
|
||||||
let
|
let
|
||||||
mkSettings = name: instance:
|
mkSettings =
|
||||||
|
name: instance:
|
||||||
let
|
let
|
||||||
serviceName = fullName name instance.settings.repository;
|
serviceName = fullName name instance.settings.repository;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
${serviceName} = mkMerge [
|
${serviceName} = mkMerge [
|
||||||
{
|
{
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Nice = cfg.performance.niceness;
|
Nice = cfg.performance.niceness;
|
||||||
IOSchedulingClass = cfg.performance.ioSchedulingClass;
|
IOSchedulingClass = cfg.performance.ioSchedulingClass;
|
||||||
IOSchedulingPriority = cfg.performance.ioPriority;
|
IOSchedulingPriority = cfg.performance.ioPriority;
|
||||||
# BindReadOnlyPaths = instance.sourceDirectories;
|
# BindReadOnlyPaths = instance.sourceDirectories;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
(optionalAttrs (instance.settings.repository.secrets != {})
|
(optionalAttrs (instance.settings.repository.secrets != { }) {
|
||||||
{
|
serviceConfig.EnvironmentFile = [
|
||||||
serviceConfig.EnvironmentFile = [
|
"/run/secrets_restic/${serviceName}"
|
||||||
"/run/secrets_restic/${serviceName}"
|
];
|
||||||
];
|
after = [ "${serviceName}-pre.service" ];
|
||||||
after = [ "${serviceName}-pre.service" ];
|
requires = [ "${serviceName}-pre.service" ];
|
||||||
requires = [ "${serviceName}-pre.service" ];
|
})
|
||||||
})
|
];
|
||||||
];
|
|
||||||
|
|
||||||
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != {})
|
"${serviceName}-pre" = mkIf (instance.settings.repository.secrets != { }) (
|
||||||
(let
|
let
|
||||||
script = lib.shb.genConfigOutOfBandSystemd {
|
script = lib.shb.genConfigOutOfBandSystemd {
|
||||||
config = instance.settings.repository.secrets;
|
config = instance.settings.repository.secrets;
|
||||||
configLocation = "/run/secrets_restic/${serviceName}";
|
configLocation = "/run/secrets_restic/${serviceName}";
|
||||||
generator = lib.shb.toEnvVar;
|
generator = lib.shb.toEnvVar;
|
||||||
user = instance.request.user;
|
user = instance.request.user;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
script = script.preStart;
|
script = script.preStart;
|
||||||
serviceConfig.Type = "oneshot";
|
serviceConfig.Type = "oneshot";
|
||||||
serviceConfig.LoadCredential = script.loadCredentials;
|
serviceConfig.LoadCredential = script.loadCredentials;
|
||||||
});
|
}
|
||||||
};
|
);
|
||||||
|
};
|
||||||
in
|
in
|
||||||
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
|
mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
systemd.services = let
|
systemd.services =
|
||||||
mkEnv = name: instance:
|
let
|
||||||
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
|
mkEnv =
|
||||||
enable = true;
|
name: instance:
|
||||||
wantedBy = [ "multi-user.target" ];
|
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
|
||||||
serviceConfig.Type = "oneshot";
|
enable = true;
|
||||||
script = (lib.shb.replaceSecrets {
|
wantedBy = [ "multi-user.target" ];
|
||||||
userConfig = instance.settings.repository.secrets // {
|
serviceConfig.Type = "oneshot";
|
||||||
RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path;
|
script = (
|
||||||
RESTIC_REPOSITORY = instance.settings.repository.path;
|
lib.shb.replaceSecrets {
|
||||||
};
|
userConfig = instance.settings.repository.secrets // {
|
||||||
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
|
RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path;
|
||||||
generator = lib.shb.toEnvVar;
|
RESTIC_REPOSITORY = instance.settings.repository.path;
|
||||||
user = instance.request.user;
|
};
|
||||||
});
|
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
|
||||||
};
|
generator = lib.shb.toEnvVar;
|
||||||
in
|
user = instance.request.user;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
in
|
||||||
listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
|
listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
environment.systemPackages = let
|
environment.systemPackages =
|
||||||
mkResticBinary = name: instance:
|
let
|
||||||
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
|
mkResticBinary =
|
||||||
set -euo pipefail
|
name: instance:
|
||||||
|
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
|
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
|
||||||
| xargs -d '\n')
|
| xargs -d '\n')
|
||||||
|
|
||||||
if ! [ "$1" = "restore" ]; then
|
if ! [ "$1" = "restore" ]; then
|
||||||
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
|
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
|
||||||
else
|
else
|
||||||
shift
|
shift
|
||||||
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic restore $@ --target /"
|
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic restore $@ --target /"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkResticBinary cfg.instances);
|
flatten (mapAttrsToList mkResticBinary cfg.instances);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
environment.systemPackages = let
|
environment.systemPackages =
|
||||||
mkResticBinary = name: instance:
|
let
|
||||||
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
|
mkResticBinary =
|
||||||
set -euo pipefail
|
name: instance:
|
||||||
|
pkgs.writeShellScriptBin (fullName name instance.settings.repository) ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
|
export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \
|
||||||
| xargs -d '\n')
|
| xargs -d '\n')
|
||||||
|
|
||||||
if ! [ "$1" = "restore" ]; then
|
if ! [ "$1" = "restore" ]; then
|
||||||
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
|
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
|
||||||
else
|
else
|
||||||
shift
|
shift
|
||||||
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupName} | ${instance.request.restoreCmd}"
|
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupName} | ${instance.request.restoreCmd}"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkResticBinary cfg.databases);
|
flatten (mapAttrsToList mkResticBinary cfg.databases);
|
||||||
}
|
}
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib) mapAttrs mkOption;
|
inherit (lib) mapAttrs mkOption;
|
||||||
inherit (lib.types) attrsOf anything submodule;
|
inherit (lib.types) attrsOf anything submodule;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
cfg = config.shb.sops;
|
cfg = config.shb.sops;
|
||||||
in
|
in
|
||||||
|
|
@ -11,35 +16,42 @@ in
|
||||||
options.shb.sops = {
|
options.shb.sops = {
|
||||||
secret = mkOption {
|
secret = mkOption {
|
||||||
description = "Secret following the [secret contract](./contracts-secret.html).";
|
description = "Secret following the [secret contract](./contracts-secret.html).";
|
||||||
default = {};
|
default = { };
|
||||||
type = attrsOf (submodule ({ name, options, ... }: {
|
type = attrsOf (
|
||||||
options = contracts.secret.mkProvider {
|
submodule (
|
||||||
settings = mkOption {
|
{ name, options, ... }:
|
||||||
description = ''
|
{
|
||||||
Settings specific to the Sops provider.
|
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`
|
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.
|
are managed by the [shb.sops.secret.<name>.request](#blocks-sops-options-shb.sops.secret._name_.request) option.
|
||||||
'';
|
'';
|
||||||
|
|
||||||
type = attrsOf anything;
|
type = attrsOf anything;
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
resultCfg = {
|
resultCfg = {
|
||||||
path = "/run/secrets/${name}";
|
path = "/run/secrets/${name}";
|
||||||
pathText = "/run/secrets/<name>";
|
pathText = "/run/secrets/<name>";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
sops.secrets = let
|
sops.secrets =
|
||||||
mkSecret = n: secretCfg: secretCfg.request // secretCfg.settings;
|
let
|
||||||
in mapAttrs mkSecret cfg.secret;
|
mkSecret = n: secretCfg: secretCfg.request // secretCfg.settings;
|
||||||
|
in
|
||||||
|
mapAttrs mkSecret cfg.secret;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,28 +1,41 @@
|
||||||
# Inspired from https://github.com/NixOS/nixpkgs/pull/231152 but made it so we can have multiple instances.
|
# Inspired from https://github.com/NixOS/nixpkgs/pull/231152 but made it so we can have multiple instances.
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
with lib;
|
with lib;
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.tinyproxy;
|
cfg = config.shb.tinyproxy;
|
||||||
|
|
||||||
mkValueStringTinyproxy = with lib; v:
|
mkValueStringTinyproxy =
|
||||||
if true == v then "yes"
|
with lib;
|
||||||
else if false == v then "no"
|
v:
|
||||||
else generators.mkValueStringDefault {} v;
|
if true == v then
|
||||||
|
"yes"
|
||||||
|
else if false == v then
|
||||||
|
"no"
|
||||||
|
else
|
||||||
|
generators.mkValueStringDefault { } v;
|
||||||
|
|
||||||
mkKeyValueTinyproxy = {
|
mkKeyValueTinyproxy =
|
||||||
mkValueString ? mkValueStringDefault {}
|
{
|
||||||
}: sep: k: v:
|
mkValueString ? mkValueStringDefault { },
|
||||||
if null == v then ""
|
}:
|
||||||
else "${lib.strings.escape [sep] k}${sep}${mkValueString v}";
|
sep: k: v:
|
||||||
|
if null == v then "" else "${lib.strings.escape [ sep ] k}${sep}${mkValueString v}";
|
||||||
|
|
||||||
settingsFormat = (pkgs.formats.keyValue {
|
settingsFormat = (
|
||||||
|
pkgs.formats.keyValue {
|
||||||
mkKeyValue = mkKeyValueTinyproxy {
|
mkKeyValue = mkKeyValueTinyproxy {
|
||||||
mkValueString = mkValueStringTinyproxy;
|
mkValueString = mkValueStringTinyproxy;
|
||||||
} " ";
|
} " ";
|
||||||
listsAsDuplicateKeys= true;
|
listsAsDuplicateKeys = true;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings;
|
configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings;
|
||||||
in
|
in
|
||||||
|
|
@ -33,115 +46,124 @@ in
|
||||||
options = {
|
options = {
|
||||||
enable = mkEnableOption "Tinyproxy daemon";
|
enable = mkEnableOption "Tinyproxy daemon";
|
||||||
|
|
||||||
package = mkPackageOption pkgs "tinyproxy" {};
|
package = mkPackageOption pkgs "tinyproxy" { };
|
||||||
|
|
||||||
dynamicBindFile = mkOption {
|
dynamicBindFile = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
File holding the IP to bind to.
|
File holding the IP to bind to.
|
||||||
'';
|
'';
|
||||||
default = "";
|
default = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
settings = mkOption {
|
settings = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Configuration for [tinyproxy](https://tinyproxy.github.io/).
|
Configuration for [tinyproxy](https://tinyproxy.github.io/).
|
||||||
'';
|
'';
|
||||||
default = { };
|
default = { };
|
||||||
example = literalExpression ''{
|
example = literalExpression ''
|
||||||
Port 8888;
|
{
|
||||||
Listen 127.0.0.1;
|
Port 8888;
|
||||||
Timeout 600;
|
Listen 127.0.0.1;
|
||||||
Allow 127.0.0.1;
|
Timeout 600;
|
||||||
Anonymous = ['"Host"' '"Authorization"'];
|
Allow 127.0.0.1;
|
||||||
ReversePath = '"/example/" "http://www.example.com/"';
|
Anonymous = ['"Host"' '"Authorization"'];
|
||||||
}'';
|
ReversePath = '"/example/" "http://www.example.com/"';
|
||||||
type = types.submodule ({name, ...}: {
|
}'';
|
||||||
freeformType = settingsFormat.type;
|
type = types.submodule (
|
||||||
options = {
|
{ name, ... }:
|
||||||
Listen = mkOption {
|
{
|
||||||
type = types.str;
|
freeformType = settingsFormat.type;
|
||||||
default = "127.0.0.1";
|
options = {
|
||||||
description = ''
|
Listen = mkOption {
|
||||||
Specify which address to listen to.
|
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
|
in
|
||||||
{
|
{
|
||||||
shb.tinyproxy = mkOption {
|
shb.tinyproxy = mkOption {
|
||||||
description = "Tinyproxy instances.";
|
description = "Tinyproxy instances.";
|
||||||
default = {};
|
default = { };
|
||||||
type = types.attrsOf instanceOption;
|
type = types.attrsOf instanceOption;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
systemd.services =
|
systemd.services =
|
||||||
let
|
let
|
||||||
instanceConfig = name: c: mkIf c.enable {
|
instanceConfig =
|
||||||
"tinyproxy-${name}" = {
|
name: c:
|
||||||
description = "TinyProxy daemon - instance ${name}";
|
mkIf c.enable {
|
||||||
after = [ "network.target" ];
|
"tinyproxy-${name}" = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
description = "TinyProxy daemon - instance ${name}";
|
||||||
serviceConfig = {
|
after = [ "network.target" ];
|
||||||
User = "tinyproxy";
|
wantedBy = [ "multi-user.target" ];
|
||||||
Group = "tinyproxy";
|
serviceConfig = {
|
||||||
Type = "simple";
|
User = "tinyproxy";
|
||||||
ExecStart = "${getExe c.package} -d -c /etc/tinyproxy/${name}.conf";
|
Group = "tinyproxy";
|
||||||
ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
|
Type = "simple";
|
||||||
KillSignal = "SIGINT";
|
ExecStart = "${getExe c.package} -d -c /etc/tinyproxy/${name}.conf";
|
||||||
TimeoutStopSec = "30s";
|
ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
|
||||||
Restart = "on-failure";
|
KillSignal = "SIGINT";
|
||||||
RestartSec = "1s";
|
TimeoutStopSec = "30s";
|
||||||
RestartSteps = "3";
|
Restart = "on-failure";
|
||||||
RestartMaxDelaySec = "10s";
|
RestartSec = "1s";
|
||||||
ConfigurationDirectory = "tinyproxy";
|
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
|
in
|
||||||
mkMerge (mapAttrsToList instanceConfig cfg);
|
mkMerge (mapAttrsToList instanceConfig cfg);
|
||||||
|
|
||||||
users.users.tinyproxy = {
|
users.users.tinyproxy = {
|
||||||
group = "tinyproxy";
|
group = "tinyproxy";
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
};
|
};
|
||||||
users.groups.tinyproxy = {};
|
users.groups.tinyproxy = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
meta.maintainers = with maintainers; [ tcheronneau ];
|
meta.maintainers = with maintainers; [ tcheronneau ];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.vpn;
|
cfg = config.shb.vpn;
|
||||||
|
|
@ -6,197 +11,212 @@ let
|
||||||
quoteEach = lib.concatMapStrings (x: ''"${x}"'');
|
quoteEach = lib.concatMapStrings (x: ''"${x}"'');
|
||||||
|
|
||||||
nordvpnConfig =
|
nordvpnConfig =
|
||||||
{ name
|
{
|
||||||
, dev
|
name,
|
||||||
, authFile
|
dev,
|
||||||
, remoteServerIP
|
authFile,
|
||||||
, dependentServices ? []
|
remoteServerIP,
|
||||||
}: ''
|
dependentServices ? [ ],
|
||||||
client
|
}:
|
||||||
dev ${dev}
|
''
|
||||||
proto tcp
|
client
|
||||||
remote ${remoteServerIP} 443
|
dev ${dev}
|
||||||
resolv-retry infinite
|
proto tcp
|
||||||
remote-random
|
remote ${remoteServerIP} 443
|
||||||
nobind
|
resolv-retry infinite
|
||||||
tun-mtu 1500
|
remote-random
|
||||||
tun-mtu-extra 32
|
nobind
|
||||||
mssfix 1450
|
tun-mtu 1500
|
||||||
persist-key
|
tun-mtu-extra 32
|
||||||
persist-tun
|
mssfix 1450
|
||||||
ping 15
|
persist-key
|
||||||
ping-restart 0
|
persist-tun
|
||||||
ping-timer-rem
|
ping 15
|
||||||
reneg-sec 0
|
ping-restart 0
|
||||||
comp-lzo no
|
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}
|
auth-user-pass ${authFile}
|
||||||
verb 3
|
verb 3
|
||||||
pull
|
pull
|
||||||
fast-io
|
fast-io
|
||||||
cipher AES-256-CBC
|
cipher AES-256-CBC
|
||||||
auth SHA512
|
auth SHA512
|
||||||
|
|
||||||
script-security 2
|
script-security 2
|
||||||
route-noexec
|
route-noexec
|
||||||
route-up ${routeUp name dependentServices}/bin/routeUp.sh
|
route-up ${routeUp name dependentServices}/bin/routeUp.sh
|
||||||
down ${routeDown name dependentServices}/bin/routeDown.sh
|
down ${routeDown name dependentServices}/bin/routeDown.sh
|
||||||
|
|
||||||
<ca>
|
<ca>
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIIFCjCCAvKgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA5MQswCQYDVQQGEwJQQTEQ
|
MIIFCjCCAvKgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA5MQswCQYDVQQGEwJQQTEQ
|
||||||
MA4GA1UEChMHTm9yZFZQTjEYMBYGA1UEAxMPTm9yZFZQTiBSb290IENBMB4XDTE2
|
MA4GA1UEChMHTm9yZFZQTjEYMBYGA1UEAxMPTm9yZFZQTiBSb290IENBMB4XDTE2
|
||||||
MDEwMTAwMDAwMFoXDTM1MTIzMTIzNTk1OVowOTELMAkGA1UEBhMCUEExEDAOBgNV
|
MDEwMTAwMDAwMFoXDTM1MTIzMTIzNTk1OVowOTELMAkGA1UEBhMCUEExEDAOBgNV
|
||||||
BAoTB05vcmRWUE4xGDAWBgNVBAMTD05vcmRWUE4gUm9vdCBDQTCCAiIwDQYJKoZI
|
BAoTB05vcmRWUE4xGDAWBgNVBAMTD05vcmRWUE4gUm9vdCBDQTCCAiIwDQYJKoZI
|
||||||
hvcNAQEBBQADggIPADCCAgoCggIBAMkr/BYhyo0F2upsIMXwC6QvkZps3NN2/eQF
|
hvcNAQEBBQADggIPADCCAgoCggIBAMkr/BYhyo0F2upsIMXwC6QvkZps3NN2/eQF
|
||||||
kfQIS1gql0aejsKsEnmY0Kaon8uZCTXPsRH1gQNgg5D2gixdd1mJUvV3dE3y9FJr
|
kfQIS1gql0aejsKsEnmY0Kaon8uZCTXPsRH1gQNgg5D2gixdd1mJUvV3dE3y9FJr
|
||||||
XMoDkXdCGBodvKJyU6lcfEVF6/UxHcbBguZK9UtRHS9eJYm3rpL/5huQMCppX7kU
|
XMoDkXdCGBodvKJyU6lcfEVF6/UxHcbBguZK9UtRHS9eJYm3rpL/5huQMCppX7kU
|
||||||
eQ8dpCwd3iKITqwd1ZudDqsWaU0vqzC2H55IyaZ/5/TnCk31Q1UP6BksbbuRcwOV
|
eQ8dpCwd3iKITqwd1ZudDqsWaU0vqzC2H55IyaZ/5/TnCk31Q1UP6BksbbuRcwOV
|
||||||
skEDsm6YoWDnn/IIzGOYnFJRzQH5jTz3j1QBvRIuQuBuvUkfhx1FEwhwZigrcxXu
|
skEDsm6YoWDnn/IIzGOYnFJRzQH5jTz3j1QBvRIuQuBuvUkfhx1FEwhwZigrcxXu
|
||||||
MP+QgM54kezgziJUaZcOM2zF3lvrwMvXDMfNeIoJABv9ljw969xQ8czQCU5lMVmA
|
MP+QgM54kezgziJUaZcOM2zF3lvrwMvXDMfNeIoJABv9ljw969xQ8czQCU5lMVmA
|
||||||
37ltv5Ec9U5hZuwk/9QO1Z+d/r6Jx0mlurS8gnCAKJgwa3kyZw6e4FZ8mYL4vpRR
|
37ltv5Ec9U5hZuwk/9QO1Z+d/r6Jx0mlurS8gnCAKJgwa3kyZw6e4FZ8mYL4vpRR
|
||||||
hPdvRTWCMJkeB4yBHyhxUmTRgJHm6YR3D6hcFAc9cQcTEl/I60tMdz33G6m0O42s
|
hPdvRTWCMJkeB4yBHyhxUmTRgJHm6YR3D6hcFAc9cQcTEl/I60tMdz33G6m0O42s
|
||||||
Qt/+AR3YCY/RusWVBJB/qNS94EtNtj8iaebCQW1jHAhvGmFILVR9lzD0EzWKHkvy
|
Qt/+AR3YCY/RusWVBJB/qNS94EtNtj8iaebCQW1jHAhvGmFILVR9lzD0EzWKHkvy
|
||||||
WEjmUVRgCDd6Ne3eFRNS73gdv/C3l5boYySeu4exkEYVxVRn8DhCxs0MnkMHWFK6
|
WEjmUVRgCDd6Ne3eFRNS73gdv/C3l5boYySeu4exkEYVxVRn8DhCxs0MnkMHWFK6
|
||||||
MyzXCCn+JnWFDYPfDKHvpff/kLDobtPBf+Lbch5wQy9quY27xaj0XwLyjOltpiST
|
MyzXCCn+JnWFDYPfDKHvpff/kLDobtPBf+Lbch5wQy9quY27xaj0XwLyjOltpiST
|
||||||
LWae/Q4vAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG
|
LWae/Q4vAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG
|
||||||
SIb3DQEBDQUAA4ICAQC9fUL2sZPxIN2mD32VeNySTgZlCEdVmlq471o/bDMP4B8g
|
SIb3DQEBDQUAA4ICAQC9fUL2sZPxIN2mD32VeNySTgZlCEdVmlq471o/bDMP4B8g
|
||||||
nQesFRtXY2ZCjs50Jm73B2LViL9qlREmI6vE5IC8IsRBJSV4ce1WYxyXro5rmVg/
|
nQesFRtXY2ZCjs50Jm73B2LViL9qlREmI6vE5IC8IsRBJSV4ce1WYxyXro5rmVg/
|
||||||
k6a10rlsbK/eg//GHoJxDdXDOokLUSnxt7gk3QKpX6eCdh67p0PuWm/7WUJQxH2S
|
k6a10rlsbK/eg//GHoJxDdXDOokLUSnxt7gk3QKpX6eCdh67p0PuWm/7WUJQxH2S
|
||||||
DxsT9vB/iZriTIEe/ILoOQF0Aqp7AgNCcLcLAmbxXQkXYCCSB35Vp06u+eTWjG0/
|
DxsT9vB/iZriTIEe/ILoOQF0Aqp7AgNCcLcLAmbxXQkXYCCSB35Vp06u+eTWjG0/
|
||||||
pyS5V14stGtw+fA0DJp5ZJV4eqJ5LqxMlYvEZ/qKTEdoCeaXv2QEmN6dVqjDoTAo
|
pyS5V14stGtw+fA0DJp5ZJV4eqJ5LqxMlYvEZ/qKTEdoCeaXv2QEmN6dVqjDoTAo
|
||||||
k0t5u4YRXzEVCfXAC3ocplNdtCA72wjFJcSbfif4BSC8bDACTXtnPC7nD0VndZLp
|
k0t5u4YRXzEVCfXAC3ocplNdtCA72wjFJcSbfif4BSC8bDACTXtnPC7nD0VndZLp
|
||||||
+RiNLeiENhk0oTC+UVdSc+n2nJOzkCK0vYu0Ads4JGIB7g8IB3z2t9ICmsWrgnhd
|
+RiNLeiENhk0oTC+UVdSc+n2nJOzkCK0vYu0Ads4JGIB7g8IB3z2t9ICmsWrgnhd
|
||||||
NdcOe15BincrGA8avQ1cWXsfIKEjbrnEuEk9b5jel6NfHtPKoHc9mDpRdNPISeVa
|
NdcOe15BincrGA8avQ1cWXsfIKEjbrnEuEk9b5jel6NfHtPKoHc9mDpRdNPISeVa
|
||||||
wDBM1mJChneHt59Nh8Gah74+TM1jBsw4fhJPvoc7Atcg740JErb904mZfkIEmojC
|
wDBM1mJChneHt59Nh8Gah74+TM1jBsw4fhJPvoc7Atcg740JErb904mZfkIEmojC
|
||||||
VPhBHVQ9LHBAdM8qFI2kRK0IynOmAZhexlP/aT/kpEsEPyaZQlnBn3An1CRz8h0S
|
VPhBHVQ9LHBAdM8qFI2kRK0IynOmAZhexlP/aT/kpEsEPyaZQlnBn3An1CRz8h0S
|
||||||
PApL8PytggYKeQmRhl499+6jLxcZ2IegLfqq41dzIjwHwTMplg+1pKIOVojpWA==
|
PApL8PytggYKeQmRhl499+6jLxcZ2IegLfqq41dzIjwHwTMplg+1pKIOVojpWA==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
</ca>
|
</ca>
|
||||||
key-direction 1
|
key-direction 1
|
||||||
<tls-auth>
|
<tls-auth>
|
||||||
#
|
#
|
||||||
# 2048 bit OpenVPN static key
|
# 2048 bit OpenVPN static key
|
||||||
#
|
#
|
||||||
-----BEGIN OpenVPN Static key V1-----
|
-----BEGIN OpenVPN Static key V1-----
|
||||||
e685bdaf659a25a200e2b9e39e51ff03
|
e685bdaf659a25a200e2b9e39e51ff03
|
||||||
0fc72cf1ce07232bd8b2be5e6c670143
|
0fc72cf1ce07232bd8b2be5e6c670143
|
||||||
f51e937e670eee09d4f2ea5a6e4e6996
|
f51e937e670eee09d4f2ea5a6e4e6996
|
||||||
5db852c275351b86fc4ca892d78ae002
|
5db852c275351b86fc4ca892d78ae002
|
||||||
d6f70d029bd79c4d1c26cf14e9588033
|
d6f70d029bd79c4d1c26cf14e9588033
|
||||||
cf639f8a74809f29f72b9d58f9b8f5fe
|
cf639f8a74809f29f72b9d58f9b8f5fe
|
||||||
fc7938eade40e9fed6cb92184abb2cc1
|
fc7938eade40e9fed6cb92184abb2cc1
|
||||||
0eb1a296df243b251df0643d53724cdb
|
0eb1a296df243b251df0643d53724cdb
|
||||||
5a92a1d6cb817804c4a9319b57d53be5
|
5a92a1d6cb817804c4a9319b57d53be5
|
||||||
80815bcfcb2df55018cc83fc43bc7ff8
|
80815bcfcb2df55018cc83fc43bc7ff8
|
||||||
2d51f9b88364776ee9d12fc85cc7ea5b
|
2d51f9b88364776ee9d12fc85cc7ea5b
|
||||||
9741c4f598c485316db066d52db4540e
|
9741c4f598c485316db066d52db4540e
|
||||||
212e1518a9bd4828219e24b20d88f598
|
212e1518a9bd4828219e24b20d88f598
|
||||||
a196c9de96012090e333519ae18d3509
|
a196c9de96012090e333519ae18d3509
|
||||||
9427e7b372d348d352dc4c85e18cd4b9
|
9427e7b372d348d352dc4c85e18cd4b9
|
||||||
3f8a56ddb2e64eb67adfc9b337157ff4
|
3f8a56ddb2e64eb67adfc9b337157ff4
|
||||||
-----END OpenVPN Static key V1-----
|
-----END OpenVPN Static key V1-----
|
||||||
</tls-auth>
|
</tls-auth>
|
||||||
'';
|
'';
|
||||||
|
|
||||||
routeUp = name: dependentServices: pkgs.writeShellApplication {
|
routeUp =
|
||||||
name = "routeUp.sh";
|
name: dependentServices:
|
||||||
|
pkgs.writeShellApplication {
|
||||||
|
name = "routeUp.sh";
|
||||||
|
|
||||||
runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools ];
|
runtimeInputs = [
|
||||||
|
pkgs.iproute2
|
||||||
|
pkgs.systemd
|
||||||
|
pkgs.nettools
|
||||||
|
];
|
||||||
|
|
||||||
text = ''
|
text = ''
|
||||||
echo "Running route-up..."
|
echo "Running route-up..."
|
||||||
|
|
||||||
echo "dev=''${dev:?}"
|
echo "dev=''${dev:?}"
|
||||||
echo "ifconfig_local=''${ifconfig_local:?}"
|
echo "ifconfig_local=''${ifconfig_local:?}"
|
||||||
echo "route_vpn_gateway=''${route_vpn_gateway:?}"
|
echo "route_vpn_gateway=''${route_vpn_gateway:?}"
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
ip rule
|
ip rule
|
||||||
ip rule add from "''${ifconfig_local:?}/32" table ${name}
|
ip rule add from "''${ifconfig_local:?}/32" table ${name}
|
||||||
ip rule add to "''${route_vpn_gateway:?}/32" table ${name}
|
ip rule add to "''${route_vpn_gateway:?}/32" table ${name}
|
||||||
ip rule
|
ip rule
|
||||||
|
|
||||||
ip route list table ${name} || :
|
ip route list table ${name} || :
|
||||||
retVal=$?
|
retVal=$?
|
||||||
if [ $retVal -eq 2 ]; then
|
if [ $retVal -eq 2 ]; then
|
||||||
echo "table is empty"
|
echo "table is empty"
|
||||||
elif [ $retVal -ne 0 ]; then
|
elif [ $retVal -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
ip route add default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name}
|
ip route add default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name}
|
||||||
ip route flush cache
|
ip route flush cache
|
||||||
ip route list table ${name} || :
|
ip route list table ${name} || :
|
||||||
retVal=$?
|
retVal=$?
|
||||||
if [ $retVal -eq 2 ]; then
|
if [ $retVal -eq 2 ]; then
|
||||||
echo "table is empty"
|
echo "table is empty"
|
||||||
elif [ $retVal -ne 0 ]; then
|
elif [ $retVal -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "''${ifconfig_local:?}" > /run/openvpn/${name}/ifconfig_local
|
echo "''${ifconfig_local:?}" > /run/openvpn/${name}/ifconfig_local
|
||||||
|
|
||||||
dependencies=(${quoteEach dependentServices})
|
dependencies=(${quoteEach dependentServices})
|
||||||
for i in "''${dependencies[@]}"; do
|
for i in "''${dependencies[@]}"; do
|
||||||
systemctl restart "$i" || :
|
systemctl restart "$i" || :
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Running route-up DONE"
|
echo "Running route-up DONE"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
routeDown = name: dependentServices: pkgs.writeShellApplication {
|
routeDown =
|
||||||
name = "routeDown.sh";
|
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 = ''
|
text = ''
|
||||||
echo "Running route-down..."
|
echo "Running route-down..."
|
||||||
|
|
||||||
echo "dev=''${dev:?}"
|
echo "dev=''${dev:?}"
|
||||||
echo "ifconfig_local=''${ifconfig_local:?}"
|
echo "ifconfig_local=''${ifconfig_local:?}"
|
||||||
echo "route_vpn_gateway=''${route_vpn_gateway:?}"
|
echo "route_vpn_gateway=''${route_vpn_gateway:?}"
|
||||||
|
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
ip rule
|
ip rule
|
||||||
ip rule del from "''${ifconfig_local:?}/32" table ${name}
|
ip rule del from "''${ifconfig_local:?}/32" table ${name}
|
||||||
ip rule del to "''${route_vpn_gateway:?}/32" table ${name}
|
ip rule del to "''${route_vpn_gateway:?}/32" table ${name}
|
||||||
ip rule
|
ip rule
|
||||||
|
|
||||||
# This will probably fail because the dev is already gone.
|
# This will probably fail because the dev is already gone.
|
||||||
ip route list table ${name} || :
|
ip route list table ${name} || :
|
||||||
retVal=$?
|
retVal=$?
|
||||||
if [ $retVal -eq 2 ]; then
|
if [ $retVal -eq 2 ]; then
|
||||||
echo "table is empty"
|
echo "table is empty"
|
||||||
elif [ $retVal -ne 0 ]; then
|
elif [ $retVal -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
ip route del default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} || :
|
ip route del default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} || :
|
||||||
ip route flush cache
|
ip route flush cache
|
||||||
ip route list table ${name} || :
|
ip route list table ${name} || :
|
||||||
retVal=$?
|
retVal=$?
|
||||||
if [ $retVal -eq 2 ]; then
|
if [ $retVal -eq 2 ]; then
|
||||||
echo "table is empty"
|
echo "table is empty"
|
||||||
elif [ $retVal -ne 0 ]; then
|
elif [ $retVal -ne 0 ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm /run/openvpn/${name}/ifconfig_local
|
rm /run/openvpn/${name}/ifconfig_local
|
||||||
|
|
||||||
dependencies=(${quoteEach dependentServices})
|
dependencies=(${quoteEach dependentServices})
|
||||||
for i in "''${dependencies[@]}"; do
|
for i in "''${dependencies[@]}"; do
|
||||||
systemctl stop "$i" || :
|
systemctl stop "$i" || :
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Running route-down DONE"
|
echo "Running route-down DONE"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options =
|
options =
|
||||||
|
|
@ -205,7 +225,7 @@ in
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "OpenVPN config";
|
enable = lib.mkEnableOption "OpenVPN config";
|
||||||
|
|
||||||
package = lib.mkPackageOption pkgs "openvpn" {};
|
package = lib.mkPackageOption pkgs "openvpn" { };
|
||||||
|
|
||||||
provider = lib.mkOption {
|
provider = lib.mkOption {
|
||||||
description = "VPN provider, if given uses ready-made configuration.";
|
description = "VPN provider, if given uses ready-made configuration.";
|
||||||
|
|
@ -243,68 +263,76 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
shb.vpn = lib.mkOption {
|
shb.vpn = lib.mkOption {
|
||||||
description = "OpenVPN instances.";
|
description = "OpenVPN instances.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.attrsOf instanceOption;
|
type = lib.types.attrsOf instanceOption;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
services.openvpn.servers =
|
services.openvpn.servers =
|
||||||
let
|
let
|
||||||
instanceConfig = name: c: lib.mkIf c.enable {
|
instanceConfig =
|
||||||
${name} = {
|
name: c:
|
||||||
autoStart = true;
|
lib.mkIf c.enable {
|
||||||
|
${name} = {
|
||||||
|
autoStart = true;
|
||||||
|
|
||||||
up = "mkdir -p /run/openvpn/${name}";
|
up = "mkdir -p /run/openvpn/${name}";
|
||||||
|
|
||||||
config = nordvpnConfig {
|
config = nordvpnConfig {
|
||||||
inherit name;
|
inherit name;
|
||||||
inherit (c) dev remoteServerIP authFile;
|
inherit (c) dev remoteServerIP authFile;
|
||||||
dependentServices = lib.optional (c.proxyPort != null) "tinyproxy-${name}.service";
|
dependentServices = lib.optional (c.proxyPort != null) "tinyproxy-${name}.service";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
in
|
in
|
||||||
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
|
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
|
||||||
|
|
||||||
systemd.tmpfiles.rules = map (name:
|
systemd.tmpfiles.rules = map (name: "d /tmp/openvpn/${name}.status 0700 root root") (
|
||||||
"d /tmp/openvpn/${name}.status 0700 root root"
|
lib.attrNames cfg
|
||||||
) (lib.attrNames cfg);
|
);
|
||||||
|
|
||||||
networking.iproute2.enable = true;
|
networking.iproute2.enable = true;
|
||||||
networking.iproute2.rttablesExtraConfig =
|
networking.iproute2.rttablesExtraConfig = lib.concatStringsSep "\n" (
|
||||||
lib.concatStringsSep "\n" (lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg);
|
lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg
|
||||||
|
);
|
||||||
|
|
||||||
shb.tinyproxy =
|
shb.tinyproxy =
|
||||||
let
|
let
|
||||||
instanceConfig = name: c: lib.mkIf (c.enable && c.proxyPort != null) {
|
instanceConfig =
|
||||||
${name} = {
|
name: c:
|
||||||
enable = true;
|
lib.mkIf (c.enable && c.proxyPort != null) {
|
||||||
# package = pkgs.tinyproxy.overrideAttrs (old: {
|
${name} = {
|
||||||
# withDebug = false;
|
enable = true;
|
||||||
# patches = old.patches ++ [
|
# package = pkgs.tinyproxy.overrideAttrs (old: {
|
||||||
# (pkgs.fetchpatch {
|
# withDebug = false;
|
||||||
# name = "";
|
# patches = old.patches ++ [
|
||||||
# url = "https://github.com/tinyproxy/tinyproxy/pull/494/commits/2532ba09896352b31f3538d7819daa1fc3f829f1.patch";
|
# (pkgs.fetchpatch {
|
||||||
# sha256 = "sha256-Q0MkHnttW8tH3+hoCt9ACjHjmmZQgF6pC/menIrU0Co=";
|
# 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;
|
dynamicBindFile = "/run/openvpn/${name}/ifconfig_local";
|
||||||
Listen = "127.0.0.1";
|
settings = {
|
||||||
Syslog = "On";
|
Port = c.proxyPort;
|
||||||
LogLevel = "Info";
|
Listen = "127.0.0.1";
|
||||||
Allow = [ "127.0.0.1" "::1" ];
|
Syslog = "On";
|
||||||
ViaProxyName = ''"tinyproxy"'';
|
LogLevel = "Info";
|
||||||
|
Allow = [
|
||||||
|
"127.0.0.1"
|
||||||
|
"::1"
|
||||||
|
];
|
||||||
|
ViaProxyName = ''"tinyproxy"'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
in
|
in
|
||||||
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
|
lib.mkMerge (lib.mapAttrsToList instanceConfig cfg);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.zfs;
|
cfg = config.shb.zfs;
|
||||||
|
|
@ -21,38 +26,43 @@ in
|
||||||
This block implements the following contracts:
|
This block implements the following contracts:
|
||||||
- mount
|
- mount
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
shb.zfs."safe/postgresql".path = "/var/lib/postgresql";
|
shb.zfs."safe/postgresql".path = "/var/lib/postgresql";
|
||||||
'';
|
'';
|
||||||
type = lib.types.attrsOf (lib.types.submodule {
|
type = lib.types.attrsOf (
|
||||||
options = {
|
lib.types.submodule {
|
||||||
enable = lib.mkEnableOption "shb.zfs.datasets";
|
options = {
|
||||||
|
enable = lib.mkEnableOption "shb.zfs.datasets";
|
||||||
|
|
||||||
poolName = lib.mkOption {
|
poolName = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
description = "ZFS pool name this dataset should be created on. Overrides the defaultPoolName.";
|
description = "ZFS pool name this dataset should be created on. Overrides the defaultPoolName.";
|
||||||
};
|
};
|
||||||
|
|
||||||
path = lib.mkOption {
|
path = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Path this dataset should be mounted on.";
|
description = "Path this dataset should be mounted on.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = lib.any (x: x.poolName == null) (lib.mapAttrsToList (n: v: v) cfg.datasets) -> cfg.defaultPoolName != null;
|
assertion =
|
||||||
|
lib.any (x: x.poolName == null) (lib.mapAttrsToList (n: v: v) cfg.datasets)
|
||||||
|
-> cfg.defaultPoolName != null;
|
||||||
message = "Cannot have both datasets.poolName and defaultPoolName set to null";
|
message = "Cannot have both datasets.poolName and defaultPoolName set to null";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
system.activationScripts = lib.mapAttrs' (name: cfg':
|
system.activationScripts = lib.mapAttrs' (
|
||||||
|
name: cfg':
|
||||||
let
|
let
|
||||||
dataset = (if cfg'.poolName != null then cfg'.poolName else cfg.defaultPoolName) + "/" + name;
|
dataset = (if cfg'.poolName != null then cfg'.poolName else cfg.defaultPoolName) + "/" + name;
|
||||||
in
|
in
|
||||||
|
|
@ -68,6 +78,7 @@ in
|
||||||
mountpoint=${cfg'.path} \
|
mountpoint=${cfg'.path} \
|
||||||
${dataset}
|
${dataset}
|
||||||
'';
|
'';
|
||||||
}) cfg.datasets;
|
}
|
||||||
|
) cfg.datasets;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,35 @@
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString;
|
inherit (lib)
|
||||||
inherit (lib.types) listOf nonEmptyListOf submodule str;
|
concatStringsSep
|
||||||
|
literalMD
|
||||||
|
mkOption
|
||||||
|
optionalAttrs
|
||||||
|
optionalString
|
||||||
|
;
|
||||||
|
inherit (lib.types)
|
||||||
|
listOf
|
||||||
|
nonEmptyListOf
|
||||||
|
submodule
|
||||||
|
str
|
||||||
|
;
|
||||||
inherit (lib.shb) anyNotNull;
|
inherit (lib.shb) anyNotNull;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
mkRequest =
|
mkRequest =
|
||||||
{ user ? "",
|
{
|
||||||
|
user ? "",
|
||||||
userText ? null,
|
userText ? null,
|
||||||
sourceDirectories ? [ "/var/lib/example" ],
|
sourceDirectories ? [ "/var/lib/example" ],
|
||||||
sourceDirectoriesText ? null,
|
sourceDirectoriesText ? null,
|
||||||
excludePatterns ? [],
|
excludePatterns ? [ ],
|
||||||
excludePatternsText ? null,
|
excludePatternsText ? null,
|
||||||
beforeBackup ? [],
|
beforeBackup ? [ ],
|
||||||
beforeBackupText ? null,
|
beforeBackupText ? null,
|
||||||
afterBackup ? [],
|
afterBackup ? [ ],
|
||||||
afterBackupText ? null,
|
afterBackupText ? null,
|
||||||
}: mkOption {
|
}:
|
||||||
|
mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Request part of the backup contract.
|
Request part of the backup contract.
|
||||||
|
|
||||||
|
|
@ -31,72 +44,102 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultText = optionalString (anyNotNull [
|
defaultText =
|
||||||
userText
|
optionalString
|
||||||
sourceDirectoriesText
|
(anyNotNull [
|
||||||
excludePatternsText
|
userText
|
||||||
beforeBackupText
|
sourceDirectoriesText
|
||||||
afterBackupText
|
excludePatternsText
|
||||||
]) (literalMD ''
|
beforeBackupText
|
||||||
{
|
afterBackupText
|
||||||
user = ${if userText != null then userText else user};
|
])
|
||||||
sourceDirectories = ${if sourceDirectoriesText != null then sourceDirectoriesText else "[ " + concatStringsSep " " sourceDirectories + " ]"};
|
(literalMD ''
|
||||||
excludePatterns = ${if excludePatternsText != null then excludePatternsText else "[ " + concatStringsSep " " excludePatterns + " ]"};
|
{
|
||||||
hooks.beforeBackup = ${if beforeBackupText != null then beforeBackupText else "[ " + concatStringsSep " " beforeBackup + " ]"};
|
user = ${if userText != null then userText else user};
|
||||||
hooks.afterBackup = ${if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]"};
|
sourceDirectories = ${
|
||||||
};
|
if sourceDirectoriesText != null then
|
||||||
'');
|
sourceDirectoriesText
|
||||||
|
else
|
||||||
|
"[ " + concatStringsSep " " sourceDirectories + " ]"
|
||||||
|
};
|
||||||
|
excludePatterns = ${
|
||||||
|
if excludePatternsText != null then
|
||||||
|
excludePatternsText
|
||||||
|
else
|
||||||
|
"[ " + concatStringsSep " " excludePatterns + " ]"
|
||||||
|
};
|
||||||
|
hooks.beforeBackup = ${
|
||||||
|
if beforeBackupText != null then
|
||||||
|
beforeBackupText
|
||||||
|
else
|
||||||
|
"[ " + concatStringsSep " " beforeBackup + " ]"
|
||||||
|
};
|
||||||
|
hooks.afterBackup = ${
|
||||||
|
if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
'');
|
||||||
|
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
user = mkOption {
|
user =
|
||||||
description = ''
|
mkOption {
|
||||||
Unix user doing the backups.
|
description = ''
|
||||||
'';
|
Unix user doing the backups.
|
||||||
type = str;
|
'';
|
||||||
example = "vaultwarden";
|
type = str;
|
||||||
default = user;
|
example = "vaultwarden";
|
||||||
} // optionalAttrs (userText != null) {
|
default = user;
|
||||||
defaultText = literalMD userText;
|
}
|
||||||
};
|
// optionalAttrs (userText != null) {
|
||||||
|
defaultText = literalMD userText;
|
||||||
|
};
|
||||||
|
|
||||||
sourceDirectories = mkOption {
|
sourceDirectories =
|
||||||
description = "Directories to backup.";
|
mkOption {
|
||||||
type = nonEmptyListOf str;
|
description = "Directories to backup.";
|
||||||
example = "/var/lib/vaultwarden";
|
type = nonEmptyListOf str;
|
||||||
default = sourceDirectories;
|
example = "/var/lib/vaultwarden";
|
||||||
} // optionalAttrs (sourceDirectoriesText != null) {
|
default = sourceDirectories;
|
||||||
defaultText = literalMD sourceDirectoriesText;
|
}
|
||||||
};
|
// optionalAttrs (sourceDirectoriesText != null) {
|
||||||
|
defaultText = literalMD sourceDirectoriesText;
|
||||||
|
};
|
||||||
|
|
||||||
excludePatterns = mkOption {
|
excludePatterns =
|
||||||
description = "File patterns to exclude.";
|
mkOption {
|
||||||
type = listOf str;
|
description = "File patterns to exclude.";
|
||||||
default = excludePatterns;
|
type = listOf str;
|
||||||
} // optionalAttrs (excludePatternsText != null) {
|
default = excludePatterns;
|
||||||
defaultText = literalMD excludePatternsText;
|
}
|
||||||
};
|
// optionalAttrs (excludePatternsText != null) {
|
||||||
|
defaultText = literalMD excludePatternsText;
|
||||||
|
};
|
||||||
|
|
||||||
hooks = mkOption {
|
hooks = mkOption {
|
||||||
description = "Hooks to run around the backup.";
|
description = "Hooks to run around the backup.";
|
||||||
default = {};
|
default = { };
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
beforeBackup = mkOption {
|
beforeBackup =
|
||||||
description = "Hooks to run before backup.";
|
mkOption {
|
||||||
type = listOf str;
|
description = "Hooks to run before backup.";
|
||||||
default = beforeBackup;
|
type = listOf str;
|
||||||
} // optionalAttrs (beforeBackupText != null) {
|
default = beforeBackup;
|
||||||
defaultText = literalMD beforeBackupText;
|
}
|
||||||
};
|
// optionalAttrs (beforeBackupText != null) {
|
||||||
|
defaultText = literalMD beforeBackupText;
|
||||||
|
};
|
||||||
|
|
||||||
afterBackup = mkOption {
|
afterBackup =
|
||||||
description = "Hooks to run after backup.";
|
mkOption {
|
||||||
type = listOf str;
|
description = "Hooks to run after backup.";
|
||||||
default = afterBackup;
|
type = listOf str;
|
||||||
} // optionalAttrs (afterBackupText != null) {
|
default = afterBackup;
|
||||||
defaultText = literalMD afterBackupText;
|
}
|
||||||
};
|
// optionalAttrs (afterBackupText != null) {
|
||||||
|
defaultText = literalMD afterBackupText;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -104,70 +147,79 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
mkResult = {
|
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 ''
|
|
||||||
{
|
{
|
||||||
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
|
restoreScript ? "restore",
|
||||||
backupService = ${if backupServiceText != null then backupServiceText else backupService};
|
restoreScriptText ? null,
|
||||||
}
|
backupService ? "backup.service",
|
||||||
'');
|
backupServiceText ? null,
|
||||||
|
}:
|
||||||
|
mkOption {
|
||||||
|
description = ''
|
||||||
|
Result part of the backup contract.
|
||||||
|
|
||||||
type = submodule {
|
Options set by the provider module that indicates the name of the backup and restor scripts.
|
||||||
options = {
|
'';
|
||||||
restoreScript = mkOption {
|
default = {
|
||||||
description = ''
|
inherit restoreScript backupService;
|
||||||
Name of script that can restore the database.
|
};
|
||||||
One can then list snapshots with:
|
|
||||||
|
|
||||||
```bash
|
defaultText =
|
||||||
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
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
|
```bash
|
||||||
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
|
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
||||||
```
|
```
|
||||||
'';
|
|
||||||
type = str;
|
|
||||||
default = restoreScript;
|
|
||||||
} // optionalAttrs (restoreScriptText != null) {
|
|
||||||
defaultText = literalMD restoreScriptText;
|
|
||||||
};
|
|
||||||
|
|
||||||
backupService = mkOption {
|
And restore the database with:
|
||||||
description = ''
|
|
||||||
Name of service backing up the database.
|
|
||||||
|
|
||||||
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
|
backupService =
|
||||||
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
|
mkOption {
|
||||||
```
|
description = ''
|
||||||
'';
|
Name of service backing up the database.
|
||||||
type = str;
|
|
||||||
default = backupService;
|
This script can be ran manually to backup the database:
|
||||||
} // optionalAttrs (backupServiceText != null) {
|
|
||||||
defaultText = literalMD backupServiceText;
|
```bash
|
||||||
|
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
type = str;
|
||||||
|
default = backupService;
|
||||||
|
}
|
||||||
|
// optionalAttrs (backupServiceText != null) {
|
||||||
|
defaultText = literalMD backupServiceText;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{ pkgs, lib, ... }:
|
{ pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
contracts = pkgs.callPackage ../. {};
|
contracts = pkgs.callPackage ../. { };
|
||||||
|
|
||||||
inherit (lib) mkOption;
|
inherit (lib) mkOption;
|
||||||
inherit (lib.types) submodule;
|
inherit (lib.types) submodule;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
{ pkgs, lib }:
|
{ pkgs, lib }:
|
||||||
let
|
let
|
||||||
inherit (lib) concatMapStringsSep getAttrFromPath mkIf optionalAttrs setAttrByPath;
|
inherit (lib)
|
||||||
|
concatMapStringsSep
|
||||||
|
getAttrFromPath
|
||||||
|
mkIf
|
||||||
|
optionalAttrs
|
||||||
|
setAttrByPath
|
||||||
|
;
|
||||||
in
|
in
|
||||||
{ name,
|
{
|
||||||
|
name,
|
||||||
providerRoot,
|
providerRoot,
|
||||||
modules ? [],
|
modules ? [ ],
|
||||||
username ? "me",
|
username ? "me",
|
||||||
sourceDirectories ? [
|
sourceDirectories ? [
|
||||||
"/opt/files/A"
|
"/opt/files/A"
|
||||||
|
|
@ -12,107 +19,115 @@ in
|
||||||
],
|
],
|
||||||
settings, # { repository, config } -> attrset
|
settings, # { repository, config } -> attrset
|
||||||
extraConfig ? null, # { username, config } -> attrset
|
extraConfig ? null, # { username, config } -> attrset
|
||||||
}: lib.shb.runNixOSTest {
|
}:
|
||||||
|
lib.shb.runNixOSTest {
|
||||||
inherit name;
|
inherit name;
|
||||||
|
|
||||||
nodes.machine = { config, ... }: {
|
nodes.machine =
|
||||||
imports = [ lib.shb.baseImports ] ++ modules;
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
imports = [ lib.shb.baseImports ] ++ modules;
|
||||||
|
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
(setAttrByPath providerRoot {
|
(setAttrByPath providerRoot {
|
||||||
request = {
|
request = {
|
||||||
inherit sourceDirectories;
|
inherit sourceDirectories;
|
||||||
user = username;
|
user = username;
|
||||||
};
|
};
|
||||||
settings = settings {
|
settings = settings {
|
||||||
inherit config;
|
inherit config;
|
||||||
repository = "/opt/repos/${name}";
|
repository = "/opt/repos/${name}";
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
(mkIf (username != "root") {
|
(mkIf (username != "root") {
|
||||||
users.users.${username} = {
|
users.users.${username} = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
extraGroups = [ "sudoers" ];
|
extraGroups = [ "sudoers" ];
|
||||||
group = "root";
|
group = "root";
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
(optionalAttrs (extraConfig != null) (extraConfig { inherit username config; }))
|
(optionalAttrs (extraConfig != null) (extraConfig {
|
||||||
];
|
inherit username config;
|
||||||
};
|
}))
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
extraPythonPackages = p: [ p.dictdiffer ];
|
extraPythonPackages = p: [ p.dictdiffer ];
|
||||||
skipTypeCheck = true;
|
skipTypeCheck = true;
|
||||||
|
|
||||||
testScript = { nodes, ... }: let
|
testScript =
|
||||||
provider = (getAttrFromPath providerRoot nodes.machine).result;
|
{ nodes, ... }:
|
||||||
in ''
|
let
|
||||||
from dictdiffer import diff
|
provider = (getAttrFromPath providerRoot nodes.machine).result;
|
||||||
|
in
|
||||||
|
''
|
||||||
|
from dictdiffer import diff
|
||||||
|
|
||||||
username = "${username}"
|
username = "${username}"
|
||||||
sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ]
|
sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ]
|
||||||
|
|
||||||
def list_files(dir):
|
def list_files(dir):
|
||||||
files_and_content = {}
|
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:
|
for f in files:
|
||||||
content = machine.succeed(f"""cat {f}""").strip()
|
content = machine.succeed(f"""cat {f}""").strip()
|
||||||
files_and_content[f] = content
|
files_and_content[f] = content
|
||||||
|
|
||||||
return files_and_content
|
return files_and_content
|
||||||
|
|
||||||
def assert_files(dir, files):
|
def assert_files(dir, files):
|
||||||
result = list(diff(list_files(dir), files))
|
result = list(diff(list_files(dir), files))
|
||||||
if len(result) > 0:
|
if len(result) > 0:
|
||||||
raise Exception("Unexpected files:", result)
|
raise Exception("Unexpected files:", result)
|
||||||
|
|
||||||
with subtest("Create initial content"):
|
with subtest("Create initial content"):
|
||||||
for path in sourceDirectories:
|
for path in sourceDirectories:
|
||||||
machine.succeed(f"""
|
machine.succeed(f"""
|
||||||
mkdir -p {path}
|
mkdir -p {path}
|
||||||
echo repo_fileA_1 > {path}/fileA
|
echo repo_fileA_1 > {path}/fileA
|
||||||
echo repo_fileB_1 > {path}/fileB
|
echo repo_fileB_1 > {path}/fileB
|
||||||
|
|
||||||
chown {username}: -R {path}
|
chown {username}: -R {path}
|
||||||
chmod go-rwx -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
|
|
||||||
""")
|
""")
|
||||||
|
|
||||||
assert_files(path, {
|
for path in sourceDirectories:
|
||||||
f'{path}/fileA': 'repo_fileA_2',
|
assert_files(path, {
|
||||||
f'{path}/fileB': 'repo_fileB_2',
|
f'{path}/fileA': 'repo_fileA_1',
|
||||||
})
|
f'{path}/fileB': 'repo_fileB_1',
|
||||||
|
})
|
||||||
|
|
||||||
with subtest("Delete content"):
|
with subtest("First backup in repo"):
|
||||||
for path in sourceDirectories:
|
print(machine.succeed("systemctl cat ${provider.backupService}"))
|
||||||
machine.succeed(f"""rm -r {path}/*""")
|
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"):
|
assert_files(path, {
|
||||||
machine.succeed("""${provider.restoreScript} restore latest""")
|
f'{path}/fileA': 'repo_fileA_2',
|
||||||
|
f'{path}/fileB': 'repo_fileB_2',
|
||||||
|
})
|
||||||
|
|
||||||
for path in sourceDirectories:
|
with subtest("Delete content"):
|
||||||
assert_files(path, {
|
for path in sourceDirectories:
|
||||||
f'{path}/fileA': 'repo_fileA_1',
|
machine.succeed(f"""rm -r {path}/*""")
|
||||||
f'{path}/fileB': 'repo_fileB_1',
|
|
||||||
})
|
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',
|
||||||
|
})
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib) mkOption literalExpression literalMD optionalAttrs optionalString;
|
inherit (lib)
|
||||||
|
mkOption
|
||||||
|
literalExpression
|
||||||
|
literalMD
|
||||||
|
optionalAttrs
|
||||||
|
optionalString
|
||||||
|
;
|
||||||
inherit (lib.types) submodule str;
|
inherit (lib.types) submodule str;
|
||||||
inherit (lib.shb) anyNotNull;
|
inherit (lib.shb) anyNotNull;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
mkRequest =
|
mkRequest =
|
||||||
{ user ? "root",
|
{
|
||||||
|
user ? "root",
|
||||||
userText ? null,
|
userText ? null,
|
||||||
backupName ? "dump",
|
backupName ? "dump",
|
||||||
backupNameText ? null,
|
backupNameText ? null,
|
||||||
|
|
@ -14,7 +21,8 @@ in
|
||||||
backupCmdText ? null,
|
backupCmdText ? null,
|
||||||
restoreCmd ? "",
|
restoreCmd ? "",
|
||||||
restoreCmdText ? null,
|
restoreCmdText ? null,
|
||||||
}: mkOption {
|
}:
|
||||||
|
mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Request part of the backup contract.
|
Request part of the backup contract.
|
||||||
|
|
||||||
|
|
@ -23,137 +31,161 @@ in
|
||||||
'';
|
'';
|
||||||
|
|
||||||
default = {
|
default = {
|
||||||
inherit user backupName backupCmd restoreCmd;
|
inherit
|
||||||
|
user
|
||||||
|
backupName
|
||||||
|
backupCmd
|
||||||
|
restoreCmd
|
||||||
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultText = optionalString (anyNotNull [
|
defaultText =
|
||||||
userText
|
optionalString
|
||||||
backupNameText
|
(anyNotNull [
|
||||||
backupCmdText
|
userText
|
||||||
restoreCmdText
|
backupNameText
|
||||||
]) (literalMD ''
|
backupCmdText
|
||||||
{
|
restoreCmdText
|
||||||
user = ${if userText != null then userText else user};
|
])
|
||||||
backupName = ${if backupNameText != null then backupNameText else backupName};
|
(literalMD ''
|
||||||
backupCmd = ${if backupCmdText != null then backupCmdText else backupCmd};
|
{
|
||||||
restoreCmd = ${if restoreCmdText != null then restoreCmdText else restoreCmd};
|
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 {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
user = mkOption {
|
user =
|
||||||
description = ''
|
mkOption {
|
||||||
Unix user doing the backups.
|
description = ''
|
||||||
|
Unix user doing the backups.
|
||||||
|
|
||||||
This should be an admin user having access to all databases.
|
This should be an admin user having access to all databases.
|
||||||
'';
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
example = "postgres";
|
example = "postgres";
|
||||||
default = user;
|
default = user;
|
||||||
} // optionalAttrs (userText != null) {
|
}
|
||||||
defaultText = literalMD userText;
|
// optionalAttrs (userText != null) {
|
||||||
};
|
defaultText = literalMD userText;
|
||||||
|
};
|
||||||
|
|
||||||
backupName = mkOption {
|
backupName =
|
||||||
description = "Name of the backup in the repository.";
|
mkOption {
|
||||||
type = str;
|
description = "Name of the backup in the repository.";
|
||||||
example = "postgresql.sql";
|
type = str;
|
||||||
default = backupName;
|
example = "postgresql.sql";
|
||||||
} // optionalAttrs (backupNameText != null) {
|
default = backupName;
|
||||||
defaultText = literalMD backupNameText;
|
}
|
||||||
};
|
// optionalAttrs (backupNameText != null) {
|
||||||
|
defaultText = literalMD backupNameText;
|
||||||
|
};
|
||||||
|
|
||||||
backupCmd = mkOption {
|
backupCmd =
|
||||||
description = "Command that produces the database dump on stdout.";
|
mkOption {
|
||||||
type = str;
|
description = "Command that produces the database dump on stdout.";
|
||||||
example = literalExpression ''
|
type = str;
|
||||||
''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable
|
example = literalExpression ''
|
||||||
'';
|
''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable
|
||||||
default = backupCmd;
|
'';
|
||||||
} // optionalAttrs (backupCmdText != null) {
|
default = backupCmd;
|
||||||
defaultText = literalMD backupCmdText;
|
}
|
||||||
};
|
// optionalAttrs (backupCmdText != null) {
|
||||||
|
defaultText = literalMD backupCmdText;
|
||||||
|
};
|
||||||
|
|
||||||
restoreCmd = mkOption {
|
restoreCmd =
|
||||||
description = "Command that reads the database dump on stdin and restores the database.";
|
mkOption {
|
||||||
type = str;
|
description = "Command that reads the database dump on stdin and restores the database.";
|
||||||
example = literalExpression ''
|
type = str;
|
||||||
''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres
|
example = literalExpression ''
|
||||||
'';
|
''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres
|
||||||
default = restoreCmd;
|
'';
|
||||||
} // optionalAttrs (restoreCmdText != null) {
|
default = restoreCmd;
|
||||||
defaultText = literalMD restoreCmdText;
|
}
|
||||||
};
|
// optionalAttrs (restoreCmdText != null) {
|
||||||
|
defaultText = literalMD restoreCmdText;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mkResult =
|
||||||
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 ''
|
|
||||||
{
|
{
|
||||||
restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript};
|
restoreScript ? "restore",
|
||||||
backupService = ${if backupServiceText != null then backupServiceText else backupService};
|
restoreScriptText ? null,
|
||||||
}
|
backupService ? "backup.service",
|
||||||
'');
|
backupServiceText ? null,
|
||||||
|
}:
|
||||||
|
mkOption {
|
||||||
|
description = ''
|
||||||
|
Result part of the backup contract.
|
||||||
|
|
||||||
type = submodule {
|
Options set by the provider module that indicates the name of the backup and restor scripts.
|
||||||
options = {
|
'';
|
||||||
restoreScript = mkOption {
|
default = {
|
||||||
description = ''
|
inherit restoreScript backupService;
|
||||||
Name of script that can restore the database.
|
};
|
||||||
One can then list snapshots with:
|
|
||||||
|
|
||||||
```bash
|
defaultText =
|
||||||
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
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
|
```bash
|
||||||
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
|
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
||||||
```
|
```
|
||||||
'';
|
|
||||||
type = str;
|
|
||||||
default = restoreScript;
|
|
||||||
} // optionalAttrs (restoreScriptText != null) {
|
|
||||||
defaultText = literalMD restoreScriptText;
|
|
||||||
};
|
|
||||||
|
|
||||||
backupService = mkOption {
|
And restore the database with:
|
||||||
description = ''
|
|
||||||
Name of service backing up the database.
|
|
||||||
|
|
||||||
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
|
backupService =
|
||||||
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
|
mkOption {
|
||||||
```
|
description = ''
|
||||||
'';
|
Name of service backing up the database.
|
||||||
type = str;
|
|
||||||
default = backupService;
|
This script can be ran manually to backup the database:
|
||||||
} // optionalAttrs (backupServiceText != null) {
|
|
||||||
defaultText = literalMD backupServiceText;
|
```bash
|
||||||
|
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
type = str;
|
||||||
|
default = backupService;
|
||||||
|
}
|
||||||
|
// optionalAttrs (backupServiceText != null) {
|
||||||
|
defaultText = literalMD backupServiceText;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{ pkgs, lib, ... }:
|
{ pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
contracts = pkgs.callPackage ../. {};
|
contracts = pkgs.callPackage ../. { };
|
||||||
|
|
||||||
inherit (lib) mkOption;
|
inherit (lib) mkOption;
|
||||||
inherit (lib.types) submodule;
|
inherit (lib.types) submodule;
|
||||||
|
|
|
||||||
|
|
@ -1,84 +1,98 @@
|
||||||
{ pkgs, lib }:
|
{ pkgs, lib }:
|
||||||
let
|
let
|
||||||
inherit (lib) getAttrFromPath mkIf optionalAttrs setAttrByPath;
|
inherit (lib)
|
||||||
|
getAttrFromPath
|
||||||
|
mkIf
|
||||||
|
optionalAttrs
|
||||||
|
setAttrByPath
|
||||||
|
;
|
||||||
in
|
in
|
||||||
{ name,
|
{
|
||||||
|
name,
|
||||||
requesterRoot,
|
requesterRoot,
|
||||||
providerRoot,
|
providerRoot,
|
||||||
extraConfig ? null, # { config, database } -> attrset
|
extraConfig ? null, # { config, database } -> attrset
|
||||||
modules ? [],
|
modules ? [ ],
|
||||||
database ? "me",
|
database ? "me",
|
||||||
settings, # { repository, config } -> attrset
|
settings, # { repository, config } -> attrset
|
||||||
}: lib.shb.runNixOSTest {
|
}:
|
||||||
|
lib.shb.runNixOSTest {
|
||||||
inherit name;
|
inherit name;
|
||||||
|
|
||||||
nodes.machine = { config, ... }: {
|
nodes.machine =
|
||||||
imports = [ lib.shb.baseImports ] ++ modules;
|
{ config, ... }:
|
||||||
config = lib.mkMerge [
|
{
|
||||||
(setAttrByPath providerRoot {
|
imports = [ lib.shb.baseImports ] ++ modules;
|
||||||
request = (getAttrFromPath requesterRoot config).request;
|
config = lib.mkMerge [
|
||||||
settings = settings {
|
(setAttrByPath providerRoot {
|
||||||
inherit config;
|
request = (getAttrFromPath requesterRoot config).request;
|
||||||
repository = "/opt/repos/database";
|
settings = settings {
|
||||||
};
|
inherit config;
|
||||||
})
|
repository = "/opt/repos/database";
|
||||||
(mkIf (database != "root") {
|
};
|
||||||
users.users.${database} = {
|
})
|
||||||
isSystemUser = true;
|
(mkIf (database != "root") {
|
||||||
extraGroups = [ "sudoers" ];
|
users.users.${database} = {
|
||||||
group = "root";
|
isSystemUser = true;
|
||||||
};
|
extraGroups = [ "sudoers" ];
|
||||||
})
|
group = "root";
|
||||||
(optionalAttrs (extraConfig != null) (extraConfig { inherit config database; }))
|
};
|
||||||
];
|
})
|
||||||
};
|
(optionalAttrs (extraConfig != null) (extraConfig {
|
||||||
|
inherit config database;
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
testScript = { nodes, ... }: let
|
testScript =
|
||||||
provider = getAttrFromPath providerRoot nodes.machine;
|
{ nodes, ... }:
|
||||||
in ''
|
let
|
||||||
import csv
|
provider = getAttrFromPath providerRoot nodes.machine;
|
||||||
|
in
|
||||||
|
''
|
||||||
|
import csv
|
||||||
|
|
||||||
start_all()
|
start_all()
|
||||||
machine.wait_for_unit("postgresql.service")
|
machine.wait_for_unit("postgresql.service")
|
||||||
machine.wait_for_open_port(5432)
|
machine.wait_for_open_port(5432)
|
||||||
|
|
||||||
def peer_cmd(cmd, db="me"):
|
def peer_cmd(cmd, db="me"):
|
||||||
return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db)
|
return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db)
|
||||||
|
|
||||||
def query(query):
|
def query(query):
|
||||||
res = machine.succeed(peer_cmd(query))
|
res = machine.succeed(peer_cmd(query))
|
||||||
return list(dict(l) for l in csv.DictReader(res.splitlines()))
|
return list(dict(l) for l in csv.DictReader(res.splitlines()))
|
||||||
|
|
||||||
def cmp_tables(a, b):
|
def cmp_tables(a, b):
|
||||||
for i in range(max(len(a), len(b))):
|
for i in range(max(len(a), len(b))):
|
||||||
diff = set(a[i]) ^ set(b[i])
|
diff = set(a[i]) ^ set(b[i])
|
||||||
if len(diff) > 0:
|
if len(diff) > 0:
|
||||||
raise Exception(i, diff)
|
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"):
|
with subtest("create fixture"):
|
||||||
machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)"))
|
machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)"))
|
||||||
machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)"))
|
machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)"))
|
||||||
|
|
||||||
res = query("SELECT * FROM test")
|
res = query("SELECT * FROM test")
|
||||||
cmp_tables(res, table)
|
cmp_tables(res, table)
|
||||||
|
|
||||||
with subtest("backup"):
|
with subtest("backup"):
|
||||||
print(machine.succeed("systemctl cat ${provider.result.backupService}"))
|
print(machine.succeed("systemctl cat ${provider.result.backupService}"))
|
||||||
print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase"))
|
print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase"))
|
||||||
machine.succeed("systemctl start ${provider.result.backupService}")
|
machine.succeed("systemctl start ${provider.result.backupService}")
|
||||||
|
|
||||||
with subtest("drop database"):
|
with subtest("drop database"):
|
||||||
machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres"))
|
machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres"))
|
||||||
machine.fail(peer_cmd("SELECT * FROM test"))
|
machine.fail(peer_cmd("SELECT * FROM test"))
|
||||||
|
|
||||||
with subtest("restore"):
|
with subtest("restore"):
|
||||||
print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})"))
|
print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})"))
|
||||||
machine.succeed("${provider.result.restoreScript} restore latest ")
|
machine.succeed("${provider.result.restoreScript} restore latest ")
|
||||||
|
|
||||||
with subtest("check restoration"):
|
with subtest("check restoration"):
|
||||||
res = query("SELECT * FROM test")
|
res = query("SELECT * FROM test")
|
||||||
cmp_tables(res, table)
|
cmp_tables(res, table)
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,55 +4,61 @@ let
|
||||||
inherit (lib.types) anything;
|
inherit (lib.types) anything;
|
||||||
|
|
||||||
mkContractFunctions =
|
mkContractFunctions =
|
||||||
{ mkRequest,
|
{
|
||||||
|
mkRequest,
|
||||||
mkResult,
|
mkResult,
|
||||||
}: {
|
}:
|
||||||
|
{
|
||||||
mkRequester = requestCfg: {
|
mkRequester = requestCfg: {
|
||||||
request = mkRequest requestCfg;
|
request = mkRequest requestCfg;
|
||||||
|
|
||||||
result = mkResult {};
|
result = mkResult { };
|
||||||
};
|
};
|
||||||
|
|
||||||
mkProvider =
|
mkProvider =
|
||||||
{ resultCfg,
|
{
|
||||||
settings ? {},
|
resultCfg,
|
||||||
}: {
|
settings ? { },
|
||||||
request = mkRequest {};
|
}:
|
||||||
|
{
|
||||||
|
request = mkRequest { };
|
||||||
|
|
||||||
result = mkResult resultCfg;
|
result = mkResult resultCfg;
|
||||||
} // optionalAttrs (settings != {}) { inherit settings; };
|
}
|
||||||
|
// optionalAttrs (settings != { }) { inherit settings; };
|
||||||
|
|
||||||
contract = {
|
contract = {
|
||||||
request = mkRequest {};
|
request = mkRequest { };
|
||||||
|
|
||||||
result = mkResult {};
|
result = mkResult { };
|
||||||
|
|
||||||
settings = mkOption {
|
settings = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Optional attribute set with options specific to the provider.
|
Optional attribute set with options specific to the provider.
|
||||||
'';
|
'';
|
||||||
type = anything;
|
type = anything;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
importContract = module:
|
importContract =
|
||||||
|
module:
|
||||||
let
|
let
|
||||||
importedModule = pkgs.callPackage module {};
|
importedModule = pkgs.callPackage module { };
|
||||||
in
|
in
|
||||||
mkContractFunctions {
|
mkContractFunctions {
|
||||||
inherit (importedModule) mkRequest mkResult;
|
inherit (importedModule) mkRequest mkResult;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
databasebackup = importContract ./databasebackup.nix;
|
databasebackup = importContract ./databasebackup.nix;
|
||||||
backup = importContract ./backup.nix;
|
backup = importContract ./backup.nix;
|
||||||
mount = pkgs.callPackage ./mount.nix {};
|
mount = pkgs.callPackage ./mount.nix { };
|
||||||
secret = importContract ./secret.nix;
|
secret = importContract ./secret.nix;
|
||||||
ssl = pkgs.callPackage ./ssl.nix {};
|
ssl = pkgs.callPackage ./ssl.nix { };
|
||||||
test = {
|
test = {
|
||||||
secret = pkgs.callPackage ./secret/test.nix {};
|
secret = pkgs.callPackage ./secret/test.nix { };
|
||||||
databasebackup = pkgs.callPackage ./databasebackup/test.nix {};
|
databasebackup = pkgs.callPackage ./databasebackup/test.nix { };
|
||||||
backup = pkgs.callPackage ./backup/test.nix {};
|
backup = pkgs.callPackage ./backup/test.nix { };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,28 @@
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString;
|
inherit (lib)
|
||||||
|
concatStringsSep
|
||||||
|
literalMD
|
||||||
|
mkOption
|
||||||
|
optionalAttrs
|
||||||
|
optionalString
|
||||||
|
;
|
||||||
inherit (lib.types) listOf submodule str;
|
inherit (lib.types) listOf submodule str;
|
||||||
inherit (lib.shb) anyNotNull;
|
inherit (lib.shb) anyNotNull;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
mkRequest =
|
mkRequest =
|
||||||
{ mode ? "0400",
|
{
|
||||||
|
mode ? "0400",
|
||||||
modeText ? null,
|
modeText ? null,
|
||||||
owner ? "root",
|
owner ? "root",
|
||||||
ownerText ? null,
|
ownerText ? null,
|
||||||
group ? "root",
|
group ? "root",
|
||||||
groupText ? null,
|
groupText ? null,
|
||||||
restartUnits ? [],
|
restartUnits ? [ ],
|
||||||
restartUnitsText ? null,
|
restartUnitsText ? null,
|
||||||
}: mkOption {
|
}:
|
||||||
|
mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Request part of the secret contract.
|
Request part of the secret contract.
|
||||||
|
|
||||||
|
|
@ -23,64 +31,87 @@ in
|
||||||
'';
|
'';
|
||||||
|
|
||||||
default = {
|
default = {
|
||||||
inherit mode owner group restartUnits;
|
inherit
|
||||||
|
mode
|
||||||
|
owner
|
||||||
|
group
|
||||||
|
restartUnits
|
||||||
|
;
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultText = optionalString (anyNotNull [
|
defaultText =
|
||||||
modeText
|
optionalString
|
||||||
ownerText
|
(anyNotNull [
|
||||||
groupText
|
modeText
|
||||||
restartUnitsText
|
ownerText
|
||||||
]) (literalMD ''
|
groupText
|
||||||
{
|
restartUnitsText
|
||||||
mode = ${if modeText != null then modeText else mode};
|
])
|
||||||
owner = ${if ownerText != null then ownerText else owner};
|
(literalMD ''
|
||||||
group = ${if groupText != null then groupText else group};
|
{
|
||||||
restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + concatStringsSep " " restartUnits + " ]"};
|
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 {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
mode = mkOption {
|
mode =
|
||||||
description = ''
|
mkOption {
|
||||||
Mode of the secret file.
|
description = ''
|
||||||
'';
|
Mode of the secret file.
|
||||||
type = str;
|
'';
|
||||||
default = mode;
|
type = str;
|
||||||
} // optionalAttrs (modeText != null) {
|
default = mode;
|
||||||
defaultText = literalMD modeText;
|
}
|
||||||
};
|
// optionalAttrs (modeText != null) {
|
||||||
|
defaultText = literalMD modeText;
|
||||||
|
};
|
||||||
|
|
||||||
owner = mkOption ({
|
owner = mkOption (
|
||||||
description = ''
|
{
|
||||||
Linux user owning the secret file.
|
description = ''
|
||||||
'';
|
Linux user owning the secret file.
|
||||||
type = str;
|
'';
|
||||||
default = owner;
|
type = str;
|
||||||
} // optionalAttrs (ownerText != null) {
|
default = owner;
|
||||||
defaultText = literalMD ownerText;
|
}
|
||||||
});
|
// optionalAttrs (ownerText != null) {
|
||||||
|
defaultText = literalMD ownerText;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
group = mkOption {
|
group =
|
||||||
description = ''
|
mkOption {
|
||||||
Linux group owning the secret file.
|
description = ''
|
||||||
'';
|
Linux group owning the secret file.
|
||||||
type = str;
|
'';
|
||||||
default = group;
|
type = str;
|
||||||
} // optionalAttrs (groupText != null) {
|
default = group;
|
||||||
defaultText = literalMD groupText;
|
}
|
||||||
};
|
// optionalAttrs (groupText != null) {
|
||||||
|
defaultText = literalMD groupText;
|
||||||
|
};
|
||||||
|
|
||||||
restartUnits = mkOption ({
|
restartUnits = mkOption (
|
||||||
description = ''
|
{
|
||||||
Systemd units to restart after the secret is updated.
|
description = ''
|
||||||
'';
|
Systemd units to restart after the secret is updated.
|
||||||
type = listOf str;
|
'';
|
||||||
default = restartUnits;
|
type = listOf str;
|
||||||
} // optionalAttrs (restartUnitsText != null) {
|
default = restartUnits;
|
||||||
defaultText = literalMD restartUnitsText;
|
}
|
||||||
});
|
// optionalAttrs (restartUnitsText != null) {
|
||||||
|
defaultText = literalMD restartUnitsText;
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -90,34 +121,39 @@ in
|
||||||
path ? "/run/secrets/secret",
|
path ? "/run/secrets/secret",
|
||||||
pathText ? null,
|
pathText ? null,
|
||||||
}:
|
}:
|
||||||
mkOption ({
|
mkOption (
|
||||||
description = ''
|
{
|
||||||
Result part of the secret contract.
|
description = ''
|
||||||
|
Result part of the secret contract.
|
||||||
|
|
||||||
Options set by the provider module that indicates where the secret can be found.
|
Options set by the provider module that indicates where the secret can be found.
|
||||||
'';
|
'';
|
||||||
default = {
|
default = {
|
||||||
inherit path;
|
inherit path;
|
||||||
};
|
};
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
path = mkOption {
|
path =
|
||||||
type = lib.types.path;
|
mkOption {
|
||||||
description = ''
|
type = lib.types.path;
|
||||||
Path to the file containing the secret generated out of band.
|
description = ''
|
||||||
|
Path to the file containing the secret generated out of band.
|
||||||
|
|
||||||
This path will exist after deploying to a target host,
|
This path will exist after deploying to a target host,
|
||||||
it is not available through the nix store.
|
it is not available through the nix store.
|
||||||
'';
|
'';
|
||||||
default = path;
|
default = path;
|
||||||
} // optionalAttrs (pathText != null) {
|
}
|
||||||
defaultText = pathText;
|
// optionalAttrs (pathText != null) {
|
||||||
|
defaultText = pathText;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
} // optionalAttrs (pathText != null) {
|
// optionalAttrs (pathText != null) {
|
||||||
defaultText = {
|
defaultText = {
|
||||||
path = pathText;
|
path = pathText;
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{ pkgs, lib, ... }:
|
{ pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
contracts = pkgs.callPackage ../. {};
|
contracts = pkgs.callPackage ../. { };
|
||||||
|
|
||||||
inherit (lib) mkOption;
|
inherit (lib) mkOption;
|
||||||
inherit (lib.types) submodule;
|
inherit (lib.types) submodule;
|
||||||
|
|
|
||||||
|
|
@ -3,24 +3,33 @@ let
|
||||||
inherit (lib) getAttrFromPath setAttrByPath;
|
inherit (lib) getAttrFromPath setAttrByPath;
|
||||||
inherit (lib) mkIf;
|
inherit (lib) mkIf;
|
||||||
in
|
in
|
||||||
{ name,
|
{
|
||||||
configRoot,
|
name,
|
||||||
settingsCfg, # str -> attrset
|
configRoot,
|
||||||
modules ? [],
|
settingsCfg, # str -> attrset
|
||||||
owner ? "root",
|
modules ? [ ],
|
||||||
group ? "root",
|
owner ? "root",
|
||||||
mode ? "0400",
|
group ? "root",
|
||||||
restartUnits ? [ "myunit.service" ],
|
mode ? "0400",
|
||||||
}: lib.shb.runNixOSTest {
|
restartUnits ? [ "myunit.service" ],
|
||||||
name = "secret_${name}_${owner}_${group}_${mode}";
|
}:
|
||||||
|
lib.shb.runNixOSTest {
|
||||||
|
name = "secret_${name}_${owner}_${group}_${mode}";
|
||||||
|
|
||||||
nodes.machine = { config, ... }: {
|
nodes.machine =
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
imports = [ lib.shb.baseImports ] ++ modules;
|
imports = [ lib.shb.baseImports ] ++ modules;
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
(setAttrByPath configRoot {
|
(setAttrByPath configRoot {
|
||||||
A = {
|
A = {
|
||||||
request = {
|
request = {
|
||||||
inherit owner group mode restartUnits;
|
inherit
|
||||||
|
owner
|
||||||
|
group
|
||||||
|
mode
|
||||||
|
restartUnits
|
||||||
|
;
|
||||||
};
|
};
|
||||||
settings = settingsCfg "secretA";
|
settings = settingsCfg "secretA";
|
||||||
};
|
};
|
||||||
|
|
@ -29,35 +38,36 @@ in
|
||||||
users.users.${owner}.isNormalUser = true;
|
users.users.${owner}.isNormalUser = true;
|
||||||
})
|
})
|
||||||
(mkIf (group != "root") {
|
(mkIf (group != "root") {
|
||||||
users.groups.${group} = {};
|
users.groups.${group} = { };
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = { nodes, ... }:
|
testScript =
|
||||||
let
|
{ nodes, ... }:
|
||||||
result = (getAttrFromPath configRoot nodes.machine)."A".result;
|
let
|
||||||
in
|
result = (getAttrFromPath configRoot nodes.machine)."A".result;
|
||||||
''
|
in
|
||||||
owner = machine.succeed("stat -c '%U' ${result.path}").strip()
|
''
|
||||||
print(f"Got owner {owner}")
|
owner = machine.succeed("stat -c '%U' ${result.path}").strip()
|
||||||
if owner != "${owner}":
|
print(f"Got owner {owner}")
|
||||||
raise Exception(f"Owner should be '${owner}' but got '{owner}'")
|
if owner != "${owner}":
|
||||||
|
raise Exception(f"Owner should be '${owner}' but got '{owner}'")
|
||||||
|
|
||||||
group = machine.succeed("stat -c '%G' ${result.path}").strip()
|
group = machine.succeed("stat -c '%G' ${result.path}").strip()
|
||||||
print(f"Got group {group}")
|
print(f"Got group {group}")
|
||||||
if group != "${group}":
|
if group != "${group}":
|
||||||
raise Exception(f"Group should be '${group}' but got '{group}'")
|
raise Exception(f"Group should be '${group}' but got '{group}'")
|
||||||
|
|
||||||
mode = str(int(machine.succeed("stat -c '%a' ${result.path}").strip()))
|
mode = str(int(machine.succeed("stat -c '%a' ${result.path}").strip()))
|
||||||
print(f"Got mode {mode}")
|
print(f"Got mode {mode}")
|
||||||
wantedMode = str(int("${mode}"))
|
wantedMode = str(int("${mode}"))
|
||||||
if mode != wantedMode:
|
if mode != wantedMode:
|
||||||
raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'")
|
raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'")
|
||||||
|
|
||||||
content = machine.succeed("cat ${result.path}").strip()
|
content = machine.succeed("cat ${result.path}").strip()
|
||||||
print(f"Got content {content}")
|
print(f"Got content {content}")
|
||||||
if content != "secretA":
|
if content != "secretA":
|
||||||
raise Exception(f"Content should be 'secretA' but got '{content}'")
|
raise Exception(f"Content should be 'secretA' but got '{content}'")
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{ pkgs, lib, ... }:
|
{ pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
contracts = pkgs.callPackage ../. {};
|
contracts = pkgs.callPackage ../. { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.contracts.ssl = lib.mkOption {
|
options.shb.contracts.ssl = lib.mkOption {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.arr;
|
cfg = config.shb.arr;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
apps = {
|
apps = {
|
||||||
radarr = {
|
radarr = {
|
||||||
|
|
@ -11,7 +16,7 @@ let
|
||||||
moreOptions = {
|
moreOptions = {
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
description = "Specific options for radarr.";
|
description = "Specific options for radarr.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = apps.radarr.settingsFormat.type;
|
freeformType = apps.radarr.settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
|
|
@ -20,7 +25,10 @@ let
|
||||||
description = "Path to api key secret file.";
|
description = "Path to api key secret file.";
|
||||||
};
|
};
|
||||||
LogLevel = lib.mkOption {
|
LogLevel = lib.mkOption {
|
||||||
type = lib.types.enum ["debug" "info"];
|
type = lib.types.enum [
|
||||||
|
"debug"
|
||||||
|
"info"
|
||||||
|
];
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
|
|
@ -69,7 +77,7 @@ let
|
||||||
moreOptions = {
|
moreOptions = {
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
description = "Specific options for sonarr.";
|
description = "Specific options for sonarr.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = apps.sonarr.settingsFormat.type;
|
freeformType = apps.sonarr.settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
|
|
@ -78,7 +86,10 @@ let
|
||||||
description = "Path to api key secret file.";
|
description = "Path to api key secret file.";
|
||||||
};
|
};
|
||||||
LogLevel = lib.mkOption {
|
LogLevel = lib.mkOption {
|
||||||
type = lib.types.enum ["debug" "info"];
|
type = lib.types.enum [
|
||||||
|
"debug"
|
||||||
|
"info"
|
||||||
|
];
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
|
|
@ -122,12 +133,15 @@ let
|
||||||
moreOptions = {
|
moreOptions = {
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
description = "Specific options for bazarr.";
|
description = "Specific options for bazarr.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = apps.bazarr.settingsFormat.type;
|
freeformType = apps.bazarr.settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
LogLevel = lib.mkOption {
|
LogLevel = lib.mkOption {
|
||||||
type = lib.types.enum ["debug" "info"];
|
type = lib.types.enum [
|
||||||
|
"debug"
|
||||||
|
"info"
|
||||||
|
];
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
|
|
@ -147,12 +161,15 @@ let
|
||||||
moreOptions = {
|
moreOptions = {
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
description = "Specific options for readarr.";
|
description = "Specific options for readarr.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = apps.readarr.settingsFormat.type;
|
freeformType = apps.readarr.settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
LogLevel = lib.mkOption {
|
LogLevel = lib.mkOption {
|
||||||
type = lib.types.enum ["debug" "info"];
|
type = lib.types.enum [
|
||||||
|
"debug"
|
||||||
|
"info"
|
||||||
|
];
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
|
|
@ -171,12 +188,15 @@ let
|
||||||
moreOptions = {
|
moreOptions = {
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
description = "Specific options for lidarr.";
|
description = "Specific options for lidarr.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = apps.lidarr.settingsFormat.type;
|
freeformType = apps.lidarr.settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
LogLevel = lib.mkOption {
|
LogLevel = lib.mkOption {
|
||||||
type = lib.types.enum ["debug" "info"];
|
type = lib.types.enum [
|
||||||
|
"debug"
|
||||||
|
"info"
|
||||||
|
];
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
|
|
@ -191,11 +211,11 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
jackett = {
|
jackett = {
|
||||||
settingsFormat = pkgs.formats.json {};
|
settingsFormat = pkgs.formats.json { };
|
||||||
moreOptions = {
|
moreOptions = {
|
||||||
settings = lib.mkOption {
|
settings = lib.mkOption {
|
||||||
description = "Specific options for jackett.";
|
description = "Specific options for jackett.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
freeformType = apps.jackett.settingsFormat.type;
|
freeformType = apps.jackett.settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
|
|
@ -214,13 +234,18 @@ let
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
ProxyType = lib.mkOption {
|
ProxyType = lib.mkOption {
|
||||||
type = lib.types.enum [ "-1" "0" "1" "2" ];
|
type = lib.types.enum [
|
||||||
|
"-1"
|
||||||
|
"0"
|
||||||
|
"1"
|
||||||
|
"2"
|
||||||
|
];
|
||||||
default = "-1";
|
default = "-1";
|
||||||
description = ''
|
description = ''
|
||||||
-1 = disabled
|
-1 = disabled
|
||||||
0 = HTTP
|
0 = HTTP
|
||||||
1 = SOCKS4
|
1 = SOCKS4
|
||||||
2 = SOCKS5
|
2 = SOCKS5
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
ProxyUrl = lib.mkOption {
|
ProxyUrl = lib.mkOption {
|
||||||
|
|
@ -256,83 +281,101 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
vhosts = { extraBypassResources ? [] }: c: {
|
vhosts =
|
||||||
inherit (c) subdomain domain authEndpoint ssl;
|
{
|
||||||
|
extraBypassResources ? [ ],
|
||||||
|
}:
|
||||||
|
c: {
|
||||||
|
inherit (c)
|
||||||
|
subdomain
|
||||||
|
domain
|
||||||
|
authEndpoint
|
||||||
|
ssl
|
||||||
|
;
|
||||||
|
|
||||||
upstream = "http://127.0.0.1:${toString c.settings.Port}";
|
upstream = "http://127.0.0.1:${toString c.settings.Port}";
|
||||||
autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [
|
autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [
|
||||||
{
|
{
|
||||||
domain = "${c.subdomain}.${c.domain}";
|
domain = "${c.subdomain}.${c.domain}";
|
||||||
policy = "bypass";
|
policy = "bypass";
|
||||||
resources = extraBypassResources ++ [
|
resources = extraBypassResources ++ [
|
||||||
"^/api.*"
|
"^/api.*"
|
||||||
"^/feed.*"
|
"^/feed.*"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
domain = "${c.subdomain}.${c.domain}";
|
domain = "${c.subdomain}.${c.domain}";
|
||||||
policy = "two_factor";
|
policy = "two_factor";
|
||||||
subject = ["group:arr_user"];
|
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 {});
|
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -343,155 +386,167 @@ in
|
||||||
|
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
(lib.mkIf cfg.radarr.enable (
|
(lib.mkIf cfg.radarr.enable (
|
||||||
let
|
let
|
||||||
cfg' = cfg.radarr;
|
cfg' = cfg.radarr;
|
||||||
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.nginx.enable = true;
|
services.nginx.enable = true;
|
||||||
|
|
||||||
services.radarr = {
|
services.radarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/radarr";
|
dataDir = "/var/lib/radarr";
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.radarr.preStart = lib.shb.replaceSecrets {
|
systemd.services.radarr.preStart = lib.shb.replaceSecrets {
|
||||||
userConfig = cfg'.settings
|
userConfig =
|
||||||
// (lib.optionalAttrs isSSOEnabled {
|
cfg'.settings
|
||||||
AuthenticationRequired = "DisabledForLocalAddresses";
|
// (lib.optionalAttrs isSSOEnabled {
|
||||||
AuthenticationMethod = "External";
|
AuthenticationRequired = "DisabledForLocalAddresses";
|
||||||
});
|
AuthenticationMethod = "External";
|
||||||
resultPath = "${config.services.radarr.dataDir}/config.xml";
|
});
|
||||||
generator = lib.shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat;
|
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 (
|
(lib.mkIf cfg.sonarr.enable (
|
||||||
let
|
let
|
||||||
cfg' = cfg.sonarr;
|
cfg' = cfg.sonarr;
|
||||||
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.nginx.enable = true;
|
services.nginx.enable = true;
|
||||||
|
|
||||||
services.sonarr = {
|
services.sonarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/sonarr";
|
dataDir = "/var/lib/sonarr";
|
||||||
};
|
};
|
||||||
users.users.sonarr = {
|
users.users.sonarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.sonarr.preStart = lib.shb.replaceSecrets {
|
systemd.services.sonarr.preStart = lib.shb.replaceSecrets {
|
||||||
userConfig = cfg'.settings
|
userConfig =
|
||||||
// (lib.optionalAttrs isSSOEnabled {
|
cfg'.settings
|
||||||
AuthenticationRequired = "DisabledForLocalAddresses";
|
// (lib.optionalAttrs isSSOEnabled {
|
||||||
AuthenticationMethod = "External";
|
AuthenticationRequired = "DisabledForLocalAddresses";
|
||||||
});
|
AuthenticationMethod = "External";
|
||||||
resultPath = "${config.services.sonarr.dataDir}/config.xml";
|
});
|
||||||
generator = apps.sonarr.settingsFormat.generate;
|
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 (
|
(lib.mkIf cfg.bazarr.enable (
|
||||||
let
|
let
|
||||||
cfg' = cfg.bazarr;
|
cfg' = cfg.bazarr;
|
||||||
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.bazarr = {
|
services.bazarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
listenPort = cfg'.settings.Port;
|
listenPort = cfg'.settings.Port;
|
||||||
};
|
};
|
||||||
users.users.bazarr = {
|
users.users.bazarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
systemd.services.bazarr.preStart = lib.shb.replaceSecrets {
|
systemd.services.bazarr.preStart = lib.shb.replaceSecrets {
|
||||||
userConfig = cfg'.settings
|
userConfig =
|
||||||
// (lib.optionalAttrs isSSOEnabled {
|
cfg'.settings
|
||||||
AuthenticationRequired = "DisabledForLocalAddresses";
|
// (lib.optionalAttrs isSSOEnabled {
|
||||||
AuthenticationMethod = "External";
|
AuthenticationRequired = "DisabledForLocalAddresses";
|
||||||
});
|
AuthenticationMethod = "External";
|
||||||
resultPath = "/var/lib/bazarr/config.xml";
|
});
|
||||||
generator = apps.bazarr.settingsFormat.generate;
|
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 (
|
(lib.mkIf cfg.readarr.enable (
|
||||||
let
|
let
|
||||||
cfg' = cfg.readarr;
|
cfg' = cfg.readarr;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.readarr = {
|
services.readarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/readarr";
|
dataDir = "/var/lib/readarr";
|
||||||
};
|
};
|
||||||
users.users.readarr = {
|
users.users.readarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
systemd.services.readarr.preStart = lib.shb.replaceSecrets {
|
systemd.services.readarr.preStart = lib.shb.replaceSecrets {
|
||||||
userConfig = cfg'.settings;
|
userConfig = cfg'.settings;
|
||||||
resultPath = "${config.services.readarr.dataDir}/config.xml";
|
resultPath = "${config.services.readarr.dataDir}/config.xml";
|
||||||
generator = apps.readarr.settingsFormat.generate;
|
generator = apps.readarr.settingsFormat.generate;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [ (vhosts {} cfg') ];
|
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
||||||
}))
|
}
|
||||||
|
))
|
||||||
|
|
||||||
(lib.mkIf cfg.lidarr.enable (
|
(lib.mkIf cfg.lidarr.enable (
|
||||||
let
|
let
|
||||||
cfg' = cfg.lidarr;
|
cfg' = cfg.lidarr;
|
||||||
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.lidarr = {
|
services.lidarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/lidarr";
|
dataDir = "/var/lib/lidarr";
|
||||||
};
|
};
|
||||||
users.users.lidarr = {
|
users.users.lidarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
systemd.services.lidarr.preStart = lib.shb.replaceSecrets {
|
systemd.services.lidarr.preStart = lib.shb.replaceSecrets {
|
||||||
userConfig = cfg'.settings
|
userConfig =
|
||||||
// (lib.optionalAttrs isSSOEnabled {
|
cfg'.settings
|
||||||
AuthenticationRequired = "DisabledForLocalAddresses";
|
// (lib.optionalAttrs isSSOEnabled {
|
||||||
AuthenticationMethod = "External";
|
AuthenticationRequired = "DisabledForLocalAddresses";
|
||||||
});
|
AuthenticationMethod = "External";
|
||||||
resultPath = "${config.services.lidarr.dataDir}/config.xml";
|
});
|
||||||
generator = apps.lidarr.settingsFormat.generate;
|
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 (
|
(lib.mkIf cfg.jackett.enable (
|
||||||
let
|
let
|
||||||
cfg' = cfg.jackett;
|
cfg' = cfg.jackett;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.jackett = {
|
services.jackett = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/var/lib/jackett";
|
dataDir = "/var/lib/jackett";
|
||||||
};
|
};
|
||||||
# TODO: avoid implicitly relying on the media group
|
# TODO: avoid implicitly relying on the media group
|
||||||
users.users.jackett = {
|
users.users.jackett = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
systemd.services.jackett.preStart = lib.shb.replaceSecrets {
|
systemd.services.jackett.preStart = lib.shb.replaceSecrets {
|
||||||
userConfig = lib.shb.renameAttrName cfg'.settings "ApiKey" "APIKey";
|
userConfig = lib.shb.renameAttrName cfg'.settings "ApiKey" "APIKey";
|
||||||
resultPath = "${config.services.jackett.dataDir}/ServerConfig.json";
|
resultPath = "${config.services.jackett.dataDir}/ServerConfig.json";
|
||||||
generator = apps.jackett.settingsFormat.generate;
|
generator = apps.jackett.settingsFormat.generate;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [ (vhosts {
|
shb.nginx.vhosts = [
|
||||||
extraBypassResources = [ "^/dl.*" ];
|
(vhosts {
|
||||||
} cfg') ];
|
extraBypassResources = [ "^/dl.*" ];
|
||||||
}))
|
} cfg')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.audiobookshelf;
|
cfg = config.shb.audiobookshelf;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
|
|
@ -40,18 +45,18 @@ in
|
||||||
extraServiceConfig = lib.mkOption {
|
extraServiceConfig = lib.mkOption {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = lib.types.attrsOf lib.types.str;
|
||||||
description = "Extra configuration given to the systemd service file.";
|
description = "Extra configuration given to the systemd service file.";
|
||||||
default = {};
|
default = { };
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
{
|
{
|
||||||
MemoryHigh = "512M";
|
MemoryHigh = "512M";
|
||||||
MemoryMax = "900M";
|
MemoryMax = "900M";
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = lib.mkOption {
|
sso = lib.mkOption {
|
||||||
description = "SSO configuration.";
|
description = "SSO configuration.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "SSO";
|
enable = lib.mkEnableOption "SSO";
|
||||||
|
|
@ -87,7 +92,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
authorization_policy = lib.mkOption {
|
authorization_policy = lib.mkOption {
|
||||||
type = lib.types.enum [ "one_factor" "two_factor" ];
|
type = lib.types.enum [
|
||||||
|
"one_factor"
|
||||||
|
"two_factor"
|
||||||
|
];
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
description = "Require one factor (password) or two factor (device) authentication.";
|
||||||
default = "one_factor";
|
default = "one_factor";
|
||||||
};
|
};
|
||||||
|
|
@ -133,84 +141,102 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
logLevel = lib.mkOption {
|
logLevel = lib.mkOption {
|
||||||
type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
|
type = lib.types.nullOr (
|
||||||
|
lib.types.enum [
|
||||||
|
"critical"
|
||||||
|
"error"
|
||||||
|
"warning"
|
||||||
|
"info"
|
||||||
|
"debug"
|
||||||
|
]
|
||||||
|
);
|
||||||
description = "Enable logging.";
|
description = "Enable logging.";
|
||||||
default = false;
|
default = false;
|
||||||
example = true;
|
example = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (lib.mkMerge [{
|
config = lib.mkIf cfg.enable (
|
||||||
|
lib.mkMerge [
|
||||||
services.audiobookshelf = {
|
|
||||||
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";
|
services.audiobookshelf = {
|
||||||
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
|
enable = true;
|
||||||
claims_policy = "${roleClaim}";
|
openFirewall = true;
|
||||||
public = false;
|
dataDir = "audiobookshelf";
|
||||||
authorization_policy = cfg.sso.authorization_policy;
|
host = "127.0.0.1";
|
||||||
redirect_uris = [
|
port = cfg.webPort;
|
||||||
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback"
|
};
|
||||||
"https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect"
|
|
||||||
|
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;
|
}
|
||||||
}]);
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,31 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.deluge;
|
cfg = config.shb.deluge;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
authGenerator = users:
|
authGenerator =
|
||||||
|
users:
|
||||||
let
|
let
|
||||||
genLine = name: { password, priority ? 10 }:
|
genLine =
|
||||||
|
name:
|
||||||
|
{
|
||||||
|
password,
|
||||||
|
priority ? 10,
|
||||||
|
}:
|
||||||
"${name}:${password}:${toString priority}";
|
"${name}:${password}:${toString priority}";
|
||||||
|
|
||||||
lines = lib.mapAttrsToList genLine users;
|
lines = lib.mapAttrsToList genLine users;
|
||||||
in
|
in
|
||||||
lib.concatStringsSep "\n" lines;
|
lib.concatStringsSep "\n" lines;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -57,7 +68,10 @@ in
|
||||||
daemonListenPorts = lib.mkOption {
|
daemonListenPorts = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.int;
|
type = lib.types.listOf lib.types.int;
|
||||||
description = "Deluge daemon listen ports";
|
description = "Deluge daemon listen ports";
|
||||||
default = [ 6881 6889 ];
|
default = [
|
||||||
|
6881
|
||||||
|
6889
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
webPort = lib.mkOption {
|
webPort = lib.mkOption {
|
||||||
|
|
@ -158,12 +172,12 @@ in
|
||||||
extraServiceConfig = lib.mkOption {
|
extraServiceConfig = lib.mkOption {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = lib.types.attrsOf lib.types.str;
|
||||||
description = "Extra configuration given to the systemd service file.";
|
description = "Extra configuration given to the systemd service file.";
|
||||||
default = {};
|
default = { };
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
{
|
{
|
||||||
MemoryHigh = "512M";
|
MemoryHigh = "512M";
|
||||||
MemoryMax = "900M";
|
MemoryMax = "900M";
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -176,14 +190,16 @@ in
|
||||||
|
|
||||||
extraUsers = lib.mkOption {
|
extraUsers = lib.mkOption {
|
||||||
description = "Users having access to this deluge instance. Attrset of username to user options.";
|
description = "Users having access to this deluge instance. Attrset of username to user options.";
|
||||||
type = lib.types.attrsOf (lib.types.submodule {
|
type = lib.types.attrsOf (
|
||||||
options = {
|
lib.types.submodule {
|
||||||
password = lib.mkOption {
|
options = {
|
||||||
type = lib.shb.secretFileType;
|
password = lib.mkOption {
|
||||||
description = "File containing the user password.";
|
type = lib.shb.secretFileType;
|
||||||
|
description = "File containing the user password.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
localclientPassword = lib.mkOption {
|
localclientPassword = lib.mkOption {
|
||||||
|
|
@ -198,12 +214,17 @@ in
|
||||||
|
|
||||||
prometheusScraperPassword = lib.mkOption {
|
prometheusScraperPassword = lib.mkOption {
|
||||||
description = "Password for prometheus scraper. Setting this option will activate the prometheus deluge exporter.";
|
description = "Password for prometheus scraper. Setting this option will activate the prometheus deluge exporter.";
|
||||||
type = lib.types.nullOr (lib.types.submodule {
|
type = lib.types.nullOr (
|
||||||
options = contracts.secret.mkRequester {
|
lib.types.submodule {
|
||||||
owner = "deluge";
|
options = contracts.secret.mkRequester {
|
||||||
restartUnits = [ "deluged.service" "prometheus.service" ];
|
owner = "deluge";
|
||||||
};
|
restartUnits = [
|
||||||
});
|
"deluged.service"
|
||||||
|
"prometheus.service"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -214,35 +235,35 @@ in
|
||||||
|
|
||||||
Label is automatically enabled if any of the `shb.arr.*` service is enabled.
|
Label is automatically enabled if any of the `shb.arr.*` service is enabled.
|
||||||
'';
|
'';
|
||||||
example = ["Label"];
|
example = [ "Label" ];
|
||||||
default = [];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
additionalPlugins = lib.mkOption {
|
additionalPlugins = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.path;
|
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.";
|
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 ''
|
example = lib.literalExpression ''
|
||||||
additionalPlugins = [
|
additionalPlugins = [
|
||||||
(pkgs.callPackage ({ python3, fetchFromGitHub }: python3.pkgs.buildPythonPackage {
|
(pkgs.callPackage ({ python3, fetchFromGitHub }: python3.pkgs.buildPythonPackage {
|
||||||
name = "deluge-autotracker";
|
name = "deluge-autotracker";
|
||||||
version = "1.0.0";
|
version = "1.0.0";
|
||||||
src = fetchFromGitHub {
|
src = fetchFromGitHub {
|
||||||
owner = "ibizaman";
|
owner = "ibizaman";
|
||||||
repo = "deluge-autotracker";
|
repo = "deluge-autotracker";
|
||||||
rev = "cc40d816a497bbf1c2ebeb3d8b1176210548a3e6";
|
rev = "cc40d816a497bbf1c2ebeb3d8b1176210548a3e6";
|
||||||
sha256 = "sha256-0LpVdv1fak2a5eX4unjhUcN7nMAl9fgpr3X+7XnQE6c=";
|
sha256 = "sha256-0LpVdv1fak2a5eX4unjhUcN7nMAl9fgpr3X+7XnQE6c=";
|
||||||
} + "/autotracker";
|
} + "/autotracker";
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
format = "other";
|
format = "other";
|
||||||
nativeBuildInputs = [ python3.pkgs.setuptools ];
|
nativeBuildInputs = [ python3.pkgs.setuptools ];
|
||||||
buildPhase = '''
|
buildPhase = '''
|
||||||
mkdir "$out"
|
mkdir "$out"
|
||||||
python3 setup.py install --install-lib "$out"
|
python3 setup.py install --install-lib "$out"
|
||||||
''';
|
''';
|
||||||
doInstallPhase = false;
|
doInstallPhase = false;
|
||||||
}) {})
|
}) {})
|
||||||
];
|
];
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -250,7 +271,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Backup configuration.
|
Backup configuration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "deluge";
|
user = "deluge";
|
||||||
|
|
@ -262,146 +283,175 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
logLevel = lib.mkOption {
|
logLevel = lib.mkOption {
|
||||||
type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
|
type = lib.types.nullOr (
|
||||||
|
lib.types.enum [
|
||||||
|
"critical"
|
||||||
|
"error"
|
||||||
|
"warning"
|
||||||
|
"info"
|
||||||
|
"debug"
|
||||||
|
]
|
||||||
|
);
|
||||||
description = "Enable logging.";
|
description = "Enable logging.";
|
||||||
default = null;
|
default = null;
|
||||||
example = "info";
|
example = "info";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (lib.mkMerge [{
|
config = lib.mkIf cfg.enable (
|
||||||
services.deluge = {
|
lib.mkMerge [
|
||||||
enable = true;
|
{
|
||||||
declarative = true;
|
services.deluge = {
|
||||||
openFirewall = true;
|
enable = true;
|
||||||
inherit (cfg) dataDir;
|
declarative = true;
|
||||||
|
openFirewall = true;
|
||||||
|
inherit (cfg) dataDir;
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
download_location = cfg.settings.downloadLocation;
|
download_location = cfg.settings.downloadLocation;
|
||||||
allow_remote = true;
|
allow_remote = true;
|
||||||
daemon_port = cfg.daemonPort;
|
daemon_port = cfg.daemonPort;
|
||||||
listen_ports = cfg.daemonListenPorts;
|
listen_ports = cfg.daemonListenPorts;
|
||||||
proxy = lib.optionalAttrs (cfg.proxyPort != null) {
|
proxy = lib.optionalAttrs (cfg.proxyPort != null) {
|
||||||
force_proxy = true;
|
force_proxy = true;
|
||||||
hostname = "127.0.0.1";
|
hostname = "127.0.0.1";
|
||||||
port = cfg.proxyPort;
|
port = cfg.proxyPort;
|
||||||
proxy_hostnames = true;
|
proxy_hostnames = true;
|
||||||
proxy_peer_connections = true;
|
proxy_peer_connections = true;
|
||||||
proxy_tracker_connections = true;
|
proxy_tracker_connections = true;
|
||||||
type = 4; # HTTP
|
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
|
systemd.services.deluged.preStart = lib.mkBefore (
|
||||||
++ lib.optional (lib.any (x: x.enable) [
|
lib.shb.replaceSecrets {
|
||||||
config.services.radarr
|
userConfig =
|
||||||
config.services.sonarr
|
cfg.extraUsers
|
||||||
config.services.bazarr
|
// {
|
||||||
config.services.readarr
|
localclient.password.source = config.shb.deluge.localclientPassword.result.path;
|
||||||
config.services.lidarr
|
}
|
||||||
]) "Label";
|
// (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)
|
systemd.services.deluged.serviceConfig.ExecStart = lib.mkForce (
|
||||||
max_active_limit
|
lib.concatStringsSep " \\\n " (
|
||||||
max_active_downloading
|
[
|
||||||
max_active_seeding
|
"${config.services.deluge.package}/bin/deluged"
|
||||||
max_connections_global
|
"--do-not-daemonize"
|
||||||
max_connections_per_torrent
|
"--config ${cfg.dataDir}/.config/deluge"
|
||||||
|
]
|
||||||
|
++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
max_download_speed
|
systemd.tmpfiles.rules =
|
||||||
max_download_speed_per_torrent
|
let
|
||||||
|
plugins = pkgs.symlinkJoin {
|
||||||
|
name = "deluge-plugins";
|
||||||
|
paths = cfg.additionalPlugins;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
[
|
||||||
|
"L+ ${cfg.dataDir}/.config/deluge/plugins - - - - ${plugins}"
|
||||||
|
];
|
||||||
|
|
||||||
max_upload_slots_global
|
shb.nginx.vhosts = [
|
||||||
max_upload_slots_per_torrent
|
(
|
||||||
max_upload_speed
|
{
|
||||||
max_upload_speed_per_torrent
|
inherit (cfg) subdomain domain ssl;
|
||||||
|
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
|
||||||
dont_count_slow_torrents;
|
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
|
||||||
|
{
|
||||||
new_release_check = false;
|
domain = fqdn;
|
||||||
};
|
policy = "bypass";
|
||||||
|
resources = [
|
||||||
authFile = "${cfg.dataDir}/.config/deluge/authTemplate";
|
"^/json"
|
||||||
|
];
|
||||||
web.enable = true;
|
}
|
||||||
web.port = cfg.webPort;
|
{
|
||||||
};
|
domain = fqdn;
|
||||||
|
policy = "two_factor";
|
||||||
systemd.services.deluged.preStart = lib.mkBefore (lib.shb.replaceSecrets {
|
subject = [ "group:deluge_user" ];
|
||||||
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;
|
// (lib.optionalAttrs (cfg.authEndpoint != null) {
|
||||||
});
|
inherit (cfg) authEndpoint;
|
||||||
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}"
|
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
systemd.services.deluged.serviceConfig = cfg.extraServiceConfig;
|
||||||
|
}
|
||||||
|
(lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) {
|
||||||
|
services.prometheus.exporters.deluge = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
shb.nginx.vhosts = [
|
delugeHost = "127.0.0.1";
|
||||||
({
|
delugePort = config.services.deluge.config.daemon_port;
|
||||||
inherit (cfg) subdomain domain ssl;
|
delugeUser = "prometheus_scraper";
|
||||||
upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}";
|
delugePasswordFile = config.shb.deluge.prometheusScraperPassword.result.path;
|
||||||
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
|
exportPerTorrentMetrics = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.prometheus.scrapeConfigs = [
|
||||||
{
|
{
|
||||||
domain = fqdn;
|
job_name = "deluge";
|
||||||
policy = "bypass";
|
static_configs = [
|
||||||
resources = [
|
{
|
||||||
"^/json"
|
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;
|
|
||||||
};
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,60 @@
|
||||||
{ config, options, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.forgejo;
|
cfg = config.shb.forgejo;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
inherit (lib) all attrNames concatMapStringsSep getExe lists literalExpression mapAttrsToList mkBefore mkEnableOption mkForce mkIf mkMerge mkOption mkOverride nameValuePair optionalString optionals;
|
inherit (lib)
|
||||||
inherit (lib.types) attrsOf bool enum listOf nullOr package port submodule str;
|
all
|
||||||
|
attrNames
|
||||||
|
concatMapStringsSep
|
||||||
|
getExe
|
||||||
|
lists
|
||||||
|
literalExpression
|
||||||
|
mapAttrsToList
|
||||||
|
mkBefore
|
||||||
|
mkEnableOption
|
||||||
|
mkForce
|
||||||
|
mkIf
|
||||||
|
mkMerge
|
||||||
|
mkOption
|
||||||
|
mkOverride
|
||||||
|
nameValuePair
|
||||||
|
optionalString
|
||||||
|
optionals
|
||||||
|
;
|
||||||
|
inherit (lib.types)
|
||||||
|
attrsOf
|
||||||
|
bool
|
||||||
|
enum
|
||||||
|
listOf
|
||||||
|
nullOr
|
||||||
|
package
|
||||||
|
port
|
||||||
|
submodule
|
||||||
|
str
|
||||||
|
;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../blocks/nginx.nix
|
../blocks/nginx.nix
|
||||||
|
|
||||||
(lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''Instead, define an admin user in shb.forgejo.users and give it the same password, like so:
|
(lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''
|
||||||
shb.forgejo.users = {
|
Instead, define an admin user in shb.forgejo.users and give it the same password, like so:
|
||||||
"forgejoadmin" = {
|
shb.forgejo.users = {
|
||||||
isAdmin = true;
|
"forgejoadmin" = {
|
||||||
email = "forgejoadmin@example.com";
|
isAdmin = true;
|
||||||
password.result = <path/to/password>;
|
email = "forgejoadmin@example.com";
|
||||||
};
|
password.result = <path/to/password>;
|
||||||
};
|
};
|
||||||
|
};
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -60,7 +95,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
LDAP Integration.
|
LDAP Integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = nullOr (submodule {
|
type = nullOr (submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = mkEnableOption "LDAP integration.";
|
enable = mkEnableOption "LDAP integration.";
|
||||||
|
|
@ -125,7 +160,7 @@ in
|
||||||
|
|
||||||
waitForSystemdServices = mkOption {
|
waitForSystemdServices = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
default = [];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
List of systemd services to wait on before starting.
|
List of systemd services to wait on before starting.
|
||||||
This is needed because forgejo will try a lookup on the LDAP instance
|
This is needed because forgejo will try a lookup on the LDAP instance
|
||||||
|
|
@ -140,7 +175,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Setup SSO integration.
|
Setup SSO integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = mkEnableOption "SSO integration.";
|
enable = mkEnableOption "SSO integration.";
|
||||||
|
|
@ -164,7 +199,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
authorization_policy = mkOption {
|
authorization_policy = mkOption {
|
||||||
type = enum [ "one_factor" "two_factor" ];
|
type = enum [
|
||||||
|
"one_factor"
|
||||||
|
"two_factor"
|
||||||
|
];
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
description = "Require one factor (password) or two factor (device) authentication.";
|
||||||
default = "one_factor";
|
default = "one_factor";
|
||||||
};
|
};
|
||||||
|
|
@ -205,9 +243,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
email = mkOption {
|
email = mkOption {
|
||||||
description = ''Email of user.
|
description = ''
|
||||||
|
Email of user.
|
||||||
|
|
||||||
This is only set when the user is created, changing this later on will have no effect.
|
This is only set when the user is created, changing this later on will have no effect.
|
||||||
'';
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
};
|
};
|
||||||
|
|
@ -254,7 +293,6 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
hostPackages = mkOption {
|
hostPackages = mkOption {
|
||||||
type = listOf package;
|
type = listOf package;
|
||||||
default = with pkgs; [
|
default = with pkgs; [
|
||||||
|
|
@ -289,13 +327,14 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Backup configuration.
|
Backup configuration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = options.services.forgejo.user.value;
|
user = options.services.forgejo.user.value;
|
||||||
sourceDirectories = [
|
sourceDirectories = [
|
||||||
options.services.forgejo.dump.backupDir.value
|
options.services.forgejo.dump.backupDir.value
|
||||||
] ++ optionals (cfg.repositoryRoot != null) [
|
]
|
||||||
|
++ optionals (cfg.repositoryRoot != null) [
|
||||||
cfg.repositoryRoot
|
cfg.repositoryRoot
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
@ -317,7 +356,9 @@ in
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
default = { path = config.services.forgejo.stateDir; };
|
default = {
|
||||||
|
path = config.services.forgejo.stateDir;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
smtp = mkOption {
|
smtp = mkOption {
|
||||||
|
|
@ -390,10 +431,12 @@ in
|
||||||
# https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113
|
# https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113
|
||||||
systemd.services.forgejo.serviceConfig.Type = mkForce "exec";
|
systemd.services.forgejo.serviceConfig.Type = mkForce "exec";
|
||||||
|
|
||||||
shb.nginx.vhosts = [{
|
shb.nginx.vhosts = [
|
||||||
inherit (cfg) domain subdomain ssl;
|
{
|
||||||
upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}";
|
inherit (cfg) domain subdomain ssl;
|
||||||
}];
|
upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}";
|
||||||
|
}
|
||||||
|
];
|
||||||
})
|
})
|
||||||
|
|
||||||
(mkIf cfg.enable {
|
(mkIf cfg.enable {
|
||||||
|
|
@ -420,58 +463,60 @@ in
|
||||||
systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices;
|
systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices;
|
||||||
systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices;
|
systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices;
|
||||||
# The delimiter in the `cut` command is a TAB!
|
# The delimiter in the `cut` command is a TAB!
|
||||||
systemd.services.forgejo.preStart = let
|
systemd.services.forgejo.preStart =
|
||||||
provider = "SHB-${cfg.ldap.provider}";
|
let
|
||||||
in ''
|
provider = "SHB-${cfg.ldap.provider}";
|
||||||
auth="${getExe config.services.forgejo.package} admin auth"
|
in
|
||||||
|
''
|
||||||
|
auth="${getExe config.services.forgejo.package} admin auth"
|
||||||
|
|
||||||
echo "Trying to find existing ldap configuration for ${provider}"...
|
echo "Trying to find existing ldap configuration for ${provider}"...
|
||||||
set +e -o pipefail
|
set +e -o pipefail
|
||||||
id="$($auth list | grep "${provider}.*LDAP" | cut -d' ' -f1)"
|
id="$($auth list | grep "${provider}.*LDAP" | cut -d' ' -f1)"
|
||||||
found=$?
|
found=$?
|
||||||
set -e +o pipefail
|
set -e +o pipefail
|
||||||
|
|
||||||
if [[ $found = 0 ]]; then
|
if [[ $found = 0 ]]; then
|
||||||
echo Found ldap configuration at id=$id, updating it if needed.
|
echo Found ldap configuration at id=$id, updating it if needed.
|
||||||
$auth update-ldap \
|
$auth update-ldap \
|
||||||
--id $id \
|
--id $id \
|
||||||
--name ${provider} \
|
--name ${provider} \
|
||||||
--host ${cfg.ldap.host} \
|
--host ${cfg.ldap.host} \
|
||||||
--port ${toString cfg.ldap.port} \
|
--port ${toString cfg.ldap.port} \
|
||||||
--bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \
|
--bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \
|
||||||
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
|
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
|
||||||
--security-protocol Unencrypted \
|
--security-protocol Unencrypted \
|
||||||
--user-search-base ou=people,${cfg.ldap.dcdomain} \
|
--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)))' \
|
--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})' \
|
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
|
||||||
--username-attribute uid \
|
--username-attribute uid \
|
||||||
--firstname-attribute givenName \
|
--firstname-attribute givenName \
|
||||||
--surname-attribute sn \
|
--surname-attribute sn \
|
||||||
--email-attribute mail \
|
--email-attribute mail \
|
||||||
--avatar-attribute jpegPhoto \
|
--avatar-attribute jpegPhoto \
|
||||||
--synchronize-users
|
--synchronize-users
|
||||||
echo "Done updating LDAP configuration."
|
echo "Done updating LDAP configuration."
|
||||||
else
|
else
|
||||||
echo Did not find any ldap configuration, creating one with name ${provider}.
|
echo Did not find any ldap configuration, creating one with name ${provider}.
|
||||||
$auth add-ldap \
|
$auth add-ldap \
|
||||||
--name ${provider} \
|
--name ${provider} \
|
||||||
--host ${cfg.ldap.host} \
|
--host ${cfg.ldap.host} \
|
||||||
--port ${toString cfg.ldap.port} \
|
--port ${toString cfg.ldap.port} \
|
||||||
--bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \
|
--bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \
|
||||||
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
|
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
|
||||||
--security-protocol Unencrypted \
|
--security-protocol Unencrypted \
|
||||||
--user-search-base ou=people,${cfg.ldap.dcdomain} \
|
--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)))' \
|
--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})' \
|
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
|
||||||
--username-attribute uid \
|
--username-attribute uid \
|
||||||
--firstname-attribute givenName \
|
--firstname-attribute givenName \
|
||||||
--surname-attribute sn \
|
--surname-attribute sn \
|
||||||
--email-attribute mail \
|
--email-attribute mail \
|
||||||
--avatar-attribute jpegPhoto \
|
--avatar-attribute jpegPhoto \
|
||||||
--synchronize-users
|
--synchronize-users
|
||||||
echo "Done adding LDAP configuration."
|
echo "Done adding LDAP configuration."
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
})
|
})
|
||||||
|
|
||||||
# For Authelia to Forgejo integration: https://www.authelia.com/integration/openid-connect/gitea/
|
# For Authelia to Forgejo integration: https://www.authelia.com/integration/openid-connect/gitea/
|
||||||
|
|
@ -488,7 +533,7 @@ in
|
||||||
ENABLE_OPENID_SIGNUP = true;
|
ENABLE_OPENID_SIGNUP = true;
|
||||||
WHITELISTED_URIS = cfg.sso.endpoint;
|
WHITELISTED_URIS = cfg.sso.endpoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
service = {
|
service = {
|
||||||
# DISABLE_REGISTRATION = mkForce false;
|
# DISABLE_REGISTRATION = mkForce false;
|
||||||
# ALLOW_ONLY_EXTERNAL_REGISTRATION = false;
|
# ALLOW_ONLY_EXTERNAL_REGISTRATION = false;
|
||||||
|
|
@ -497,48 +542,53 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
# The delimiter in the `cut` command is a TAB!
|
# The delimiter in the `cut` command is a TAB!
|
||||||
systemd.services.forgejo.preStart = let
|
systemd.services.forgejo.preStart =
|
||||||
provider = "SHB-${cfg.sso.provider}";
|
let
|
||||||
in ''
|
provider = "SHB-${cfg.sso.provider}";
|
||||||
auth="${getExe config.services.forgejo.package} admin auth"
|
in
|
||||||
|
''
|
||||||
|
auth="${getExe config.services.forgejo.package} admin auth"
|
||||||
|
|
||||||
echo "Trying to find existing sso configuration for ${provider}"...
|
echo "Trying to find existing sso configuration for ${provider}"...
|
||||||
set +e -o pipefail
|
set +e -o pipefail
|
||||||
id="$($auth list | grep "${provider}.*OAuth2" | cut -d' ' -f1)"
|
id="$($auth list | grep "${provider}.*OAuth2" | cut -d' ' -f1)"
|
||||||
found=$?
|
found=$?
|
||||||
set -e +o pipefail
|
set -e +o pipefail
|
||||||
|
|
||||||
if [[ $found = 0 ]]; then
|
if [[ $found = 0 ]]; then
|
||||||
echo Found sso configuration at id=$id, updating it if needed.
|
echo Found sso configuration at id=$id, updating it if needed.
|
||||||
$auth update-oauth \
|
$auth update-oauth \
|
||||||
--id $id \
|
--id $id \
|
||||||
--name ${provider} \
|
--name ${provider} \
|
||||||
--provider openidConnect \
|
--provider openidConnect \
|
||||||
--key forgejo \
|
--key forgejo \
|
||||||
--secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \
|
--secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \
|
||||||
--auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration
|
--auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration
|
||||||
else
|
else
|
||||||
echo Did not find any sso configuration, creating one with name ${provider}.
|
echo Did not find any sso configuration, creating one with name ${provider}.
|
||||||
$auth add-oauth \
|
$auth add-oauth \
|
||||||
--name ${provider} \
|
--name ${provider} \
|
||||||
--provider openidConnect \
|
--provider openidConnect \
|
||||||
--key forgejo \
|
--key forgejo \
|
||||||
--secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \
|
--secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \
|
||||||
--auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration
|
--auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [
|
shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [
|
||||||
(let
|
(
|
||||||
provider = "SHB-${cfg.sso.provider}";
|
let
|
||||||
in {
|
provider = "SHB-${cfg.sso.provider}";
|
||||||
client_id = cfg.sso.clientID;
|
in
|
||||||
client_name = "Forgejo";
|
{
|
||||||
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
|
client_id = cfg.sso.clientID;
|
||||||
public = false;
|
client_name = "Forgejo";
|
||||||
authorization_policy = cfg.sso.authorization_policy;
|
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
|
||||||
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ];
|
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 = ''
|
systemd.services.forgejo.preStart = ''
|
||||||
admin="${getExe config.services.forgejo.package} admin user"
|
admin="${getExe config.services.forgejo.package} admin user"
|
||||||
'' + concatMapStringsSep "\n" (u: ''
|
''
|
||||||
|
+ concatMapStringsSep "\n" (u: ''
|
||||||
if ! $admin list | grep "${u.name}"; then
|
if ! $admin list | grep "${u.name}"; then
|
||||||
$admin create ${optionalString u.value.isAdmin "--admin"} --email "${u.value.email}" --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})"
|
$admin create ${optionalString u.value.isAdmin "--admin"} --email "${u.value.email}" --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})"
|
||||||
else
|
else
|
||||||
$admin change-password --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})"
|
$admin change-password --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})"
|
||||||
fi
|
fi
|
||||||
'') (mapAttrsToList nameValuePair cfg.users);
|
'') (mapAttrsToList nameValuePair cfg.users);
|
||||||
})
|
})
|
||||||
|
|
||||||
(mkIf (cfg.enable && cfg.smtp != null) {
|
(mkIf (cfg.enable && cfg.smtp != null) {
|
||||||
|
|
@ -584,15 +635,17 @@ in
|
||||||
instances.local = {
|
instances.local = {
|
||||||
enable = true;
|
enable = true;
|
||||||
name = "local";
|
name = "local";
|
||||||
url = let
|
url =
|
||||||
protocol = if cfg.ssl != null then "https" else "http";
|
let
|
||||||
in "${protocol}://${cfg.subdomain}.${cfg.domain}";
|
protocol = if cfg.ssl != null then "https" else "http";
|
||||||
|
in
|
||||||
|
"${protocol}://${cfg.subdomain}.${cfg.domain}";
|
||||||
tokenFile = ""; # Empty variable to satisfy an assertion.
|
tokenFile = ""; # Empty variable to satisfy an assertion.
|
||||||
labels = [
|
labels = [
|
||||||
# "ubuntu-latest:docker://node:16-bullseye"
|
# "ubuntu-latest:docker://node:16-bullseye"
|
||||||
# "ubuntu-22.04:docker://node:16-bullseye"
|
# "ubuntu-22.04:docker://node:16-bullseye"
|
||||||
# "ubuntu-20.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"
|
"native:host"
|
||||||
];
|
];
|
||||||
inherit (cfg) hostPackages;
|
inherit (cfg) hostPackages;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.grocy;
|
cfg = config.shb.grocy;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
|
|
@ -37,7 +42,24 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
culture = lib.mkOption {
|
culture = lib.mkOption {
|
||||||
type = lib.types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ];
|
type = lib.types.enum [
|
||||||
|
"de"
|
||||||
|
"en"
|
||||||
|
"da"
|
||||||
|
"en_GB"
|
||||||
|
"es"
|
||||||
|
"fr"
|
||||||
|
"hu"
|
||||||
|
"it"
|
||||||
|
"nl"
|
||||||
|
"no"
|
||||||
|
"pl"
|
||||||
|
"pt_BR"
|
||||||
|
"ru"
|
||||||
|
"sk_SK"
|
||||||
|
"sv_SE"
|
||||||
|
"tr"
|
||||||
|
];
|
||||||
default = "en";
|
default = "en";
|
||||||
description = ''
|
description = ''
|
||||||
Display language of the frontend.
|
Display language of the frontend.
|
||||||
|
|
@ -53,12 +75,12 @@ in
|
||||||
extraServiceConfig = lib.mkOption {
|
extraServiceConfig = lib.mkOption {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = lib.types.attrsOf lib.types.str;
|
||||||
description = "Extra configuration given to the systemd service file.";
|
description = "Extra configuration given to the systemd service file.";
|
||||||
default = {};
|
default = { };
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
{
|
{
|
||||||
MemoryHigh = "512M";
|
MemoryHigh = "512M";
|
||||||
MemoryMax = "900M";
|
MemoryMax = "900M";
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -78,34 +100,47 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
logLevel = lib.mkOption {
|
logLevel = lib.mkOption {
|
||||||
type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]);
|
type = lib.types.nullOr (
|
||||||
|
lib.types.enum [
|
||||||
|
"critical"
|
||||||
|
"error"
|
||||||
|
"warning"
|
||||||
|
"info"
|
||||||
|
"debug"
|
||||||
|
]
|
||||||
|
);
|
||||||
description = "Enable logging.";
|
description = "Enable logging.";
|
||||||
default = false;
|
default = false;
|
||||||
example = true;
|
example = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (lib.mkMerge [{
|
config = lib.mkIf cfg.enable (
|
||||||
services.grocy = {
|
lib.mkMerge [
|
||||||
enable = true;
|
{
|
||||||
hostName = fqdn;
|
services.grocy = {
|
||||||
nginx.enableSSL = !(isNull cfg.ssl);
|
enable = true;
|
||||||
dataDir = cfg.dataDir;
|
hostName = fqdn;
|
||||||
settings.currency = cfg.currency;
|
nginx.enableSSL = !(isNull cfg.ssl);
|
||||||
settings.culture = cfg.culture;
|
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.groups.grocy = { };
|
||||||
users.users.grocy.group = lib.mkForce "grocy";
|
users.users.grocy.group = lib.mkForce "grocy";
|
||||||
|
|
||||||
services.nginx.virtualHosts."${fqdn}" = {
|
services.nginx.virtualHosts."${fqdn}" = {
|
||||||
enableACME = lib.mkForce false;
|
enableACME = lib.mkForce false;
|
||||||
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
|
||||||
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
|
||||||
};
|
};
|
||||||
} {
|
}
|
||||||
systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig;
|
{
|
||||||
}]);
|
systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.hledger;
|
cfg = config.shb.hledger;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
in
|
in
|
||||||
|
|
@ -63,7 +68,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Backup configuration.
|
Backup configuration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "hledger";
|
user = "hledger";
|
||||||
|
|
@ -76,7 +81,7 @@ in
|
||||||
|
|
||||||
extraArguments = lib.mkOption {
|
extraArguments = lib.mkOption {
|
||||||
description = "Extra arguments append to the hledger command.";
|
description = "Extra arguments append to the hledger command.";
|
||||||
default = ["--forecast"];
|
default = [ "--forecast" ];
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -88,7 +93,7 @@ in
|
||||||
baseUrl = "";
|
baseUrl = "";
|
||||||
|
|
||||||
stateDir = cfg.dataDir;
|
stateDir = cfg.dataDir;
|
||||||
journalFiles = ["hledger.journal"];
|
journalFiles = [ "hledger.journal" ];
|
||||||
|
|
||||||
host = "127.0.0.1";
|
host = "127.0.0.1";
|
||||||
port = cfg.port;
|
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
|
# 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..
|
# empty one if it does not exist yet..
|
||||||
preStart = ''
|
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";
|
serviceConfig.StateDirectory = "hledger";
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [
|
shb.nginx.vhosts = [
|
||||||
{
|
{
|
||||||
inherit (cfg) subdomain domain authEndpoint ssl;
|
inherit (cfg)
|
||||||
|
subdomain
|
||||||
|
domain
|
||||||
|
authEndpoint
|
||||||
|
ssl
|
||||||
|
;
|
||||||
upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}";
|
upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}";
|
||||||
autheliaRules = [{
|
autheliaRules = [
|
||||||
domain = fqdn;
|
{
|
||||||
policy = "two_factor";
|
domain = fqdn;
|
||||||
subject = ["group:hledger_user"];
|
policy = "two_factor";
|
||||||
}];
|
subject = [ "group:hledger_user" ];
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.home-assistant;
|
cfg = config.shb.home-assistant;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
|
|
@ -24,9 +29,7 @@ let
|
||||||
|
|
||||||
nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config);
|
nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config);
|
||||||
|
|
||||||
configWithSecretsIncludes =
|
configWithSecretsIncludes = nonSecrets // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
|
||||||
nonSecrets
|
|
||||||
// (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.home-assistant = {
|
options.shb.home-assistant = {
|
||||||
|
|
@ -56,28 +59,49 @@ in
|
||||||
freeformType = lib.types.attrsOf lib.types.str;
|
freeformType = lib.types.attrsOf lib.types.str;
|
||||||
options = {
|
options = {
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
|
type = lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
lib.shb.secretFileType
|
||||||
|
];
|
||||||
description = "Name of the Home Assistant instance.";
|
description = "Name of the Home Assistant instance.";
|
||||||
};
|
};
|
||||||
country = lib.mkOption {
|
country = lib.mkOption {
|
||||||
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
|
type = lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
lib.shb.secretFileType
|
||||||
|
];
|
||||||
description = "Two letter country code where this instance is located.";
|
description = "Two letter country code where this instance is located.";
|
||||||
};
|
};
|
||||||
latitude = lib.mkOption {
|
latitude = lib.mkOption {
|
||||||
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
|
type = lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
lib.shb.secretFileType
|
||||||
|
];
|
||||||
description = "Latitude where this instance is located.";
|
description = "Latitude where this instance is located.";
|
||||||
};
|
};
|
||||||
longitude = lib.mkOption {
|
longitude = lib.mkOption {
|
||||||
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
|
type = lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
lib.shb.secretFileType
|
||||||
|
];
|
||||||
description = "Longitude where this instance is located.";
|
description = "Longitude where this instance is located.";
|
||||||
};
|
};
|
||||||
time_zone = lib.mkOption {
|
time_zone = lib.mkOption {
|
||||||
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
|
type = lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
lib.shb.secretFileType
|
||||||
|
];
|
||||||
description = "Timezone of this instance.";
|
description = "Timezone of this instance.";
|
||||||
example = "America/Los_Angeles";
|
example = "America/Los_Angeles";
|
||||||
};
|
};
|
||||||
unit_system = lib.mkOption {
|
unit_system = lib.mkOption {
|
||||||
type = lib.types.oneOf [ lib.types.str (lib.types.enum [ "metric" "us_customary" ]) ];
|
type = lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
(lib.types.enum [
|
||||||
|
"metric"
|
||||||
|
"us_customary"
|
||||||
|
])
|
||||||
|
];
|
||||||
description = "Unit system of this instance.";
|
description = "Unit system of this instance.";
|
||||||
example = "metric";
|
example = "metric";
|
||||||
};
|
};
|
||||||
|
|
@ -95,7 +119,7 @@ in
|
||||||
Also, enabling LDAP will skip onboarding
|
Also, enabling LDAP will skip onboarding
|
||||||
otherwise Home Assistant gets into a cyclic lock.
|
otherwise Home Assistant gets into a cyclic lock.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "LDAP app.";
|
enable = lib.mkEnableOption "LDAP app.";
|
||||||
|
|
@ -140,7 +164,7 @@ in
|
||||||
|
|
||||||
voice = lib.mkOption {
|
voice = lib.mkOption {
|
||||||
description = "Options related to voice service.";
|
description = "Options related to voice service.";
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
speech-to-text = lib.mkOption {
|
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
|
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;
|
type = lib.types.attrsOf lib.types.anything;
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
text-to-speech = lib.mkOption {
|
text-to-speech = lib.mkOption {
|
||||||
description = ''
|
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
|
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;
|
type = lib.types.attrsOf lib.types.anything;
|
||||||
default = {};
|
default = { };
|
||||||
};
|
};
|
||||||
wakeword = lib.mkOption {
|
wakeword = lib.mkOption {
|
||||||
description = ''
|
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
|
https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.openwakeword
|
||||||
'';
|
'';
|
||||||
type = lib.types.anything;
|
type = lib.types.anything;
|
||||||
default = { enable = false; };
|
default = {
|
||||||
|
enable = false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -178,7 +204,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Backup configuration.
|
Backup configuration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "hass";
|
user = "hass";
|
||||||
|
|
@ -219,7 +245,7 @@ in
|
||||||
config = {
|
config = {
|
||||||
# Includes dependencies for a basic setup
|
# Includes dependencies for a basic setup
|
||||||
# https://www.home-assistant.io/integrations/default_config/
|
# https://www.home-assistant.io/integrations/default_config/
|
||||||
default_config = {};
|
default_config = { };
|
||||||
http = {
|
http = {
|
||||||
use_x_forwarded_for = true;
|
use_x_forwarded_for = true;
|
||||||
server_host = "127.0.0.1";
|
server_host = "127.0.0.1";
|
||||||
|
|
@ -240,7 +266,10 @@ in
|
||||||
{
|
{
|
||||||
type = "command_line";
|
type = "command_line";
|
||||||
command = ldap_auth_script + "/bin/ldap_auth.sh";
|
command = ldap_auth_script + "/bin/ldap_auth.sh";
|
||||||
args = [ "http://${cfg.ldap.host}:${toString cfg.ldap.port}" cfg.ldap.userGroup ];
|
args = [
|
||||||
|
"http://${cfg.ldap.host}:${toString cfg.ldap.port}"
|
||||||
|
cfg.ldap.userGroup
|
||||||
|
];
|
||||||
meta = true;
|
meta = true;
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
@ -261,11 +290,11 @@ in
|
||||||
action = [
|
action = [
|
||||||
{
|
{
|
||||||
service = "shell_command.delete_backups";
|
service = "shell_command.delete_backups";
|
||||||
data = {};
|
data = { };
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
service = "backup.create";
|
service = "backup.create";
|
||||||
data = {};
|
data = { };
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
mode = "single";
|
mode = "single";
|
||||||
|
|
@ -286,7 +315,11 @@ in
|
||||||
{
|
{
|
||||||
name = "random_joke";
|
name = "random_joke";
|
||||||
platform = "rest";
|
platform = "rest";
|
||||||
json_attributes = ["joke" "id" "status"];
|
json_attributes = [
|
||||||
|
"joke"
|
||||||
|
"id"
|
||||||
|
"status"
|
||||||
|
];
|
||||||
value_template = "{{ value_json.joke }}";
|
value_template = "{{ value_json.joke }}";
|
||||||
resource = "https://icanhazdadjoke.com/";
|
resource = "https://icanhazdadjoke.com/";
|
||||||
scan_interval = "3600";
|
scan_interval = "3600";
|
||||||
|
|
@ -324,33 +357,36 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.home-assistant.preStart =
|
systemd.services.home-assistant.preStart =
|
||||||
(let
|
(
|
||||||
# TODO: this probably does not work anymore
|
let
|
||||||
onboarding = pkgs.writeText "onboarding" ''
|
# TODO: this probably does not work anymore
|
||||||
{
|
onboarding = pkgs.writeText "onboarding" ''
|
||||||
"version": 4,
|
{
|
||||||
"minor_version": 1,
|
"version": 4,
|
||||||
"key": "onboarding",
|
"minor_version": 1,
|
||||||
"data": {
|
"key": "onboarding",
|
||||||
"done": [
|
"data": {
|
||||||
${lib.optionalString cfg.ldap.enable ''"user",''}
|
"done": [
|
||||||
"core_config",
|
${lib.optionalString cfg.ldap.enable ''"user",''}
|
||||||
"analytics"
|
"core_config",
|
||||||
]
|
"analytics"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
'';
|
||||||
'';
|
storage = "${config.services.home-assistant.configDir}";
|
||||||
storage = "${config.services.home-assistant.configDir}";
|
file = "${storage}/.storage/onboarding";
|
||||||
file = "${storage}/.storage/onboarding";
|
in
|
||||||
in ''
|
''
|
||||||
if [ ! -f ${file} ]; then
|
if [ ! -f ${file} ]; then
|
||||||
mkdir -p ''$(dirname ${file}) && cp ${onboarding} ${file}
|
mkdir -p ''$(dirname ${file}) && cp ${onboarding} ${file}
|
||||||
fi
|
fi
|
||||||
'')
|
''
|
||||||
|
)
|
||||||
+ (lib.shb.replaceSecrets {
|
+ (lib.shb.replaceSecrets {
|
||||||
userConfig = cfg.config;
|
userConfig = cfg.config;
|
||||||
resultPath = "${config.services.home-assistant.configDir}/secrets.yaml";
|
resultPath = "${config.services.home-assistant.configDir}/secrets.yaml";
|
||||||
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML {});
|
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { });
|
||||||
});
|
});
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,45 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.immich;
|
cfg = config.shb.immich;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
protocol = if !(isNull cfg.ssl) then "https" else "http";
|
protocol = if !(isNull cfg.ssl) then "https" else "http";
|
||||||
|
|
||||||
roleClaim = "immich_user";
|
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";
|
#quotaClaim = "immich_quota";
|
||||||
scopes = [ "openid" "email" "profile" "groups" "immich_scope"];
|
scopes = [
|
||||||
|
"openid"
|
||||||
|
"email"
|
||||||
|
"profile"
|
||||||
|
"groups"
|
||||||
|
"immich_scope"
|
||||||
|
];
|
||||||
|
|
||||||
dataFolder = cfg.mediaLocation;
|
dataFolder = cfg.mediaLocation;
|
||||||
ssoFqdnWithPort = if isNull cfg.sso.port
|
ssoFqdnWithPort =
|
||||||
then cfg.sso.endpoint
|
if isNull cfg.sso.port then cfg.sso.endpoint else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
|
||||||
else "${cfg.sso.endpoint}:${toString cfg.sso.port}";
|
|
||||||
# Generate Immich configuration file only for SHB-managed settings
|
# Generate Immich configuration file only for SHB-managed settings
|
||||||
shbManagedSettings = lib.optionalAttrs (cfg.settings != {}) cfg.settings
|
shbManagedSettings =
|
||||||
|
lib.optionalAttrs (cfg.settings != { }) cfg.settings
|
||||||
// lib.optionalAttrs (cfg.sso.enable) {
|
// lib.optionalAttrs (cfg.sso.enable) {
|
||||||
oauth = {
|
oauth = {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
issuerUrl = "${ssoFqdnWithPort}";
|
issuerUrl = "${ssoFqdnWithPort}";
|
||||||
clientId = cfg.sso.clientID;
|
clientId = cfg.sso.clientID;
|
||||||
roleClaim = roleClaim;
|
roleClaim = roleClaim;
|
||||||
clientSecret = { source = cfg.sso.sharedSecret.result.path; };
|
clientSecret = {
|
||||||
|
source = cfg.sso.sharedSecret.result.path;
|
||||||
|
};
|
||||||
scope = builtins.concatStringsSep " " scopes;
|
scope = builtins.concatStringsSep " " scopes;
|
||||||
storageLabelClaim = cfg.sso.storageLabelClaim;
|
storageLabelClaim = cfg.sso.storageLabelClaim;
|
||||||
#storageQuotaClaim = quotaClaim; # TODO (commented out, otherwise defaults to 0 bytes!)
|
#storageQuotaClaim = quotaClaim; # TODO (commented out, otherwise defaults to 0 bytes!)
|
||||||
|
|
@ -49,7 +62,9 @@ let
|
||||||
host = cfg.smtp.host;
|
host = cfg.smtp.host;
|
||||||
port = cfg.smtp.port;
|
port = cfg.smtp.port;
|
||||||
username = cfg.smtp.username;
|
username = cfg.smtp.username;
|
||||||
password = { source = cfg.smtp.password.result.path; };
|
password = {
|
||||||
|
source = cfg.smtp.password.result.path;
|
||||||
|
};
|
||||||
ignoreTLS = cfg.smtp.ignoreTLS;
|
ignoreTLS = cfg.smtp.ignoreTLS;
|
||||||
secure = cfg.smtp.secure;
|
secure = cfg.smtp.secure;
|
||||||
};
|
};
|
||||||
|
|
@ -58,19 +73,36 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
configFile = "/var/lib/immich/config.json";
|
configFile = "/var/lib/immich/config.json";
|
||||||
|
|
||||||
# Use SHB's replaceSecrets function for loading secrets at runtime
|
# Use SHB's replaceSecrets function for loading secrets at runtime
|
||||||
configSetupScript = lib.optionalString (cfg.sso.enable || cfg.smtp != null) (
|
configSetupScript = lib.optionalString (cfg.sso.enable || cfg.smtp != null) (
|
||||||
lib.shb.replaceSecrets {
|
lib.shb.replaceSecrets {
|
||||||
userConfig = shbManagedSettings;
|
userConfig = shbManagedSettings;
|
||||||
resultPath = configFile;
|
resultPath = configFile;
|
||||||
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json {});
|
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json { });
|
||||||
user = "immich";
|
user = "immich";
|
||||||
permissions = "u=r,g=,o=";
|
permissions = "u=r,g=,o=";
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
inherit (lib) mkEnableOption mkIf lists mkOption optionals;
|
inherit (lib)
|
||||||
inherit (lib.types) attrs attrsOf bool enum listOf nullOr port submodule str path;
|
mkEnableOption
|
||||||
|
mkIf
|
||||||
|
lists
|
||||||
|
mkOption
|
||||||
|
optionals
|
||||||
|
;
|
||||||
|
inherit (lib.types)
|
||||||
|
attrs
|
||||||
|
attrsOf
|
||||||
|
bool
|
||||||
|
enum
|
||||||
|
listOf
|
||||||
|
nullOr
|
||||||
|
port
|
||||||
|
submodule
|
||||||
|
str
|
||||||
|
path
|
||||||
|
;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -154,14 +186,16 @@ in
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
default = { path = dataFolder; };
|
default = {
|
||||||
|
path = dataFolder;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
backup = mkOption {
|
backup = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Backup configuration for Immich media files and database.
|
Backup configuration for Immich media files and database.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "immich";
|
user = "immich";
|
||||||
|
|
@ -190,7 +224,7 @@ in
|
||||||
|
|
||||||
machineLearning = mkOption {
|
machineLearning = mkOption {
|
||||||
description = "Machine learning configuration.";
|
description = "Machine learning configuration.";
|
||||||
default = {};
|
default = { };
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = mkOption {
|
enable = mkOption {
|
||||||
|
|
@ -202,7 +236,7 @@ in
|
||||||
environment = mkOption {
|
environment = mkOption {
|
||||||
description = "Extra environment variables for machine learning service.";
|
description = "Extra environment variables for machine learning service.";
|
||||||
type = attrsOf str;
|
type = attrsOf str;
|
||||||
default = {};
|
default = { };
|
||||||
example = {
|
example = {
|
||||||
MACHINE_LEARNING_WORKERS = "2";
|
MACHINE_LEARNING_WORKERS = "2";
|
||||||
MACHINE_LEARNING_WORKER_TIMEOUT = "180";
|
MACHINE_LEARNING_WORKER_TIMEOUT = "180";
|
||||||
|
|
@ -216,13 +250,17 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Setup SSO integration.
|
Setup SSO integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = mkEnableOption "SSO integration.";
|
enable = mkEnableOption "SSO integration.";
|
||||||
|
|
||||||
provider = mkOption {
|
provider = mkOption {
|
||||||
type = enum [ "Authelia" "Keycloak" "Generic" ];
|
type = enum [
|
||||||
|
"Authelia"
|
||||||
|
"Keycloak"
|
||||||
|
"Generic"
|
||||||
|
];
|
||||||
description = "OIDC provider name, used for display.";
|
description = "OIDC provider name, used for display.";
|
||||||
default = "Authelia";
|
default = "Authelia";
|
||||||
};
|
};
|
||||||
|
|
@ -311,7 +349,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
authorization_policy = mkOption {
|
authorization_policy = mkOption {
|
||||||
type = enum [ "one_factor" "two_factor" ];
|
type = enum [
|
||||||
|
"one_factor"
|
||||||
|
"two_factor"
|
||||||
|
];
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
description = "Require one factor (password) or two factor (device) authentication.";
|
||||||
default = "one_factor";
|
default = "one_factor";
|
||||||
};
|
};
|
||||||
|
|
@ -325,10 +366,10 @@ in
|
||||||
Immich configuration settings.
|
Immich configuration settings.
|
||||||
Only specify settings that you want SHB to manage declaratively.
|
Only specify settings that you want SHB to manage declaratively.
|
||||||
Other settings can be configured through Immich's admin UI.
|
Other settings can be configured through Immich's admin UI.
|
||||||
|
|
||||||
See https://immich.app/docs/install/config-file/ for available options.
|
See https://immich.app/docs/install/config-file/ for available options.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
example = {
|
example = {
|
||||||
ffmpeg.crf = 23;
|
ffmpeg.crf = 23;
|
||||||
job.backgroundTask.concurrency = 5;
|
job.backgroundTask.concurrency = 5;
|
||||||
|
|
@ -351,31 +392,31 @@ in
|
||||||
description = "SMTP address from which the emails originate.";
|
description = "SMTP address from which the emails originate.";
|
||||||
example = "noreply@example.com";
|
example = "noreply@example.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
replyTo = mkOption {
|
replyTo = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Reply-to address for emails.";
|
description = "Reply-to address for emails.";
|
||||||
example = "support@example.com";
|
example = "support@example.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
host = mkOption {
|
host = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "SMTP host to send the emails to.";
|
description = "SMTP host to send the emails to.";
|
||||||
example = "smtp.example.com";
|
example = "smtp.example.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = port;
|
type = port;
|
||||||
description = "SMTP port to send the emails to.";
|
description = "SMTP port to send the emails to.";
|
||||||
default = 587;
|
default = 587;
|
||||||
};
|
};
|
||||||
|
|
||||||
username = mkOption {
|
username = mkOption {
|
||||||
type = str;
|
type = str;
|
||||||
description = "Username to connect to the SMTP host.";
|
description = "Username to connect to the SMTP host.";
|
||||||
example = "smtp-user";
|
example = "smtp-user";
|
||||||
};
|
};
|
||||||
|
|
||||||
password = mkOption {
|
password = mkOption {
|
||||||
description = "File containing the password to connect to the SMTP host.";
|
description = "File containing the password to connect to the SMTP host.";
|
||||||
type = submodule {
|
type = submodule {
|
||||||
|
|
@ -386,13 +427,13 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
ignoreTLS = mkOption {
|
ignoreTLS = mkOption {
|
||||||
type = bool;
|
type = bool;
|
||||||
description = "Ignore TLS certificate errors.";
|
description = "Ignore TLS certificate errors.";
|
||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
secure = mkOption {
|
secure = mkOption {
|
||||||
type = bool;
|
type = bool;
|
||||||
description = "Use secure connection (SSL/TLS).";
|
description = "Use secure connection (SSL/TLS).";
|
||||||
|
|
@ -428,7 +469,7 @@ in
|
||||||
host = "127.0.0.1";
|
host = "127.0.0.1";
|
||||||
port = cfg.port;
|
port = cfg.port;
|
||||||
mediaLocation = cfg.mediaLocation;
|
mediaLocation = cfg.mediaLocation;
|
||||||
|
|
||||||
# Hardware acceleration configuration
|
# Hardware acceleration configuration
|
||||||
accelerationDevices = cfg.accelerationDevices;
|
accelerationDevices = cfg.accelerationDevices;
|
||||||
|
|
||||||
|
|
@ -436,7 +477,7 @@ in
|
||||||
|
|
||||||
# Database configuration
|
# Database configuration
|
||||||
database = {
|
database = {
|
||||||
# Disable pgvecto.rs, as it was deprecated before SHB integration
|
# Disable pgvecto.rs, as it was deprecated before SHB integration
|
||||||
enableVectors = false;
|
enableVectors = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -452,9 +493,11 @@ in
|
||||||
REDIS_HOSTNAME = "127.0.0.1";
|
REDIS_HOSTNAME = "127.0.0.1";
|
||||||
REDIS_PORT = "6379";
|
REDIS_PORT = "6379";
|
||||||
REDIS_DBINDEX = "0";
|
REDIS_DBINDEX = "0";
|
||||||
} // lib.optionalAttrs (cfg.jwtSecretFile != null) {
|
}
|
||||||
|
// lib.optionalAttrs (cfg.jwtSecretFile != null) {
|
||||||
JWT_SECRET_FILE = cfg.jwtSecretFile.result.path;
|
JWT_SECRET_FILE = cfg.jwtSecretFile.result.path;
|
||||||
} // lib.optionalAttrs (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) {
|
}
|
||||||
|
// lib.optionalAttrs (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) {
|
||||||
IMMICH_CONFIG_FILE = configFile;
|
IMMICH_CONFIG_FILE = configFile;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -464,27 +507,32 @@ in
|
||||||
"d /var/lib/immich 0700 immich immich"
|
"d /var/lib/immich 0700 immich immich"
|
||||||
];
|
];
|
||||||
|
|
||||||
# Configuration setup service - generates config only for SHB-managed settings
|
# Configuration setup service - generates config only for SHB-managed settings
|
||||||
systemd.services.immich-setup-config = mkIf (cfg.enable && (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null)) {
|
systemd.services.immich-setup-config =
|
||||||
description = "Setup Immich configuration for SHB-managed settings";
|
mkIf (cfg.enable && (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null))
|
||||||
wantedBy = [ "multi-user.target" ];
|
{
|
||||||
before = [ "immich-server.service" ];
|
description = "Setup Immich configuration for SHB-managed settings";
|
||||||
after = [ "network.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
serviceConfig = {
|
before = [ "immich-server.service" ];
|
||||||
Type = "oneshot";
|
after = [ "network.target" ];
|
||||||
User = "immich";
|
serviceConfig = {
|
||||||
Group = "immich";
|
Type = "oneshot";
|
||||||
};
|
User = "immich";
|
||||||
script = ''
|
Group = "immich";
|
||||||
mkdir -p ${dataFolder}
|
};
|
||||||
|
script = ''
|
||||||
# Generate config file with only SHB-managed settings
|
mkdir -p ${dataFolder}
|
||||||
${configSetupScript}
|
|
||||||
'';
|
# Generate config file with only SHB-managed settings
|
||||||
};
|
${configSetupScript}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
# Add immich user to video and render groups for hardware acceleration
|
# Add immich user to video and render groups for hardware acceleration
|
||||||
users.users.immich.extraGroups = optionals (cfg.accelerationDevices != []) [ "video" "render" ];
|
users.users.immich.extraGroups = optionals (cfg.accelerationDevices != [ ]) [
|
||||||
|
"video"
|
||||||
|
"render"
|
||||||
|
];
|
||||||
|
|
||||||
# PostgreSQL extensions are automatically handled by the Immich service
|
# PostgreSQL extensions are automatically handled by the Immich service
|
||||||
|
|
||||||
|
|
@ -499,7 +547,10 @@ in
|
||||||
{
|
{
|
||||||
domain = fqdn;
|
domain = fqdn;
|
||||||
policy = cfg.sso.authorization_policy;
|
policy = cfg.sso.authorization_policy;
|
||||||
subject = ["group:immich_user" "group:immich_admin"];
|
subject = [
|
||||||
|
"group:immich_user"
|
||||||
|
"group:immich_admin"
|
||||||
|
];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
authEndpoint = lib.mkIf (cfg.sso.enable) cfg.sso.endpoint;
|
authEndpoint = lib.mkIf (cfg.sso.enable) cfg.sso.endpoint;
|
||||||
|
|
@ -515,10 +566,20 @@ in
|
||||||
|
|
||||||
# Ensure services start in correct order
|
# Ensure services start in correct order
|
||||||
systemd.services.immich-server = {
|
systemd.services.immich-server = {
|
||||||
after = [ "postgresql.service" "redis-immich.service" ]
|
after = [
|
||||||
++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ];
|
"postgresql.service"
|
||||||
requires = [ "postgresql.service" "redis-immich.service" ]
|
"redis-immich.service"
|
||||||
++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ];
|
]
|
||||||
|
++ optionals (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) [
|
||||||
|
"immich-setup-config.service"
|
||||||
|
];
|
||||||
|
requires = [
|
||||||
|
"postgresql.service"
|
||||||
|
"redis-immich.service"
|
||||||
|
]
|
||||||
|
++ optionals (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) [
|
||||||
|
"immich-setup-config.service"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.immich-machine-learning = mkIf cfg.machineLearning.enable {
|
systemd.services.immich-machine-learning = mkIf cfg.machineLearning.enable {
|
||||||
|
|
@ -527,21 +588,21 @@ in
|
||||||
|
|
||||||
# Authelia integration for SSO
|
# Authelia integration for SSO
|
||||||
shb.authelia.extraDefinitions = {
|
shb.authelia.extraDefinitions = {
|
||||||
# Immich expects all users that get a token to be granted access. So users can either be part of the
|
# Immich expects all users that get a token to be granted access. So users can either be part of the
|
||||||
# "admin" group or the "user" group. Users that are not part of either should be blocked by
|
# "admin" group or the "user" group. Users that are not part of either should be blocked by
|
||||||
# the ID provider (Authelia).
|
# the ID provider (Authelia).
|
||||||
user_attributes.${roleClaim}.expression = ''"${cfg.sso.adminUserGroup}" in groups ? "admin" : "user"'';
|
user_attributes.${roleClaim}.expression =
|
||||||
|
''"${cfg.sso.adminUserGroup}" in groups ? "admin" : "user"'';
|
||||||
};
|
};
|
||||||
shb.authelia.extraOidcClaimsPolicies.immich_policy = {
|
shb.authelia.extraOidcClaimsPolicies.immich_policy = {
|
||||||
custom_claims = {
|
custom_claims = {
|
||||||
${roleClaim} = {};
|
${roleClaim} = { };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
shb.authelia.extraOidcScopes.immich_scope = {
|
shb.authelia.extraOidcScopes.immich_scope = {
|
||||||
claims = [ roleClaim ];
|
claims = [ roleClaim ];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
shb.authelia.oidcClients = lists.optionals (cfg.sso.enable && cfg.sso.provider == "Authelia") [
|
shb.authelia.oidcClients = lists.optionals (cfg.sso.enable && cfg.sso.provider == "Authelia") [
|
||||||
{
|
{
|
||||||
client_id = cfg.sso.clientID;
|
client_id = cfg.sso.clientID;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
{ config, lib, pkgs, ...}:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
|
|
||||||
cfg = config.shb.jellyfin;
|
cfg = config.shb.jellyfin;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
|
|
@ -91,31 +96,33 @@ in
|
||||||
admin = lib.mkOption {
|
admin = lib.mkOption {
|
||||||
description = "Default admin user info. Only needed if LDAP or SSO is not configured.";
|
description = "Default admin user info. Only needed if LDAP or SSO is not configured.";
|
||||||
default = null;
|
default = null;
|
||||||
type = types.nullOr (types.submodule {
|
type = types.nullOr (
|
||||||
options = {
|
types.submodule {
|
||||||
username = lib.mkOption {
|
options = {
|
||||||
description = "Username of the default admin user.";
|
username = lib.mkOption {
|
||||||
type = types.str;
|
description = "Username of the default admin user.";
|
||||||
default = "jellyfin";
|
type = types.str;
|
||||||
};
|
default = "jellyfin";
|
||||||
password = lib.mkOption {
|
};
|
||||||
description = "Password of the default admin user.";
|
password = lib.mkOption {
|
||||||
type = types.submodule {
|
description = "Password of the default admin user.";
|
||||||
options = contracts.secret.mkRequester {
|
type = types.submodule {
|
||||||
mode = "0440";
|
options = contracts.secret.mkRequester {
|
||||||
owner = "jellyfin";
|
mode = "0440";
|
||||||
group = "jellyfin";
|
owner = "jellyfin";
|
||||||
restartUnits = [ "jellyfin.service" ];
|
group = "jellyfin";
|
||||||
|
restartUnits = [ "jellyfin.service" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ldap = lib.mkOption {
|
ldap = lib.mkOption {
|
||||||
description = "LDAP configuration.";
|
description = "LDAP configuration.";
|
||||||
default = {};
|
default = { };
|
||||||
type = types.submodule {
|
type = types.submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "LDAP";
|
enable = lib.mkEnableOption "LDAP";
|
||||||
|
|
@ -167,7 +174,7 @@ in
|
||||||
|
|
||||||
sso = lib.mkOption {
|
sso = lib.mkOption {
|
||||||
description = "SSO configuration.";
|
description = "SSO configuration.";
|
||||||
default = {};
|
default = { };
|
||||||
type = types.submodule {
|
type = types.submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "SSO";
|
enable = lib.mkEnableOption "SSO";
|
||||||
|
|
@ -203,7 +210,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
authorization_policy = lib.mkOption {
|
authorization_policy = lib.mkOption {
|
||||||
type = types.enum [ "one_factor" "two_factor" ];
|
type = types.enum [
|
||||||
|
"one_factor"
|
||||||
|
"two_factor"
|
||||||
|
];
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
description = "Require one factor (password) or two factor (device) authentication.";
|
||||||
default = "one_factor";
|
default = "one_factor";
|
||||||
};
|
};
|
||||||
|
|
@ -238,23 +248,27 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Backup configuration.
|
Backup configuration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = types.submodule {
|
type = types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "jellyfin";
|
user = "jellyfin";
|
||||||
sourceDirectories = [
|
sourceDirectories = [
|
||||||
config.services.jellyfin.dataDir
|
config.services.jellyfin.dataDir
|
||||||
];
|
];
|
||||||
sourceDirectoriesText = ''[
|
sourceDirectoriesText = ''
|
||||||
"services.jellyfin.dataDir"
|
[
|
||||||
]'';
|
"services.jellyfin.dataDir"
|
||||||
|
]'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
(lib.mkRenamedOptionModule [ "shb" "jellyfin" "adminPassword" ] [ "shb" "jellyfin" "admin" "password" ])
|
(lib.mkRenamedOptionModule
|
||||||
|
[ "shb" "jellyfin" "adminPassword" ]
|
||||||
|
[ "shb" "jellyfin" "admin" "password" ]
|
||||||
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -269,7 +283,10 @@ in
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
# from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery
|
# from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery
|
||||||
allowedUDPPorts = [ 1900 7359 ];
|
allowedUDPPorts = [
|
||||||
|
1900
|
||||||
|
7359
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.nginx.enable = true;
|
services.nginx.enable = true;
|
||||||
|
|
@ -380,21 +397,23 @@ in
|
||||||
proxy_set_header X-Forwarded-Protocol $scheme;
|
proxy_set_header X-Forwarded-Protocol $scheme;
|
||||||
proxy_set_header X-Forwarded-Host $http_host;
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
services.prometheus.scrapeConfigs = [{
|
services.prometheus.scrapeConfigs = [
|
||||||
job_name = "jellyfin";
|
{
|
||||||
static_configs = [
|
job_name = "jellyfin";
|
||||||
{
|
static_configs = [
|
||||||
targets = ["127.0.0.1:${toString cfg.port}"];
|
{
|
||||||
labels = {
|
targets = [ "127.0.0.1:${toString cfg.port}" ];
|
||||||
"hostname" = config.networking.hostName;
|
labels = {
|
||||||
"domain" = cfg.domain;
|
"hostname" = config.networking.hostName;
|
||||||
};
|
"domain" = cfg.domain;
|
||||||
}
|
};
|
||||||
];
|
}
|
||||||
}];
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
# LDAP config but you need to install the plugin by hand
|
# LDAP config but you need to install the plugin by hand
|
||||||
|
|
||||||
|
|
@ -427,7 +446,7 @@ in
|
||||||
<EnabledFolders />
|
<EnabledFolders />
|
||||||
<PasswordResetUrl />
|
<PasswordResetUrl />
|
||||||
</PluginConfiguration>
|
</PluginConfiguration>
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# SchemeOverride is needed because of
|
# SchemeOverride is needed because of
|
||||||
# https://github.com/9p4/jellyfin-plugin-sso/issues/264
|
# https://github.com/9p4/jellyfin-plugin-sso/issues/264
|
||||||
|
|
@ -560,23 +579,23 @@ in
|
||||||
</NetworkConfiguration>
|
</NetworkConfiguration>
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
lib.strings.optionalString cfg.debug
|
lib.strings.optionalString cfg.debug ''
|
||||||
''
|
if [ -f "${config.services.jellyfin.configDir}/logging.json" ] && [ ! -L "${config.services.jellyfin.configDir}/logging.json" ]; then
|
||||||
if [ -f "${config.services.jellyfin.configDir}/logging.json" ] && [ ! -L "${config.services.jellyfin.configDir}/logging.json" ]; then
|
echo "A ${config.services.jellyfin.configDir}/logging.json file exists already, this indicates probably an existing installation. Please remove it before continuing."
|
||||||
echo "A ${config.services.jellyfin.configDir}/logging.json file exists already, this indicates probably an existing installation. Please remove it before continuing."
|
exit 1
|
||||||
exit 1
|
fi
|
||||||
fi
|
ln -fs "${debugLogging}" "${config.services.jellyfin.configDir}/logging.json"
|
||||||
ln -fs "${debugLogging}" "${config.services.jellyfin.configDir}/logging.json"
|
''
|
||||||
''
|
+ (lib.shb.replaceSecretsScript {
|
||||||
+ (lib.shb.replaceSecretsScript {
|
file = networkConfig;
|
||||||
file = networkConfig;
|
# Write permissions are needed otherwise the jellyfin-cli tool will not work correctly.
|
||||||
# Write permissions are needed otherwise the jellyfin-cli tool will not work correctly.
|
permissions = "u=rw,g=rw,o=";
|
||||||
permissions = "u=rw,g=rw,o=";
|
resultPath = "${config.services.jellyfin.dataDir}/config/network.xml";
|
||||||
resultPath = "${config.services.jellyfin.dataDir}/config/network.xml";
|
replacements = [
|
||||||
replacements = [
|
];
|
||||||
];
|
})
|
||||||
})
|
+ lib.strings.optionalString cfg.ldap.enable (
|
||||||
+ lib.strings.optionalString cfg.ldap.enable (lib.shb.replaceSecretsScript {
|
lib.shb.replaceSecretsScript {
|
||||||
file = ldapConfig;
|
file = ldapConfig;
|
||||||
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml";
|
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml";
|
||||||
replacements = [
|
replacements = [
|
||||||
|
|
@ -585,8 +604,10 @@ in
|
||||||
source = cfg.ldap.adminPassword.result.path;
|
source = cfg.ldap.adminPassword.result.path;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
})
|
}
|
||||||
+ lib.strings.optionalString cfg.sso.enable (lib.shb.replaceSecretsScript {
|
)
|
||||||
|
+ lib.strings.optionalString cfg.sso.enable (
|
||||||
|
lib.shb.replaceSecretsScript {
|
||||||
file = ssoConfig;
|
file = ssoConfig;
|
||||||
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/SSO-Auth.xml";
|
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/SSO-Auth.xml";
|
||||||
replacements = [
|
replacements = [
|
||||||
|
|
@ -595,92 +616,96 @@ in
|
||||||
source = cfg.sso.sharedSecret.result.path;
|
source = cfg.sso.sharedSecret.result.path;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
})
|
}
|
||||||
+ lib.strings.optionalString cfg.sso.enable (lib.shb.replaceSecretsScript {
|
)
|
||||||
|
+ lib.strings.optionalString cfg.sso.enable (
|
||||||
|
lib.shb.replaceSecretsScript {
|
||||||
file = brandingConfig;
|
file = brandingConfig;
|
||||||
resultPath = "${config.services.jellyfin.dataDir}/config/branding.xml";
|
resultPath = "${config.services.jellyfin.dataDir}/config/branding.xml";
|
||||||
replacements = [
|
replacements = [
|
||||||
];
|
];
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
systemd.services.jellyfin.serviceConfig.ExecStartPost = let
|
systemd.services.jellyfin.serviceConfig.ExecStartPost =
|
||||||
# We must always wait for the service to be fully initialized,
|
let
|
||||||
# even if we're planning on changing the config and restarting.
|
# We must always wait for the service to be fully initialized,
|
||||||
waitForCurl = pkgs.writeShellApplication {
|
# even if we're planning on changing the config and restarting.
|
||||||
name = "waitForCurl";
|
waitForCurl = pkgs.writeShellApplication {
|
||||||
runtimeInputs = [ pkgs.curl ];
|
name = "waitForCurl";
|
||||||
text = ''
|
runtimeInputs = [ pkgs.curl ];
|
||||||
URL="http://127.0.0.1:${toString cfg.port}/System/Info/Public"
|
text = ''
|
||||||
SLEEP_INTERVAL_SEC=2
|
URL="http://127.0.0.1:${toString cfg.port}/System/Info/Public"
|
||||||
TIMEOUT=60
|
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
|
while true; do
|
||||||
status_code=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || true)
|
status_code=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || true)
|
||||||
if [ "$status_code" = "200" ]; then
|
if [ "$status_code" = "200" ]; then
|
||||||
echo "Service is up (HTTP 200 received)."
|
echo "Service is up (HTTP 200 received)."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
elapsed=$(( now - start_time ))
|
elapsed=$(( now - start_time ))
|
||||||
|
|
||||||
if [ $elapsed -ge $TIMEOUT ]; then
|
if [ $elapsed -ge $TIMEOUT ]; then
|
||||||
echo "Timeout reached ($TIMEOUT seconds). Exiting with failure."
|
echo "Timeout reached ($TIMEOUT seconds). Exiting with failure."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Waiting for service... (status: $status_code), elapsed: ''${elapsed}s"
|
echo "Waiting for service... (status: $status_code), elapsed: ''${elapsed}s"
|
||||||
sleep "$SLEEP_INTERVAL_SEC"
|
sleep "$SLEEP_INTERVAL_SEC"
|
||||||
done
|
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
|
# This file is used to know if the jellyfin service has been restarted
|
||||||
# because a new config just got written to.
|
# 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 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.
|
# If the file exists, do nothing and remove the file, resetting the state for the next time.
|
||||||
restartedFile="${config.services.jellyfin.dataDir}/.jellyfin-restarted";
|
restartedFile = "${config.services.jellyfin.dataDir}/.jellyfin-restarted";
|
||||||
|
|
||||||
writeConfig = pkgs.writeShellApplication {
|
writeConfig = pkgs.writeShellApplication {
|
||||||
name = "writeConfig";
|
name = "writeConfig";
|
||||||
runtimeInputs = [ pkgs.systemd ];
|
runtimeInputs = [ pkgs.systemd ];
|
||||||
text = ''
|
text = ''
|
||||||
if ! [ -f "${restartedFile}" ]; then
|
if ! [ -f "${restartedFile}" ]; then
|
||||||
${lib.getExe jellyfin-cli} wizard \
|
${lib.getExe jellyfin-cli} wizard \
|
||||||
--datadir='${config.services.jellyfin.dataDir}' \
|
--datadir='${config.services.jellyfin.dataDir}' \
|
||||||
--configdir='${config.services.jellyfin.configDir}' \
|
--configdir='${config.services.jellyfin.configDir}' \
|
||||||
--cachedir='${config.services.jellyfin.cacheDir}' \
|
--cachedir='${config.services.jellyfin.cacheDir}' \
|
||||||
--logdir='${config.services.jellyfin.logDir}' \
|
--logdir='${config.services.jellyfin.logDir}' \
|
||||||
--username=${cfg.admin.username} \
|
--username=${cfg.admin.username} \
|
||||||
--password-file=${cfg.admin.password.result.path} \
|
--password-file=${cfg.admin.password.result.path} \
|
||||||
--enable-remote-access=true \
|
--enable-remote-access=true \
|
||||||
--write
|
--write
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
restartJellyfinOnce = pkgs.writeShellApplication {
|
restartJellyfinOnce = pkgs.writeShellApplication {
|
||||||
name = "restartJellyfin";
|
name = "restartJellyfin";
|
||||||
runtimeInputs = [ pkgs.systemd ];
|
runtimeInputs = [ pkgs.systemd ];
|
||||||
text = ''
|
text = ''
|
||||||
if [ -f "${restartedFile}" ]; then
|
if [ -f "${restartedFile}" ]; then
|
||||||
echo "jellyfin.service has been restarted"
|
echo "jellyfin.service has been restarted"
|
||||||
rm "${restartedFile}"
|
rm "${restartedFile}"
|
||||||
else
|
else
|
||||||
echo "Restarting jellyfin.service"
|
echo "Restarting jellyfin.service"
|
||||||
touch "${restartedFile}"
|
touch "${restartedFile}"
|
||||||
systemctl reload-or-restart jellyfin.service
|
systemctl reload-or-restart jellyfin.service
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.optionals (cfg.admin != null) [
|
lib.optionals (cfg.admin != null) [
|
||||||
(lib.getExe waitForCurl)
|
(lib.getExe waitForCurl)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.shb.karakeep;
|
cfg = config.shb.karakeep;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -37,18 +42,18 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
environment = lib.mkOption {
|
environment = lib.mkOption {
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = lib.types.attrsOf lib.types.str;
|
||||||
description = "Extra environment variables. See https://docs.karakeep.app/configuration/";
|
description = "Extra environment variables. See https://docs.karakeep.app/configuration/";
|
||||||
example = ''
|
example = ''
|
||||||
{
|
{
|
||||||
OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}";
|
OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}";
|
||||||
INFERENCE_TEXT_MODEL = "deepseek-r1:1.5b";
|
INFERENCE_TEXT_MODEL = "deepseek-r1:1.5b";
|
||||||
INFERENCE_IMAGE_MODEL = "llava";
|
INFERENCE_IMAGE_MODEL = "llava";
|
||||||
EMBEDDING_TEXT_MODEL = "nomic-embed-text:v1.5";
|
EMBEDDING_TEXT_MODEL = "nomic-embed-text:v1.5";
|
||||||
INFERENCE_ENABLE_AUTO_SUMMARIZATION = "true";
|
INFERENCE_ENABLE_AUTO_SUMMARIZATION = "true";
|
||||||
INFERENCE_JOB_TIMEOUT_SEC = "200";
|
INFERENCE_JOB_TIMEOUT_SEC = "200";
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -56,7 +61,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Setup LDAP integration.
|
Setup LDAP integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
userGroup = lib.mkOption {
|
userGroup = lib.mkOption {
|
||||||
|
|
@ -72,7 +77,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Setup SSO integration.
|
Setup SSO integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "SSO integration.";
|
enable = lib.mkEnableOption "SSO integration.";
|
||||||
|
|
@ -91,7 +96,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
authorization_policy = lib.mkOption {
|
authorization_policy = lib.mkOption {
|
||||||
type = lib.types.enum [ "one_factor" "two_factor" ];
|
type = lib.types.enum [
|
||||||
|
"one_factor"
|
||||||
|
"two_factor"
|
||||||
|
];
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
description = "Require one factor (password) or two factor (device) authentication.";
|
||||||
default = "one_factor";
|
default = "one_factor";
|
||||||
};
|
};
|
||||||
|
|
@ -102,7 +110,11 @@ in
|
||||||
options = contracts.secret.mkRequester {
|
options = contracts.secret.mkRequester {
|
||||||
owner = "karakeep";
|
owner = "karakeep";
|
||||||
# These services are the ones relying on the environment file containing the secrets.
|
# These services are the ones relying on the environment file containing the secrets.
|
||||||
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
|
restartUnits = [
|
||||||
|
"karakeep-init.service"
|
||||||
|
"karakeep-workers.service"
|
||||||
|
"karakeep-workers.service"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -125,7 +137,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Backup state directory.
|
Backup state directory.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "karakeep";
|
user = "karakeep";
|
||||||
|
|
@ -142,7 +154,11 @@ in
|
||||||
options = contracts.secret.mkRequester {
|
options = contracts.secret.mkRequester {
|
||||||
owner = "karakeep";
|
owner = "karakeep";
|
||||||
# These services are the ones relying on the environment file containing the secrets.
|
# These services are the ones relying on the environment file containing the secrets.
|
||||||
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
|
restartUnits = [
|
||||||
|
"karakeep-init.service"
|
||||||
|
"karakeep-workers.service"
|
||||||
|
"karakeep-workers.service"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -153,93 +169,108 @@ in
|
||||||
options = contracts.secret.mkRequester {
|
options = contracts.secret.mkRequester {
|
||||||
owner = "karakeep";
|
owner = "karakeep";
|
||||||
# These services are the ones relying on the environment file containing the secrets.
|
# These services are the ones relying on the environment file containing the secrets.
|
||||||
restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ];
|
restartUnits = [
|
||||||
|
"karakeep-init.service"
|
||||||
|
"karakeep-workers.service"
|
||||||
|
"karakeep-workers.service"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = (lib.mkMerge [
|
config = (
|
||||||
(lib.mkIf cfg.enable {
|
lib.mkMerge [
|
||||||
services.karakeep = {
|
(lib.mkIf cfg.enable {
|
||||||
enable = true;
|
services.karakeep = {
|
||||||
meilisearch.enable = true;
|
enable = true;
|
||||||
|
meilisearch.enable = true;
|
||||||
|
|
||||||
extraEnvironment = {
|
extraEnvironment = {
|
||||||
PORT = toString cfg.port;
|
PORT = toString cfg.port;
|
||||||
DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS
|
DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS
|
||||||
} // cfg.environment;
|
}
|
||||||
};
|
// cfg.environment;
|
||||||
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [
|
shb.nginx.vhosts = [
|
||||||
{
|
|
||||||
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 = [
|
|
||||||
{
|
{
|
||||||
subject = [ "group:${cfg.ldap.userGroup}" ];
|
inherit (cfg) subdomain domain ssl;
|
||||||
policy = cfg.sso.authorization_policy;
|
upstream = "http://127.0.0.1:${toString cfg.port}/";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
|
||||||
shb.authelia.oidcClients = [
|
# 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
|
||||||
client_id = cfg.sso.clientID;
|
# instead of using the value from the cfg.meilisearchMasterKey option.
|
||||||
client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path;
|
systemd.services.karakeep-init = {
|
||||||
scopes = [ "openid" "email" "profile" ];
|
script = lib.mkForce (
|
||||||
authorization_policy = "karakeep";
|
(lib.shb.replaceSecrets {
|
||||||
redirect_uris = [
|
userConfig = {
|
||||||
"https://${cfg.subdomain}.${cfg.domain}/api/auth/callback/custom"
|
MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path;
|
||||||
];
|
NEXTAUTH_SECRET.source = cfg.nextauthSecret.result.path;
|
||||||
}
|
}
|
||||||
];
|
// lib.optionalAttrs cfg.sso.enable {
|
||||||
services.karakeep = {
|
OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path;
|
||||||
extraEnvironment = {
|
};
|
||||||
DISABLE_SIGNUPS = "false";
|
resultPath = "/var/lib/karakeep/settings.env";
|
||||||
DISABLE_PASSWORD_AUTH = "true";
|
generator = lib.shb.toEnvVar;
|
||||||
NEXTAUTH_URL = "https://${cfg.subdomain}.${cfg.domain}";
|
})
|
||||||
OAUTH_WELLKNOWN_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration";
|
+ ''
|
||||||
OAUTH_PROVIDER_NAME = "Single Sign-On";
|
export DATA_DIR="$STATE_DIRECTORY"
|
||||||
OAUTH_CLIENT_ID = cfg.sso.clientID;
|
exec ${config.services.karakeep.package}/lib/karakeep/migrate
|
||||||
OAUTH_SCOPE = "openid email profile";
|
''
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
})
|
||||||
})
|
(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
|
|
@ -1,11 +1,22 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.shb.open-webui;
|
cfg = config.shb.open-webui;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
roleClaim = "openwebui_groups";
|
roleClaim = "openwebui_groups";
|
||||||
oauthScopes = [ "openid" "email" "profile" "groups" "${roleClaim}" ];
|
oauthScopes = [
|
||||||
|
"openid"
|
||||||
|
"email"
|
||||||
|
"profile"
|
||||||
|
"groups"
|
||||||
|
"${roleClaim}"
|
||||||
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -42,19 +53,19 @@ in
|
||||||
environment = lib.mkOption {
|
environment = lib.mkOption {
|
||||||
type = lib.types.attrsOf lib.types.str;
|
type = lib.types.attrsOf lib.types.str;
|
||||||
description = "Extra environment variables. See https://docs.openwebui.com/getting-started/env-configuration";
|
description = "Extra environment variables. See https://docs.openwebui.com/getting-started/env-configuration";
|
||||||
default = {};
|
default = { };
|
||||||
example = ''
|
example = ''
|
||||||
{
|
{
|
||||||
WEBUI_NAME = "SelfHostBlocks";
|
WEBUI_NAME = "SelfHostBlocks";
|
||||||
|
|
||||||
OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}";
|
OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}";
|
||||||
RAG_EMBEDDING_MODEL = "nomic-embed-text:v1.5";
|
RAG_EMBEDDING_MODEL = "nomic-embed-text:v1.5";
|
||||||
|
|
||||||
ENABLE_OPENAI_API = "True";
|
ENABLE_OPENAI_API = "True";
|
||||||
OPENAI_API_BASE_URL = "http://127.0.0.1:''${toString config.services.llama-cpp.port}";
|
OPENAI_API_BASE_URL = "http://127.0.0.1:''${toString config.services.llama-cpp.port}";
|
||||||
ENABLE_WEB_SEARCH = "True";
|
ENABLE_WEB_SEARCH = "True";
|
||||||
RAG_EMBEDDING_ENGINE = "openai";
|
RAG_EMBEDDING_ENGINE = "openai";
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -62,7 +73,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Setup LDAP integration.
|
Setup LDAP integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
userGroup = lib.mkOption {
|
userGroup = lib.mkOption {
|
||||||
|
|
@ -84,7 +95,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Setup SSO integration.
|
Setup SSO integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "SSO integration.";
|
enable = lib.mkEnableOption "SSO integration.";
|
||||||
|
|
@ -103,7 +114,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
authorization_policy = lib.mkOption {
|
authorization_policy = lib.mkOption {
|
||||||
type = lib.types.enum [ "one_factor" "two_factor" ];
|
type = lib.types.enum [
|
||||||
|
"one_factor"
|
||||||
|
"two_factor"
|
||||||
|
];
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
description = "Require one factor (password) or two factor (device) authentication.";
|
||||||
default = "one_factor";
|
default = "one_factor";
|
||||||
};
|
};
|
||||||
|
|
@ -136,7 +150,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Backup state directory.
|
Backup state directory.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "open-webui";
|
user = "open-webui";
|
||||||
|
|
@ -149,124 +163,127 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = (lib.mkMerge [
|
config = (
|
||||||
(lib.mkIf cfg.enable {
|
lib.mkMerge [
|
||||||
users.users.open-webui = {
|
(lib.mkIf cfg.enable {
|
||||||
isSystemUser = true;
|
users.users.open-webui = {
|
||||||
group = "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;
|
|
||||||
};
|
};
|
||||||
};
|
users.groups.open-webui = { };
|
||||||
|
|
||||||
shb.authelia.extraDefinitions = {
|
services.open-webui = {
|
||||||
user_attributes.${roleClaim}.expression =
|
enable = true;
|
||||||
''"${cfg.ldap.adminGroup}" in groups ? ["admin"] : ("${cfg.ldap.userGroup}" in groups ? ["user"] : [""])'';
|
|
||||||
};
|
host = "127.0.0.1";
|
||||||
shb.authelia.extraOidcClaimsPolicies.${roleClaim} = {
|
inherit (cfg) port;
|
||||||
custom_claims = {
|
|
||||||
"${roleClaim}" = {};
|
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 = [
|
systemd.services.open-webui.path = [
|
||||||
{
|
pkgs.ffmpeg-headless
|
||||||
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";
|
shb.nginx.vhosts = [
|
||||||
systemd.tmpfiles.rules = [
|
{
|
||||||
"d '/run/open-webui' 0750 root root - -"
|
inherit (cfg) subdomain domain ssl;
|
||||||
];
|
upstream = "http://127.0.0.1:${toString cfg.port}/";
|
||||||
systemd.services.open-webui-pre = {
|
extraConfig = ''
|
||||||
script = lib.shb.replaceSecrets {
|
proxy_read_timeout 300s;
|
||||||
userConfig = {
|
proxy_send_timeout 300s;
|
||||||
OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path;
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
})
|
||||||
|
(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" ];
|
shb.authelia.extraDefinitions = {
|
||||||
before = [ "open-webui.service" ];
|
user_attributes.${roleClaim}.expression =
|
||||||
requiredBy = [ "open-webui.service" ];
|
''"${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" ];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.shb.pinchflat;
|
cfg = config.shb.pinchflat;
|
||||||
|
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -57,7 +62,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
timeZone = lib.mkOption {
|
timeZone = lib.mkOption {
|
||||||
type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ];
|
type = lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
lib.shb.secretFileType
|
||||||
|
];
|
||||||
description = "Timezone of this instance.";
|
description = "Timezone of this instance.";
|
||||||
example = "America/Los_Angeles";
|
example = "America/Los_Angeles";
|
||||||
};
|
};
|
||||||
|
|
@ -66,7 +74,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Setup LDAP integration.
|
Setup LDAP integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = types.submodule {
|
type = types.submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "LDAP integration." // {
|
enable = lib.mkEnableOption "LDAP integration." // {
|
||||||
|
|
@ -86,7 +94,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Setup SSO integration.
|
Setup SSO integration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = types.submodule {
|
type = types.submodule {
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "SSO integration.";
|
enable = lib.mkEnableOption "SSO integration.";
|
||||||
|
|
@ -99,7 +107,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
authorization_policy = lib.mkOption {
|
authorization_policy = lib.mkOption {
|
||||||
type = types.enum [ "one_factor" "two_factor" ];
|
type = types.enum [
|
||||||
|
"one_factor"
|
||||||
|
"two_factor"
|
||||||
|
];
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
description = "Require one factor (password) or two factor (device) authentication.";
|
||||||
default = "one_factor";
|
default = "one_factor";
|
||||||
};
|
};
|
||||||
|
|
@ -111,7 +122,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Backup media directory `shb.mediaDir`.
|
Backup media directory `shb.mediaDir`.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "pinchflat";
|
user = "pinchflat";
|
||||||
|
|
@ -142,7 +153,9 @@ in
|
||||||
|
|
||||||
# This should be using a contract instead of setting the option directly.
|
# This should be using a contract instead of setting the option directly.
|
||||||
shb.lldap = lib.mkIf config.shb.lldap.enable {
|
shb.lldap = lib.mkIf config.shb.lldap.enable {
|
||||||
ensureGroups = { ${cfg.ldap.userGroup} = {}; };
|
ensureGroups = {
|
||||||
|
${cfg.ldap.userGroup} = { };
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.pinchflat-pre = {
|
systemd.services.pinchflat-pre = {
|
||||||
|
|
@ -160,7 +173,7 @@ in
|
||||||
requiredBy = [ "pinchflat.service" ];
|
requiredBy = [ "pinchflat.service" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [
|
shb.nginx.vhosts = [
|
||||||
{
|
{
|
||||||
inherit (cfg) subdomain domain ssl;
|
inherit (cfg) subdomain domain ssl;
|
||||||
inherit (cfg.sso) authEndpoint;
|
inherit (cfg.sso) authEndpoint;
|
||||||
|
|
@ -181,7 +194,7 @@ in
|
||||||
job_name = "pinchflat";
|
job_name = "pinchflat";
|
||||||
static_configs = [
|
static_configs = [
|
||||||
{
|
{
|
||||||
targets = ["127.0.0.1:${toString cfg.port}"];
|
targets = [ "127.0.0.1:${toString cfg.port}" ];
|
||||||
labels = {
|
labels = {
|
||||||
"hostname" = config.networking.hostName;
|
"hostname" = config.networking.hostName;
|
||||||
"domain" = cfg.domain;
|
"domain" = cfg.domain;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,22 @@
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.vaultwarden;
|
cfg = config.shb.vaultwarden;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts { };
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
dataFolder = if lib.versionOlder (config.system.stateVersion or "24.11") "24.11" then "/var/lib/bitwarden_rs" else "/var/lib/vaultwarden";
|
dataFolder =
|
||||||
|
if lib.versionOlder (config.system.stateVersion or "24.11") "24.11" then
|
||||||
|
"/var/lib/bitwarden_rs"
|
||||||
|
else
|
||||||
|
"/var/lib/vaultwarden";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -55,7 +64,10 @@ in
|
||||||
mode = "0440";
|
mode = "0440";
|
||||||
owner = "vaultwarden";
|
owner = "vaultwarden";
|
||||||
group = "postgres";
|
group = "postgres";
|
||||||
restartUnits = [ "vaultwarden.service" "postgresql.service" ];
|
restartUnits = [
|
||||||
|
"vaultwarden.service"
|
||||||
|
"postgresql.service"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -63,53 +75,59 @@ in
|
||||||
smtp = lib.mkOption {
|
smtp = lib.mkOption {
|
||||||
description = "SMTP options.";
|
description = "SMTP options.";
|
||||||
default = null;
|
default = null;
|
||||||
type = lib.types.nullOr (lib.types.submodule {
|
type = lib.types.nullOr (
|
||||||
options = {
|
lib.types.submodule {
|
||||||
from_address = lib.mkOption {
|
options = {
|
||||||
type = lib.types.str;
|
from_address = lib.mkOption {
|
||||||
description = "SMTP address from which the emails originate.";
|
type = lib.types.str;
|
||||||
example = "vaultwarden@mydomain.com";
|
description = "SMTP address from which the emails originate.";
|
||||||
};
|
example = "vaultwarden@mydomain.com";
|
||||||
from_name = lib.mkOption {
|
};
|
||||||
type = lib.types.str;
|
from_name = lib.mkOption {
|
||||||
description = "SMTP name from which the emails originate.";
|
type = lib.types.str;
|
||||||
default = "Vaultwarden";
|
description = "SMTP name from which the emails originate.";
|
||||||
};
|
default = "Vaultwarden";
|
||||||
host = lib.mkOption {
|
};
|
||||||
type = lib.types.str;
|
host = lib.mkOption {
|
||||||
description = "SMTP host to send the emails to.";
|
type = lib.types.str;
|
||||||
};
|
description = "SMTP host to send the emails to.";
|
||||||
security = lib.mkOption {
|
};
|
||||||
type = lib.types.enum [ "starttls" "force_tls" "off" ];
|
security = lib.mkOption {
|
||||||
description = "Security expected by SMTP host.";
|
type = lib.types.enum [
|
||||||
default = "starttls";
|
"starttls"
|
||||||
};
|
"force_tls"
|
||||||
port = lib.mkOption {
|
"off"
|
||||||
type = lib.types.port;
|
];
|
||||||
description = "SMTP port to send the emails to.";
|
description = "Security expected by SMTP host.";
|
||||||
default = 25;
|
default = "starttls";
|
||||||
};
|
};
|
||||||
username = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.port;
|
||||||
description = "Username to connect to the SMTP host.";
|
description = "SMTP port to send the emails to.";
|
||||||
};
|
default = 25;
|
||||||
auth_mechanism = lib.mkOption {
|
};
|
||||||
type = lib.types.enum [ "Login" ];
|
username = lib.mkOption {
|
||||||
description = "Auth mechanism.";
|
type = lib.types.str;
|
||||||
default = "Login";
|
description = "Username to connect to the SMTP host.";
|
||||||
};
|
};
|
||||||
password = lib.mkOption {
|
auth_mechanism = lib.mkOption {
|
||||||
description = "File containing the password to connect to the SMTP host.";
|
type = lib.types.enum [ "Login" ];
|
||||||
type = lib.types.submodule {
|
description = "Auth mechanism.";
|
||||||
options = contracts.secret.mkRequester {
|
default = "Login";
|
||||||
mode = "0400";
|
};
|
||||||
owner = "vaultwarden";
|
password = lib.mkOption {
|
||||||
restartUnits = [ "vaultwarden.service" ];
|
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 {
|
mount = lib.mkOption {
|
||||||
|
|
@ -127,14 +145,16 @@ in
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
default = { path = dataFolder; };
|
default = {
|
||||||
|
path = dataFolder;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
backup = lib.mkOption {
|
backup = lib.mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Backup configuration.
|
Backup configuration.
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = { };
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = contracts.backup.mkRequester {
|
options = contracts.backup.mkRequester {
|
||||||
user = "vaultwarden";
|
user = "vaultwarden";
|
||||||
|
|
@ -170,7 +190,8 @@ in
|
||||||
ROCKET_LOG = if cfg.debug then "trace" else "info";
|
ROCKET_LOG = if cfg.debug then "trace" else "info";
|
||||||
ROCKET_ADDRESS = "127.0.0.1";
|
ROCKET_ADDRESS = "127.0.0.1";
|
||||||
ROCKET_PORT = cfg.port;
|
ROCKET_PORT = cfg.port;
|
||||||
} // lib.optionalAttrs (cfg.smtp != null) {
|
}
|
||||||
|
// lib.optionalAttrs (cfg.smtp != null) {
|
||||||
SMTP_FROM = cfg.smtp.from_address;
|
SMTP_FROM = cfg.smtp.from_address;
|
||||||
SMTP_FROM_NAME = cfg.smtp.from_name;
|
SMTP_FROM_NAME = cfg.smtp.from_name;
|
||||||
SMTP_HOST = cfg.smtp.host;
|
SMTP_HOST = cfg.smtp.host;
|
||||||
|
|
@ -189,27 +210,32 @@ in
|
||||||
];
|
];
|
||||||
# Needed to be able to write template config.
|
# Needed to be able to write template config.
|
||||||
systemd.services.vaultwarden.serviceConfig.ProtectHome = lib.mkForce false;
|
systemd.services.vaultwarden.serviceConfig.ProtectHome = lib.mkForce false;
|
||||||
systemd.services.vaultwarden.preStart =
|
systemd.services.vaultwarden.preStart = lib.shb.replaceSecrets {
|
||||||
lib.shb.replaceSecrets {
|
userConfig = {
|
||||||
userConfig = {
|
DATABASE_URL.source = cfg.databasePassword.result.path;
|
||||||
DATABASE_URL.source = cfg.databasePassword.result.path;
|
DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden";
|
||||||
DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden";
|
}
|
||||||
} // lib.optionalAttrs (cfg.smtp != null) {
|
// lib.optionalAttrs (cfg.smtp != null) {
|
||||||
SMTP_PASSWORD.source = cfg.smtp.password.result.path;
|
SMTP_PASSWORD.source = cfg.smtp.password.result.path;
|
||||||
};
|
|
||||||
resultPath = "${dataFolder}/vaultwarden.env";
|
|
||||||
generator = lib.shb.toEnvVar;
|
|
||||||
};
|
};
|
||||||
|
resultPath = "${dataFolder}/vaultwarden.env";
|
||||||
|
generator = lib.shb.toEnvVar;
|
||||||
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [
|
shb.nginx.vhosts = [
|
||||||
{
|
{
|
||||||
inherit (cfg) subdomain domain authEndpoint ssl;
|
inherit (cfg)
|
||||||
|
subdomain
|
||||||
|
domain
|
||||||
|
authEndpoint
|
||||||
|
ssl
|
||||||
|
;
|
||||||
upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}";
|
upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}";
|
||||||
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
|
autheliaRules = lib.mkIf (cfg.authEndpoint != null) [
|
||||||
{
|
{
|
||||||
domain = "${fqdn}";
|
domain = "${fqdn}";
|
||||||
policy = "two_factor";
|
policy = "two_factor";
|
||||||
subject = ["group:vaultwarden_admin"];
|
subject = [ "group:vaultwarden_admin" ];
|
||||||
resources = [
|
resources = [
|
||||||
"^/admin"
|
"^/admin"
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -8,178 +8,185 @@ in
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
name = "authelia-basic";
|
name = "authelia-basic";
|
||||||
|
|
||||||
nodes.machine = { config, pkgs, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
{
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
imports = [
|
||||||
../../modules/blocks/authelia.nix
|
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
||||||
../../modules/blocks/hardcodedsecret.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"
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
|
|
||||||
shb.lldap = {
|
networking.hosts = {
|
||||||
enable = true;
|
"127.0.0.1" = [
|
||||||
dcdomain = "dc=example,dc=com";
|
"machine.com"
|
||||||
subdomain = "ldap";
|
"client1.machine.com"
|
||||||
domain = "machine.com";
|
"client2.machine.com"
|
||||||
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
|
"ldap.machine.com"
|
||||||
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
|
"authelia.machine.com"
|
||||||
};
|
];
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
oidcClients = [
|
shb.lldap = {
|
||||||
{
|
enable = true;
|
||||||
client_id = "client1";
|
dcdomain = "dc=example,dc=com";
|
||||||
client_name = "My Client 1";
|
subdomain = "ldap";
|
||||||
client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$LR2wY11djfLrVQixdlLJew$rPByqFt6JfbIIAITxzAXckwh51QgV8E5YZmA8rXOzkMfBUcMq7cnOKEXF6MAFbjZaGf3J/B1OzLWZTCuZtALVw";
|
domain = "machine.com";
|
||||||
public = false;
|
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
|
||||||
authorization_policy = "one_factor";
|
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
|
||||||
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 = {
|
shb.hardcodedsecret.ldapUserPassword = {
|
||||||
request = config.shb.authelia.secrets.jwtSecret.request;
|
request = config.shb.lldap.ldapUserPassword.request;
|
||||||
settings.content = "jwtSecret";
|
settings.content = ldapAdminPassword;
|
||||||
};
|
};
|
||||||
shb.hardcodedsecret.ldapAdminPassword = {
|
shb.hardcodedsecret.jwtSecret = {
|
||||||
request = config.shb.authelia.secrets.ldapAdminPassword.request;
|
request = config.shb.lldap.jwtSecret.request;
|
||||||
settings.content = ldapAdminPassword;
|
settings.content = "jwtsecret";
|
||||||
};
|
};
|
||||||
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 = {
|
shb.authelia = {
|
||||||
withDebug.configuration = {
|
enable = true;
|
||||||
shb.authelia.debug = 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, ... }:
|
testScript =
|
||||||
let
|
{ nodes, ... }:
|
||||||
specializations = "${nodes.machine.system.build.toplevel}/specialisation";
|
let
|
||||||
in
|
specializations = "${nodes.machine.system.build.toplevel}/specialisation";
|
||||||
''
|
in
|
||||||
import json
|
''
|
||||||
|
import json
|
||||||
|
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
def tests():
|
def tests():
|
||||||
machine.wait_for_unit("lldap.service")
|
machine.wait_for_unit("lldap.service")
|
||||||
machine.wait_for_unit("authelia-authelia.machine.com.target")
|
machine.wait_for_unit("authelia-authelia.machine.com.target")
|
||||||
machine.wait_for_open_port(9091)
|
machine.wait_for_open_port(9091)
|
||||||
|
|
||||||
endpoints = json.loads(machine.succeed("curl -s http://machine.com/.well-known/openid-configuration"))
|
endpoints = json.loads(machine.succeed("curl -s http://machine.com/.well-known/openid-configuration"))
|
||||||
auth_endpoint = endpoints['authorization_endpoint']
|
auth_endpoint = endpoints['authorization_endpoint']
|
||||||
print(f"auth_endpoint: {auth_endpoint}")
|
print(f"auth_endpoint: {auth_endpoint}")
|
||||||
if auth_endpoint != "http://machine.com/api/oidc/authorization":
|
if auth_endpoint != "http://machine.com/api/oidc/authorization":
|
||||||
raise Exception("Unexpected auth_endpoint")
|
raise Exception("Unexpected auth_endpoint")
|
||||||
|
|
||||||
resp = machine.succeed(
|
resp = machine.succeed(
|
||||||
"curl -f -s '"
|
"curl -f -s '"
|
||||||
+ auth_endpoint
|
+ auth_endpoint
|
||||||
+ "?client_id=other"
|
+ "?client_id=other"
|
||||||
+ "&redirect_uri=http://client1.machine.com/redirect"
|
+ "&redirect_uri=http://client1.machine.com/redirect"
|
||||||
+ "&scope=openid%20profile%20email"
|
+ "&scope=openid%20profile%20email"
|
||||||
+ "&response_type=code"
|
+ "&response_type=code"
|
||||||
+ "&state=99999999'"
|
+ "&state=99999999'"
|
||||||
)
|
)
|
||||||
print(resp)
|
print(resp)
|
||||||
if resp != "":
|
if resp != "":
|
||||||
raise Exception("unexpected response")
|
raise Exception("unexpected response")
|
||||||
|
|
||||||
resp = machine.succeed(
|
resp = machine.succeed(
|
||||||
"curl -f -s '"
|
"curl -f -s '"
|
||||||
+ auth_endpoint
|
+ auth_endpoint
|
||||||
+ "?client_id=client1"
|
+ "?client_id=client1"
|
||||||
+ "&redirect_uri=http://client1.machine.com/redirect"
|
+ "&redirect_uri=http://client1.machine.com/redirect"
|
||||||
+ "&scope=openid%20profile%20email"
|
+ "&scope=openid%20profile%20email"
|
||||||
+ "&response_type=code"
|
+ "&response_type=code"
|
||||||
+ "&state=11111111'"
|
+ "&state=11111111'"
|
||||||
)
|
)
|
||||||
print(resp)
|
print(resp)
|
||||||
if "Found" not in resp:
|
if "Found" not in resp:
|
||||||
raise Exception("unexpected response")
|
raise Exception("unexpected response")
|
||||||
|
|
||||||
resp = machine.succeed(
|
resp = machine.succeed(
|
||||||
"curl -f -s '"
|
"curl -f -s '"
|
||||||
+ auth_endpoint
|
+ auth_endpoint
|
||||||
+ "?client_id=client2"
|
+ "?client_id=client2"
|
||||||
+ "&redirect_uri=http://client2.machine.com/redirect"
|
+ "&redirect_uri=http://client2.machine.com/redirect"
|
||||||
+ "&scope=openid%20profile%20email"
|
+ "&scope=openid%20profile%20email"
|
||||||
+ "&response_type=code"
|
+ "&response_type=code"
|
||||||
+ "&state=22222222'"
|
+ "&state=22222222'"
|
||||||
)
|
)
|
||||||
print(resp)
|
print(resp)
|
||||||
if "Found" not in resp:
|
if "Found" not in resp:
|
||||||
raise Exception("unexpected response")
|
raise Exception("unexpected response")
|
||||||
|
|
||||||
with subtest("no debug"):
|
with subtest("no debug"):
|
||||||
tests()
|
tests()
|
||||||
|
|
||||||
with subtest("with debug"):
|
with subtest("with debug"):
|
||||||
machine.succeed('${specializations}/withDebug/bin/switch-to-configuration test')
|
machine.succeed('${specializations}/withDebug/bin/switch-to-configuration test')
|
||||||
tests()
|
tests()
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,9 @@ in
|
||||||
|
|
||||||
configWithTemplates = lib.shb.withReplacements userConfig;
|
configWithTemplates = lib.shb.withReplacements userConfig;
|
||||||
|
|
||||||
nonSecretConfigFile = pkgs.writeText "config.yaml.template" (lib.generators.toJSON {} configWithTemplates);
|
nonSecretConfigFile = pkgs.writeText "config.yaml.template" (
|
||||||
|
lib.generators.toJSON { } configWithTemplates
|
||||||
|
);
|
||||||
|
|
||||||
replacements = lib.shb.getReplacements userConfig;
|
replacements = lib.shb.getReplacements userConfig;
|
||||||
|
|
||||||
|
|
@ -37,105 +39,113 @@ in
|
||||||
replaceInTemplateJSON = lib.shb.replaceSecrets {
|
replaceInTemplateJSON = lib.shb.replaceSecrets {
|
||||||
inherit userConfig;
|
inherit userConfig;
|
||||||
resultPath = "/var/lib/config.json";
|
resultPath = "/var/lib/config.json";
|
||||||
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json {});
|
generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json { });
|
||||||
};
|
};
|
||||||
|
|
||||||
replaceInTemplateJSONGen = lib.shb.replaceSecrets {
|
replaceInTemplateJSONGen = lib.shb.replaceSecrets {
|
||||||
inherit userConfig;
|
inherit userConfig;
|
||||||
resultPath = "/var/lib/config_gen.json";
|
resultPath = "/var/lib/config_gen.json";
|
||||||
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toJSON {});
|
generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toJSON { });
|
||||||
};
|
};
|
||||||
|
|
||||||
replaceInTemplateXML = lib.shb.replaceSecrets {
|
replaceInTemplateXML = lib.shb.replaceSecrets {
|
||||||
inherit userConfig;
|
inherit userConfig;
|
||||||
resultPath = "/var/lib/config.xml";
|
resultPath = "/var/lib/config.xml";
|
||||||
generator = lib.shb.replaceSecretsFormatAdapter (lib.shb.formatXML {enclosingRoot = "Root";});
|
generator = lib.shb.replaceSecretsFormatAdapter (lib.shb.formatXML { enclosingRoot = "Root"; });
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.shb.runNixOSTest {
|
lib.shb.runNixOSTest {
|
||||||
name = "lib-template";
|
name = "lib-template";
|
||||||
nodes.machine = { config, pkgs, ... }:
|
nodes.machine =
|
||||||
{
|
{ config, pkgs, ... }:
|
||||||
imports = [
|
{
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
imports = [
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
||||||
{
|
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
||||||
options = {
|
{
|
||||||
libtest.config = lib.mkOption {
|
options = {
|
||||||
type = lib.types.attrsOf (lib.types.oneOf [ lib.types.str lib.secretFileType ]);
|
libtest.config = lib.mkOption {
|
||||||
};
|
type = lib.types.attrsOf (
|
||||||
|
lib.types.oneOf [
|
||||||
|
lib.types.str
|
||||||
|
lib.secretFileType
|
||||||
|
]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
];
|
}
|
||||||
|
];
|
||||||
|
|
||||||
system.activationScripts = {
|
system.activationScripts = {
|
||||||
libtest = replaceInTemplate;
|
libtest = replaceInTemplate;
|
||||||
libtestJSON = replaceInTemplateJSON;
|
libtestJSON = replaceInTemplateJSON;
|
||||||
libtestJSONGen = replaceInTemplateJSONGen;
|
libtestJSONGen = replaceInTemplateJSONGen;
|
||||||
libtestXML = replaceInTemplateXML;
|
libtestXML = replaceInTemplateXML;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
testScript = { nodes, ... }: ''
|
testScript =
|
||||||
import json
|
{ nodes, ... }:
|
||||||
from collections import ChainMap
|
''
|
||||||
from xml.etree import ElementTree
|
import json
|
||||||
|
from collections import ChainMap
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
start_all()
|
start_all()
|
||||||
machine.wait_for_file("/var/lib/config.yaml")
|
machine.wait_for_file("/var/lib/config.yaml")
|
||||||
machine.wait_for_file("/var/lib/config.json")
|
machine.wait_for_file("/var/lib/config.json")
|
||||||
machine.wait_for_file("/var/lib/config_gen.json")
|
machine.wait_for_file("/var/lib/config_gen.json")
|
||||||
machine.wait_for_file("/var/lib/config.xml")
|
machine.wait_for_file("/var/lib/config.xml")
|
||||||
|
|
||||||
def xml_to_dict_recursive(root):
|
def xml_to_dict_recursive(root):
|
||||||
all_descendants = list(root)
|
all_descendants = list(root)
|
||||||
if len(all_descendants) == 0:
|
if len(all_descendants) == 0:
|
||||||
return {root.tag: root.text}
|
return {root.tag: root.text}
|
||||||
else:
|
else:
|
||||||
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
|
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
|
||||||
return {root.tag: dict(merged_dict)}
|
return {root.tag: dict(merged_dict)}
|
||||||
|
|
||||||
wantedConfig = json.loads('${lib.generators.toJSON {} wantedConfig}')
|
wantedConfig = json.loads('${lib.generators.toJSON { } wantedConfig}')
|
||||||
|
|
||||||
with subtest("config"):
|
with subtest("config"):
|
||||||
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}"))
|
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}"))
|
||||||
|
|
||||||
gotConfig = machine.succeed("cat /var/lib/config.yaml")
|
gotConfig = machine.succeed("cat /var/lib/config.yaml")
|
||||||
print(gotConfig)
|
print(gotConfig)
|
||||||
gotConfig = json.loads(gotConfig)
|
gotConfig = json.loads(gotConfig)
|
||||||
|
|
||||||
if wantedConfig != gotConfig:
|
if wantedConfig != gotConfig:
|
||||||
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
|
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
|
||||||
|
|
||||||
with subtest("config JSON Gen"):
|
with subtest("config JSON Gen"):
|
||||||
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSONGen" replaceInTemplateJSONGen}"))
|
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSONGen" replaceInTemplateJSONGen}"))
|
||||||
|
|
||||||
gotConfig = machine.succeed("cat /var/lib/config_gen.json")
|
gotConfig = machine.succeed("cat /var/lib/config_gen.json")
|
||||||
print(gotConfig)
|
print(gotConfig)
|
||||||
gotConfig = json.loads(gotConfig)
|
gotConfig = json.loads(gotConfig)
|
||||||
|
|
||||||
if wantedConfig != gotConfig:
|
if wantedConfig != gotConfig:
|
||||||
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
|
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
|
||||||
|
|
||||||
with subtest("config JSON"):
|
with subtest("config JSON"):
|
||||||
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSON" replaceInTemplateJSON}"))
|
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSON" replaceInTemplateJSON}"))
|
||||||
|
|
||||||
gotConfig = machine.succeed("cat /var/lib/config.json")
|
gotConfig = machine.succeed("cat /var/lib/config.json")
|
||||||
print(gotConfig)
|
print(gotConfig)
|
||||||
gotConfig = json.loads(gotConfig)
|
gotConfig = json.loads(gotConfig)
|
||||||
|
|
||||||
if wantedConfig != gotConfig:
|
if wantedConfig != gotConfig:
|
||||||
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
|
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
|
||||||
|
|
||||||
with subtest("config XML"):
|
with subtest("config XML"):
|
||||||
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateXML" replaceInTemplateXML}"))
|
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateXML" replaceInTemplateXML}"))
|
||||||
|
|
||||||
gotConfig = machine.succeed("cat /var/lib/config.xml")
|
gotConfig = machine.succeed("cat /var/lib/config.xml")
|
||||||
print(gotConfig)
|
print(gotConfig)
|
||||||
gotConfig = xml_to_dict_recursive(ElementTree.XML(gotConfig))['Root']
|
gotConfig = xml_to_dict_recursive(ElementTree.XML(gotConfig))['Root']
|
||||||
|
|
||||||
if wantedConfig != gotConfig:
|
if wantedConfig != gotConfig:
|
||||||
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
|
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,145 +9,148 @@ in
|
||||||
auth = lib.shb.runNixOSTest {
|
auth = lib.shb.runNixOSTest {
|
||||||
name = "ldap-auth";
|
name = "ldap-auth";
|
||||||
|
|
||||||
nodes.server = { config, pkgs, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
{
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
imports = [
|
||||||
{
|
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
||||||
options = {
|
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
||||||
shb.ssl.enable = lib.mkEnableOption "ssl";
|
{
|
||||||
|
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 = {
|
ensureGroups = {
|
||||||
enable = true;
|
"family" = { };
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
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 = {
|
networking.firewall.allowedTCPPorts = [ 80 ]; # nginx port
|
||||||
"family" = {};
|
|
||||||
|
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
|
nodes.client = { };
|
||||||
|
|
||||||
environment.systemPackages = [ pkgs.openldap ];
|
|
||||||
|
|
||||||
specialisation = {
|
|
||||||
withDebug.configuration = {
|
|
||||||
shb.lldap.debug = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
nodes.client = {};
|
|
||||||
|
|
||||||
# Inspired from https://github.com/lldap/lldap/blob/33f50d13a2e2d24a3e6bb05a148246bc98090df0/example_configs/lldap-ha-auth.sh
|
# Inspired from https://github.com/lldap/lldap/blob/33f50d13a2e2d24a3e6bb05a148246bc98090df0/example_configs/lldap-ha-auth.sh
|
||||||
testScript = { nodes, ... }:
|
testScript =
|
||||||
let
|
{ nodes, ... }:
|
||||||
specializations = "${nodes.server.system.build.toplevel}/specialisation";
|
let
|
||||||
in
|
specializations = "${nodes.server.system.build.toplevel}/specialisation";
|
||||||
''
|
in
|
||||||
import json
|
''
|
||||||
|
import json
|
||||||
|
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
def tests():
|
def tests():
|
||||||
server.wait_for_unit("lldap.service")
|
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.webUIListenPort})
|
||||||
server.wait_for_open_port(${toString nodes.server.shb.lldap.ldapPort})
|
server.wait_for_open_port(${toString nodes.server.shb.lldap.ldapPort})
|
||||||
|
|
||||||
with subtest("fail without authenticating"):
|
with subtest("fail without authenticating"):
|
||||||
client.fail(
|
client.fail(
|
||||||
"curl -f -s -X GET"
|
"curl -f -s -X GET"
|
||||||
+ """ -H "Content-type: application/json" """
|
+ """ -H "Content-type: application/json" """
|
||||||
+ """ -H "Host: ldap.example.com" """
|
+ """ -H "Host: ldap.example.com" """
|
||||||
+ " http://server/api/graphql"
|
+ " http://server/api/graphql"
|
||||||
)
|
)
|
||||||
|
|
||||||
with subtest("fail authenticating with wrong credentials"):
|
with subtest("fail authenticating with wrong credentials"):
|
||||||
resp = client.fail(
|
resp = client.fail(
|
||||||
"curl -f -s -X POST"
|
"curl -f -s -X POST"
|
||||||
+ """ -H "Content-type: application/json" """
|
+ """ -H "Content-type: application/json" """
|
||||||
+ """ -H "Host: ldap.example.com" """
|
+ """ -H "Host: ldap.example.com" """
|
||||||
+ " http://server/auth/simple/login"
|
+ " http://server/auth/simple/login"
|
||||||
+ """ -d '{"username": "admin", "password": "wrong"}'"""
|
+ """ -d '{"username": "admin", "password": "wrong"}'"""
|
||||||
)
|
)
|
||||||
|
|
||||||
print(resp)
|
print(resp)
|
||||||
|
|
||||||
with subtest("succeed with correct authentication"):
|
with subtest("succeed with correct authentication"):
|
||||||
token = json.loads(client.succeed(
|
token = json.loads(client.succeed(
|
||||||
"curl -f -s -X POST "
|
"curl -f -s -X POST "
|
||||||
+ """ -H "Content-type: application/json" """
|
+ """ -H "Content-type: application/json" """
|
||||||
+ """ -H "Host: ldap.example.com" """
|
+ """ -H "Host: ldap.example.com" """
|
||||||
+ " http://server/auth/simple/login "
|
+ " http://server/auth/simple/login "
|
||||||
+ """ -d '{"username": "admin", "password": "${password}"}' """
|
+ """ -d '{"username": "admin", "password": "${password}"}' """
|
||||||
))['token']
|
))['token']
|
||||||
|
|
||||||
data = json.loads(client.succeed(
|
data = json.loads(client.succeed(
|
||||||
"curl -f -s -X POST "
|
"curl -f -s -X POST "
|
||||||
+ """ -H "Content-type: application/json" """
|
+ """ -H "Content-type: application/json" """
|
||||||
+ """ -H "Host: ldap.example.com" """
|
+ """ -H "Host: ldap.example.com" """
|
||||||
+ """ -H "Authorization: Bearer {token}" """.format(token=token)
|
+ """ -H "Authorization: Bearer {token}" """.format(token=token)
|
||||||
+ " http://server/api/graphql "
|
+ " http://server/api/graphql "
|
||||||
+ """ -d '{"variables": {"id": "admin"}, "query":"query($id:String!){user(userId:$id){displayName groups{displayName}}}"}' """
|
+ """ -d '{"variables": {"id": "admin"}, "query":"query($id:String!){user(userId:$id){displayName groups{displayName}}}"}' """
|
||||||
))['data']
|
))['data']
|
||||||
|
|
||||||
assert data['user']['displayName'] == "Administrator"
|
assert data['user']['displayName'] == "Administrator"
|
||||||
assert data['user']['groups'][0]['displayName'] == "lldap_admin"
|
assert data['user']['groups'][0]['displayName'] == "lldap_admin"
|
||||||
|
|
||||||
with subtest("succeed charlie"):
|
with subtest("succeed charlie"):
|
||||||
resp = client.succeed(
|
resp = client.succeed(
|
||||||
"curl -f -s -X POST "
|
"curl -f -s -X POST "
|
||||||
+ """ -H "Content-type: application/json" """
|
+ """ -H "Content-type: application/json" """
|
||||||
+ """ -H "Host: ldap.example.com" """
|
+ """ -H "Host: ldap.example.com" """
|
||||||
+ " http://server/auth/simple/login "
|
+ " http://server/auth/simple/login "
|
||||||
+ """ -d '{"username": "charlie", "password": "${charliePassword}"}' """
|
+ """ -d '{"username": "charlie", "password": "${charliePassword}"}' """
|
||||||
)
|
)
|
||||||
print(resp)
|
print(resp)
|
||||||
|
|
||||||
with subtest("ldap user search"):
|
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}')
|
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)
|
print(resp)
|
||||||
|
|
||||||
if "uid=admin" not in resp:
|
if "uid=admin" not in resp:
|
||||||
raise Exception("Expected to find admin")
|
raise Exception("Expected to find admin")
|
||||||
|
|
||||||
if "uid=charlie" not in resp:
|
if "uid=charlie" not in resp:
|
||||||
raise Exception("Expected to find charlie")
|
raise Exception("Expected to find charlie")
|
||||||
|
|
||||||
with subtest("no debug"):
|
with subtest("no debug"):
|
||||||
tests()
|
tests()
|
||||||
|
|
||||||
with subtest("with debug"):
|
with subtest("with debug"):
|
||||||
server.succeed('${specializations}/withDebug/bin/switch-to-configuration test')
|
server.succeed('${specializations}/withDebug/bin/switch-to-configuration test')
|
||||||
tests()
|
tests()
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,137 +1,148 @@
|
||||||
{ pkgs, lib, ... }:
|
{ pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
serve = port: text: lib.getExe (pkgs.writers.writePython3Bin "serve"
|
serve =
|
||||||
{
|
port: text:
|
||||||
libraries = [ pkgs.python3Packages.systemd ];
|
lib.getExe (
|
||||||
}
|
pkgs.writers.writePython3Bin "serve"
|
||||||
(let
|
{
|
||||||
content = pkgs.writeText "content" text;
|
libraries = [ pkgs.python3Packages.systemd ];
|
||||||
in ''
|
}
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
(
|
||||||
from systemd.daemon import notify
|
let
|
||||||
|
content = pkgs.writeText "content" text;
|
||||||
|
in
|
||||||
|
''
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from systemd.daemon import notify
|
||||||
|
|
||||||
with open("${content}", "rb") as f:
|
with open("${content}", "rb") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
|
|
||||||
class HardcodedHandler(BaseHTTPRequestHandler):
|
class HardcodedHandler(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
reponse = content + self.path.encode('utf-8')
|
reponse = content + self.path.encode('utf-8')
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "text/plain")
|
self.send_header("Content-Type", "text/plain")
|
||||||
self.send_header("Content-Length", str(len(reponse)))
|
self.send_header("Content-Length", str(len(reponse)))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
print("answering to GET request")
|
print("answering to GET request")
|
||||||
self.wfile.write(reponse)
|
self.wfile.write(reponse)
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
pass # optional: suppress logging
|
pass # optional: suppress logging
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
notify('STATUS=Starting up...')
|
notify('STATUS=Starting up...')
|
||||||
server_address = ('127.0.0.1', ${toString port})
|
server_address = ('127.0.0.1', ${toString port})
|
||||||
httpd = HTTPServer(server_address, HardcodedHandler)
|
httpd = HTTPServer(server_address, HardcodedHandler)
|
||||||
print("Serving hardcoded page on http://127.0.0.1:${toString port}")
|
print("Serving hardcoded page on http://127.0.0.1:${toString port}")
|
||||||
notify('READY=1')
|
notify('READY=1')
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
'')
|
''
|
||||||
);
|
)
|
||||||
|
);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = lib.shb.runNixOSTest {
|
default = lib.shb.runNixOSTest {
|
||||||
name = "mitmdump-default";
|
name = "mitmdump-default";
|
||||||
|
|
||||||
nodes.machine = { config, pkgs, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
../../modules/blocks/mitmdump.nix
|
{
|
||||||
];
|
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"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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, ... }: ''
|
testScript =
|
||||||
start_all()
|
{ nodes, ... }:
|
||||||
|
''
|
||||||
|
start_all()
|
||||||
|
|
||||||
machine.wait_for_unit("test1.service")
|
machine.wait_for_unit("test1.service")
|
||||||
machine.wait_for_unit("test2.service")
|
machine.wait_for_unit("test2.service")
|
||||||
machine.wait_for_unit("mitmdump-test1.service")
|
machine.wait_for_unit("mitmdump-test1.service")
|
||||||
machine.wait_for_unit("mitmdump-test2.service")
|
machine.wait_for_unit("mitmdump-test2.service")
|
||||||
|
|
||||||
resp = machine.succeed("curl http://127.0.0.1:8000")
|
resp = machine.succeed("curl http://127.0.0.1:8000")
|
||||||
print(resp)
|
print(resp)
|
||||||
if resp != "test1/":
|
if resp != "test1/":
|
||||||
raise Exception("wanted 'test1'")
|
raise Exception("wanted 'test1'")
|
||||||
|
|
||||||
resp = machine.succeed("curl -v http://127.0.0.1:8001")
|
resp = machine.succeed("curl -v http://127.0.0.1:8001")
|
||||||
print(resp)
|
print(resp)
|
||||||
if resp != "test1/":
|
if resp != "test1/":
|
||||||
raise Exception("wanted 'test1'")
|
raise Exception("wanted 'test1'")
|
||||||
|
|
||||||
resp = machine.succeed("curl http://127.0.0.1:8002")
|
resp = machine.succeed("curl http://127.0.0.1:8002")
|
||||||
print(resp)
|
print(resp)
|
||||||
if resp != "test2/":
|
if resp != "test2/":
|
||||||
raise Exception("wanted 'test2'")
|
raise Exception("wanted 'test2'")
|
||||||
|
|
||||||
resp = machine.succeed("curl http://127.0.0.1:8003/notverbose")
|
resp = machine.succeed("curl http://127.0.0.1:8003/notverbose")
|
||||||
print(resp)
|
print(resp)
|
||||||
if resp != "test2/notverbose":
|
if resp != "test2/notverbose":
|
||||||
raise Exception("wanted 'test2/notverbose'")
|
raise Exception("wanted 'test2/notverbose'")
|
||||||
|
|
||||||
resp = machine.succeed("curl http://127.0.0.1:8003/verbose")
|
resp = machine.succeed("curl http://127.0.0.1:8003/verbose")
|
||||||
print(resp)
|
print(resp)
|
||||||
if resp != "test2/verbose":
|
if resp != "test2/verbose":
|
||||||
raise Exception("wanted 'test2/verbose'")
|
raise Exception("wanted 'test2/verbose'")
|
||||||
|
|
||||||
dump = machine.succeed("journalctl -b -u mitmdump-test1.service")
|
dump = machine.succeed("journalctl -b -u mitmdump-test1.service")
|
||||||
print(dump)
|
print(dump)
|
||||||
if "HTTP/1.0 200 OK" not in dump:
|
if "HTTP/1.0 200 OK" not in dump:
|
||||||
raise Exception("expected to see HTTP/1.0 200 OK")
|
raise Exception("expected to see HTTP/1.0 200 OK")
|
||||||
if "test1" not in dump:
|
if "test1" not in dump:
|
||||||
raise Exception("expected to see test1")
|
raise Exception("expected to see test1")
|
||||||
|
|
||||||
dump = machine.succeed("journalctl -b -u mitmdump-test2.service")
|
dump = machine.succeed("journalctl -b -u mitmdump-test2.service")
|
||||||
print(dump)
|
print(dump)
|
||||||
if "HTTP/1.0 200 OK" not in dump:
|
if "HTTP/1.0 200 OK" not in dump:
|
||||||
raise Exception("expected to see HTTP/1.0 200 OK")
|
raise Exception("expected to see HTTP/1.0 200 OK")
|
||||||
if "test2/notverbose" in dump:
|
if "test2/notverbose" in dump:
|
||||||
raise Exception("expected not to see test2/notverbose")
|
raise Exception("expected not to see test2/notverbose")
|
||||||
if "test2/verbose" not in dump:
|
if "test2/verbose" not in dump:
|
||||||
raise Exception("expected to see test2/verbose")
|
raise Exception("expected to see test2/verbose")
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,177 +6,193 @@ in
|
||||||
peerWithoutUser = lib.shb.runNixOSTest {
|
peerWithoutUser = lib.shb.runNixOSTest {
|
||||||
name = "postgresql-peerWithoutUser";
|
name = "postgresql-peerWithoutUser";
|
||||||
|
|
||||||
nodes.machine = { config, pkgs, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
{
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
imports = [
|
||||||
../../modules/blocks/postgresql.nix
|
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
||||||
];
|
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
||||||
|
../../modules/blocks/postgresql.nix
|
||||||
|
];
|
||||||
|
|
||||||
shb.postgresql.ensures = [
|
shb.postgresql.ensures = [
|
||||||
{
|
{
|
||||||
username = "me";
|
username = "me";
|
||||||
database = "me";
|
database = "me";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = { nodes, ... }: ''
|
testScript =
|
||||||
start_all()
|
{ nodes, ... }:
|
||||||
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(user, database):
|
def peer_cmd(user, database):
|
||||||
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
|
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
|
||||||
|
|
||||||
with subtest("cannot login because of missing user"):
|
with subtest("cannot login because of missing user"):
|
||||||
machine.fail(peer_cmd("me", "me"), timeout=10)
|
machine.fail(peer_cmd("me", "me"), timeout=10)
|
||||||
|
|
||||||
with subtest("cannot login with unknown user"):
|
with subtest("cannot login with unknown user"):
|
||||||
machine.fail(peer_cmd("notme", "me"), timeout=10)
|
machine.fail(peer_cmd("notme", "me"), timeout=10)
|
||||||
|
|
||||||
with subtest("cannot login to unknown database"):
|
with subtest("cannot login to unknown database"):
|
||||||
machine.fail(peer_cmd("me", "notmine"), timeout=10)
|
machine.fail(peer_cmd("me", "notmine"), timeout=10)
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
peerAuth = lib.shb.runNixOSTest {
|
peerAuth = lib.shb.runNixOSTest {
|
||||||
name = "postgresql-peerAuth";
|
name = "postgresql-peerAuth";
|
||||||
|
|
||||||
nodes.machine = { config, pkgs, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
{
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
imports = [
|
||||||
../../modules/blocks/postgresql.nix
|
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
||||||
];
|
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
||||||
|
../../modules/blocks/postgresql.nix
|
||||||
|
];
|
||||||
|
|
||||||
users.users.me = {
|
users.users.me = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = "me";
|
group = "me";
|
||||||
extraGroups = [ "sudoers" ];
|
extraGroups = [ "sudoers" ];
|
||||||
|
};
|
||||||
|
users.groups.me = { };
|
||||||
|
|
||||||
|
shb.postgresql.ensures = [
|
||||||
|
{
|
||||||
|
username = "me";
|
||||||
|
database = "me";
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
users.groups.me = {};
|
|
||||||
|
|
||||||
shb.postgresql.ensures = [
|
testScript =
|
||||||
{
|
{ nodes, ... }:
|
||||||
username = "me";
|
''
|
||||||
database = "me";
|
start_all()
|
||||||
}
|
machine.wait_for_unit("postgresql.service")
|
||||||
];
|
machine.wait_for_open_port(5432)
|
||||||
};
|
|
||||||
|
|
||||||
testScript = { nodes, ... }: ''
|
def peer_cmd(user, database):
|
||||||
start_all()
|
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
|
||||||
machine.wait_for_unit("postgresql.service")
|
|
||||||
machine.wait_for_open_port(5432)
|
|
||||||
|
|
||||||
def peer_cmd(user, database):
|
def tcpip_cmd(user, database, port):
|
||||||
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
|
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):
|
with subtest("can login with provisioned user and database"):
|
||||||
return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port)
|
machine.succeed(peer_cmd("me", "me"), timeout=10)
|
||||||
|
|
||||||
with subtest("can login with provisioned user and database"):
|
with subtest("cannot login with unknown user"):
|
||||||
machine.succeed(peer_cmd("me", "me"), timeout=10)
|
machine.fail(peer_cmd("notme", "me"), timeout=10)
|
||||||
|
|
||||||
with subtest("cannot login with unknown user"):
|
with subtest("cannot login to unknown database"):
|
||||||
machine.fail(peer_cmd("notme", "me"), timeout=10)
|
machine.fail(peer_cmd("me", "notmine"), timeout=10)
|
||||||
|
|
||||||
with subtest("cannot login to unknown database"):
|
with subtest("cannot login with tcpip"):
|
||||||
machine.fail(peer_cmd("me", "notmine"), timeout=10)
|
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 {
|
tcpIPWithoutPasswordAuth = lib.shb.runNixOSTest {
|
||||||
name = "postgresql-tcpIpWithoutPasswordAuth";
|
name = "postgresql-tcpIpWithoutPasswordAuth";
|
||||||
|
|
||||||
nodes.machine = { config, pkgs, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
{
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
imports = [
|
||||||
../../modules/blocks/postgresql.nix
|
(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.enableTCPIP = true;
|
||||||
shb.postgresql.ensures = [
|
shb.postgresql.ensures = [
|
||||||
{
|
{
|
||||||
username = "me";
|
username = "me";
|
||||||
database = "me";
|
database = "me";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = { nodes, ... }: ''
|
testScript =
|
||||||
start_all()
|
{ nodes, ... }:
|
||||||
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(user, database):
|
def peer_cmd(user, database):
|
||||||
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
|
return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database)
|
||||||
|
|
||||||
def tcpip_cmd(user, database, 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)
|
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"):
|
with subtest("cannot login without existing user"):
|
||||||
machine.fail(peer_cmd("me", "me"), timeout=10)
|
machine.fail(peer_cmd("me", "me"), timeout=10)
|
||||||
|
|
||||||
with subtest("cannot login with user without password"):
|
with subtest("cannot login with user without password"):
|
||||||
machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10)
|
machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10)
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
tcpIPPasswordAuth = lib.shb.runNixOSTest {
|
tcpIPPasswordAuth = lib.shb.runNixOSTest {
|
||||||
name = "postgresql-tcpIPPasswordAuth";
|
name = "postgresql-tcpIPPasswordAuth";
|
||||||
|
|
||||||
nodes.machine = { config, pkgs, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
{
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
imports = [
|
||||||
../../modules/blocks/postgresql.nix
|
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
||||||
];
|
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
||||||
|
../../modules/blocks/postgresql.nix
|
||||||
|
];
|
||||||
|
|
||||||
users.users.me = {
|
users.users.me = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = "me";
|
group = "me";
|
||||||
extraGroups = [ "sudoers" ];
|
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 = ''
|
testScript =
|
||||||
echo secretpw > /run/dbsecret
|
{ 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)
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,179 +1,188 @@
|
||||||
{ pkgs, lib, ... }:
|
{ pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
testLib = pkgs.callPackage ../common.nix {};
|
testLib = pkgs.callPackage ../common.nix { };
|
||||||
|
|
||||||
commonTest = user: lib.shb.runNixOSTest {
|
commonTest =
|
||||||
name = "restic_backupAndRestore_${user}";
|
user:
|
||||||
|
lib.shb.runNixOSTest {
|
||||||
|
name = "restic_backupAndRestore_${user}";
|
||||||
|
|
||||||
nodes.machine = { config, ... }: {
|
nodes.machine =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
testLib.baseImports
|
{
|
||||||
|
imports = [
|
||||||
|
testLib.baseImports
|
||||||
|
|
||||||
../../modules/blocks/hardcodedsecret.nix
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
../../modules/blocks/restic.nix
|
../../modules/blocks/restic.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
shb.hardcodedsecret.A = {
|
shb.hardcodedsecret.A = {
|
||||||
request = {
|
request = {
|
||||||
owner = "root";
|
owner = "root";
|
||||||
group = "keys";
|
group = "keys";
|
||||||
mode = "0440";
|
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";
|
|
||||||
};
|
};
|
||||||
# Those are not needed by the repository but are still included
|
settings.content = "secretA";
|
||||||
# so we can test them in the hooks section.
|
};
|
||||||
secrets = {
|
shb.hardcodedsecret.B = {
|
||||||
A.source = config.shb.hardcodedsecret.A.result.path;
|
request = {
|
||||||
B.source = config.shb.hardcodedsecret.B.result.path;
|
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 = {
|
extraPythonPackages = p: [ p.dictdiffer ];
|
||||||
inherit user;
|
skipTypeCheck = true;
|
||||||
|
|
||||||
sourceDirectories = [
|
testScript =
|
||||||
"/opt/files/A"
|
{ nodes, ... }:
|
||||||
"/opt/files/B"
|
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
|
in
|
||||||
{
|
{
|
||||||
backupAndRestoreRoot = commonTest "root";
|
backupAndRestoreRoot = commonTest "root";
|
||||||
|
|
|
||||||
|
|
@ -6,95 +6,100 @@ in
|
||||||
test = lib.shb.runNixOSTest {
|
test = lib.shb.runNixOSTest {
|
||||||
name = "ssl-test";
|
name = "ssl-test";
|
||||||
|
|
||||||
nodes.server = { config, pkgs, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
{
|
||||||
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
imports = [
|
||||||
../../modules/blocks/ssl.nix
|
(pkgs'.path + "/nixos/modules/profiles/headless.nix")
|
||||||
];
|
(pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
|
||||||
|
../../modules/blocks/ssl.nix
|
||||||
|
];
|
||||||
|
|
||||||
users.users = {
|
users.users = {
|
||||||
user1 = {
|
user1 = {
|
||||||
group = "group1";
|
group = "group1";
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
};
|
|
||||||
user2 = {
|
|
||||||
group = "group2";
|
|
||||||
isSystemUser = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
users.groups = {
|
|
||||||
group1 = {};
|
|
||||||
group2 = {};
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.certs = {
|
|
||||||
cas.selfsigned = {
|
|
||||||
myca = {
|
|
||||||
name = "My CA";
|
|
||||||
};
|
};
|
||||||
myotherca = {
|
user2 = {
|
||||||
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";
|
|
||||||
group = "group2";
|
group = "group2";
|
||||||
|
isSystemUser = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
users.groups = {
|
||||||
|
group1 = { };
|
||||||
|
group2 = { };
|
||||||
|
};
|
||||||
|
|
||||||
# The configuration below is to create a webserver that uses the server certificate.
|
shb.certs = {
|
||||||
networking.hosts."127.0.0.1" = [
|
cas.selfsigned = {
|
||||||
"example.com"
|
myca = {
|
||||||
"subdomain.example.com"
|
name = "My CA";
|
||||||
"wrong.example.com"
|
};
|
||||||
"multi1.example.com"
|
myotherca = {
|
||||||
"multi2.example.com"
|
name = "My Other CA";
|
||||||
"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
|
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;
|
"example.com" = mkVirtualHost "Top domain" config.shb.certs.certs.selfsigned.top;
|
||||||
"subdomain.example.com" = mkVirtualHost "Subdomain" config.shb.certs.certs.selfsigned.subdomain;
|
"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;
|
"multi2.example.com" = mkVirtualHost "multi2" config.shb.certs.certs.selfsigned.multi;
|
||||||
"multi3.example.com" = mkVirtualHost "multi3" config.shb.certs.certs.selfsigned.multi;
|
"multi3.example.com" = mkVirtualHost "multi3" config.shb.certs.certs.selfsigned.multi;
|
||||||
};
|
};
|
||||||
systemd.services.nginx = {
|
systemd.services.nginx = {
|
||||||
after = [
|
after = [
|
||||||
config.shb.certs.certs.selfsigned.top.systemdService
|
config.shb.certs.certs.selfsigned.top.systemdService
|
||||||
config.shb.certs.certs.selfsigned.subdomain.systemdService
|
config.shb.certs.certs.selfsigned.subdomain.systemdService
|
||||||
config.shb.certs.certs.selfsigned.multi.systemdService
|
config.shb.certs.certs.selfsigned.multi.systemdService
|
||||||
config.shb.certs.certs.selfsigned.cert1.systemdService
|
config.shb.certs.certs.selfsigned.cert1.systemdService
|
||||||
config.shb.certs.certs.selfsigned.cert2.systemdService
|
config.shb.certs.certs.selfsigned.cert2.systemdService
|
||||||
];
|
];
|
||||||
requires = [
|
requires = [
|
||||||
config.shb.certs.certs.selfsigned.top.systemdService
|
config.shb.certs.certs.selfsigned.top.systemdService
|
||||||
config.shb.certs.certs.selfsigned.subdomain.systemdService
|
config.shb.certs.certs.selfsigned.subdomain.systemdService
|
||||||
config.shb.certs.certs.selfsigned.multi.systemdService
|
config.shb.certs.certs.selfsigned.multi.systemdService
|
||||||
config.shb.certs.certs.selfsigned.cert1.systemdService
|
config.shb.certs.certs.selfsigned.cert1.systemdService
|
||||||
config.shb.certs.certs.selfsigned.cert2.systemdService
|
config.shb.certs.certs.selfsigned.cert2.systemdService
|
||||||
];
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
|
# Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
|
||||||
testScript = { nodes, ... }:
|
testScript =
|
||||||
|
{ nodes, ... }:
|
||||||
let
|
let
|
||||||
myca = nodes.server.shb.certs.cas.selfsigned.myca;
|
myca = nodes.server.shb.certs.cas.selfsigned.myca;
|
||||||
myotherca = nodes.server.shb.certs.cas.selfsigned.myotherca;
|
myotherca = nodes.server.shb.certs.cas.selfsigned.myotherca;
|
||||||
|
|
@ -131,7 +137,7 @@ in
|
||||||
cert1 = nodes.server.shb.certs.certs.selfsigned.cert1;
|
cert1 = nodes.server.shb.certs.certs.selfsigned.cert1;
|
||||||
cert2 = nodes.server.shb.certs.certs.selfsigned.cert2;
|
cert2 = nodes.server.shb.certs.certs.selfsigned.cert2;
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
# Make sure certs are generated.
|
# 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-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://example.com")
|
||||||
server.fail("curl --cacert /etc/static/ssl/certs/ca-certificates.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://subdomain.example.com")
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
911
test/common.nix
911
test/common.nix
|
|
@ -1,7 +1,14 @@
|
||||||
{ pkgs, lib }:
|
{ pkgs, lib }:
|
||||||
let
|
let
|
||||||
inherit (lib) hasAttr mkOption optionalString;
|
inherit (lib) hasAttr mkOption optionalString;
|
||||||
inherit (lib.types) bool enum listOf nullOr submodule str;
|
inherit (lib.types)
|
||||||
|
bool
|
||||||
|
enum
|
||||||
|
listOf
|
||||||
|
nullOr
|
||||||
|
submodule
|
||||||
|
str
|
||||||
|
;
|
||||||
|
|
||||||
baseImports = {
|
baseImports = {
|
||||||
imports = [
|
imports = [
|
||||||
|
|
@ -10,15 +17,17 @@ let
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
accessScript = lib.makeOverridable ({
|
accessScript = lib.makeOverridable (
|
||||||
hasSSL
|
{
|
||||||
, waitForServices ? s: []
|
hasSSL,
|
||||||
, waitForPorts ? p: []
|
waitForServices ? s: [ ],
|
||||||
, waitForUnixSocket ? u: []
|
waitForPorts ? p: [ ],
|
||||||
, waitForUrls ? u: []
|
waitForUnixSocket ? u: [ ],
|
||||||
, extraScript ? {...}: ""
|
waitForUrls ? u: [ ],
|
||||||
, redirectSSO ? false
|
extraScript ? { ... }: "",
|
||||||
}: { nodes, ... }:
|
redirectSSO ? false,
|
||||||
|
}:
|
||||||
|
{ nodes, ... }:
|
||||||
let
|
let
|
||||||
cfg = nodes.server.test;
|
cfg = nodes.server.test;
|
||||||
|
|
||||||
|
|
@ -35,34 +44,34 @@ let
|
||||||
lldapEnabled = (hasAttr "lldap" nodes.server.shb) && nodes.server.shb.lldap.enable;
|
lldapEnabled = (hasAttr "lldap" nodes.server.shb) && nodes.server.shb.lldap.enable;
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
def curl(target, format, endpoint, data="", extra=""):
|
def curl(target, format, endpoint, data="", extra=""):
|
||||||
cmd = ("curl --show-error --location"
|
cmd = ("curl --show-error --location"
|
||||||
+ " --cookie-jar cookie.txt"
|
+ " --cookie-jar cookie.txt"
|
||||||
+ " --cookie cookie.txt"
|
+ " --cookie cookie.txt"
|
||||||
+ " --connect-to ${fqdn}:443:server:443"
|
+ " --connect-to ${fqdn}:443:server:443"
|
||||||
+ " --connect-to ${fqdn}:80:server:80"
|
+ " --connect-to ${fqdn}:80:server:80"
|
||||||
# Client must be able to resolve talking to auth server
|
# Client must be able to resolve talking to auth server
|
||||||
+ " --connect-to auth.${cfg.domain}:443:server:443"
|
+ " --connect-to auth.${cfg.domain}:443:server:443"
|
||||||
+ (f" --data '{data}'" if data != "" else "")
|
+ (f" --data '{data}'" if data != "" else "")
|
||||||
+ (f" --silent --output /dev/null --write-out '{format}'" if format != "" else "")
|
+ (f" --silent --output /dev/null --write-out '{format}'" if format != "" else "")
|
||||||
+ (f" {extra}" if extra != "" else "")
|
+ (f" {extra}" if extra != "" else "")
|
||||||
+ f" {endpoint}")
|
+ f" {endpoint}")
|
||||||
print(cmd)
|
print(cmd)
|
||||||
_, r = target.execute(cmd)
|
_, r = target.execute(cmd)
|
||||||
print(r)
|
print(r)
|
||||||
try:
|
try:
|
||||||
return json.loads(r)
|
return json.loads(r)
|
||||||
except:
|
except:
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def unline_with(j, s):
|
def unline_with(j, s):
|
||||||
return j.join((x.strip() for x in s.split("\n")))
|
return j.join((x.strip() for x in s.split("\n")))
|
||||||
''
|
''
|
||||||
+ lib.strings.concatMapStrings (s: ''server.wait_for_unit("${s}")'' + "\n") (
|
+ lib.strings.concatMapStrings (s: ''server.wait_for_unit("${s}")'' + "\n") (
|
||||||
waitForServices args
|
waitForServices args
|
||||||
|
|
@ -72,19 +81,25 @@ let
|
||||||
+ lib.strings.concatMapStrings (p: ''server.wait_for_open_port(${toString p})'' + "\n") (
|
+ lib.strings.concatMapStrings (p: ''server.wait_for_open_port(${toString p})'' + "\n") (
|
||||||
waitForPorts args
|
waitForPorts args
|
||||||
# TODO: when the SSO block exists, replace this hardcoded port.
|
# TODO: when the SSO block exists, replace this hardcoded port.
|
||||||
++ (lib.optionals autheliaEnabled [ 9091 /* nodes.server.services.authelia.instances."auth.${domain}".settings.server.port */ ])
|
++ (lib.optionals autheliaEnabled [
|
||||||
|
9091 # nodes.server.services.authelia.instances."auth.${domain}".settings.server.port
|
||||||
|
])
|
||||||
|
)
|
||||||
|
+ lib.strings.concatMapStrings (u: ''server.wait_for_open_unix_socket("${u}")'' + "\n") (
|
||||||
|
waitForUnixSocket args
|
||||||
)
|
)
|
||||||
+ lib.strings.concatMapStrings (u: ''server.wait_for_open_unix_socket("${u}")'' + "\n") (waitForUnixSocket args)
|
|
||||||
+ ''
|
+ ''
|
||||||
if ${if hasSSL args then "True" else "False"}:
|
if ${if hasSSL args then "True" else "False"}:
|
||||||
server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
|
server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
|
||||||
client.succeed("rm -r /etc/ssl/certs")
|
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")
|
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,
|
# Making a curl request to an URL needs to happen after we copied the certificates over,
|
||||||
# otherwise curl will not be able to verify the "legitimacy of the server".
|
# otherwise curl will not be able to verify the "legitimacy of the server".
|
||||||
+ lib.strings.concatMapStrings (u: ''
|
+ lib.strings.concatMapStrings (
|
||||||
|
u:
|
||||||
|
''
|
||||||
import time
|
import time
|
||||||
|
|
||||||
done = False
|
done = False
|
||||||
|
|
@ -97,416 +112,472 @@ let
|
||||||
done = response.get('code') == 200
|
done = response.get('code') == 200
|
||||||
if not done:
|
if not done:
|
||||||
raise Exception(f"Response was never 200, got last: {response}")
|
raise Exception(f"Response was never 200, got last: {response}")
|
||||||
'' + "\n") (
|
''
|
||||||
waitForUrls args
|
+ "\n"
|
||||||
|
) (waitForUrls args)
|
||||||
|
+ (
|
||||||
|
if (!redirectSSO) then
|
||||||
|
''
|
||||||
|
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) ''
|
+ (optionalString (hasAttr "test" nodes.server && hasAttr "login" nodes.server.test) ''
|
||||||
with subtest("Login from server"):
|
with subtest("Login from server"):
|
||||||
code, logs = server.execute("login_playwright")
|
code, logs = server.execute("login_playwright")
|
||||||
print(logs)
|
print(logs)
|
||||||
try:
|
try:
|
||||||
server.copy_from_vm("trace")
|
server.copy_from_vm("trace")
|
||||||
except:
|
except:
|
||||||
print("No trace found on server")
|
print("No trace found on server")
|
||||||
if code != 0:
|
if code != 0:
|
||||||
raise Exception("login_playwright did not succeed")
|
raise Exception("login_playwright did not succeed")
|
||||||
'')
|
'')
|
||||||
+ (optionalString (hasAttr "test" nodes.client && hasAttr "login" nodes.client.test) ''
|
+ (optionalString (hasAttr "test" nodes.client && hasAttr "login" nodes.client.test) ''
|
||||||
with subtest("Login from client"):
|
with subtest("Login from client"):
|
||||||
code, logs = client.execute("login_playwright")
|
code, logs = client.execute("login_playwright")
|
||||||
print(logs)
|
print(logs)
|
||||||
try:
|
try:
|
||||||
client.copy_from_vm("trace")
|
client.copy_from_vm("trace")
|
||||||
except:
|
except:
|
||||||
print("No trace found on client")
|
print("No trace found on client")
|
||||||
if code != 0:
|
if code != 0:
|
||||||
raise Exception("login_playwright did not succeed")
|
raise Exception("login_playwright did not succeed")
|
||||||
'')
|
'')
|
||||||
);
|
);
|
||||||
|
|
||||||
backupScript = args: (accessScript args).override {
|
backupScript =
|
||||||
extraScript = { proto_fqdn, ... }: ''
|
args:
|
||||||
with subtest("backup"):
|
(accessScript args).override {
|
||||||
server.succeed("systemctl start restic-backups-testinstance_opt_repos_A")
|
extraScript =
|
||||||
'';
|
{ proto_fqdn, ... }:
|
||||||
};
|
''
|
||||||
|
with subtest("backup"):
|
||||||
|
server.succeed("systemctl start restic-backups-testinstance_opt_repos_A")
|
||||||
|
'';
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit baseImports accessScript;
|
inherit baseImports accessScript;
|
||||||
|
|
||||||
runNixOSTest = args: pkgs.testers.runNixOSTest ({
|
runNixOSTest =
|
||||||
interactive.sshBackdoor.enable = true;
|
args:
|
||||||
} // args);
|
pkgs.testers.runNixOSTest (
|
||||||
|
{
|
||||||
|
interactive.sshBackdoor.enable = true;
|
||||||
|
}
|
||||||
|
// args
|
||||||
|
);
|
||||||
|
|
||||||
mkScripts = args:
|
mkScripts = args: {
|
||||||
|
access = accessScript args;
|
||||||
|
backup = backupScript args;
|
||||||
|
};
|
||||||
|
|
||||||
|
baseModule =
|
||||||
|
{ config, ... }:
|
||||||
{
|
{
|
||||||
access = accessScript args;
|
options.test = {
|
||||||
backup = backupScript args;
|
domain = mkOption {
|
||||||
};
|
type = str;
|
||||||
|
default = "example.com";
|
||||||
baseModule = { config, ... }: {
|
};
|
||||||
options.test = {
|
subdomain = mkOption {
|
||||||
domain = mkOption {
|
type = str;
|
||||||
type = str;
|
};
|
||||||
default = "example.com";
|
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 {
|
imports = [
|
||||||
type = str;
|
baseImports
|
||||||
};
|
../modules/blocks/authelia.nix
|
||||||
fqdn = mkOption {
|
../modules/blocks/hardcodedsecret.nix
|
||||||
type = str;
|
../modules/blocks/mitmdump.nix
|
||||||
readOnly = true;
|
../modules/blocks/nginx.nix
|
||||||
default = "${config.test.subdomain}.${config.test.domain}";
|
../modules/blocks/postgresql.nix
|
||||||
};
|
|
||||||
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()
|
|
||||||
'')
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
};
|
config = {
|
||||||
};
|
# HTTP(s) server port.
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
80
|
||||||
|
443
|
||||||
|
];
|
||||||
|
shb.nginx.accessLog = true;
|
||||||
|
|
||||||
backup = backupOption: { config, ... }: {
|
networking.hosts = {
|
||||||
imports = [
|
"192.168.1.2" = [
|
||||||
../modules/blocks/restic.nix
|
config.test.fqdn
|
||||||
];
|
"auth.${config.test.domain}"
|
||||||
shb.restic.instances."testinstance" = {
|
];
|
||||||
request = backupOption.request;
|
};
|
||||||
settings = {
|
};
|
||||||
enable = true;
|
};
|
||||||
passphrase.result = config.shb.hardcodedsecret.backupPassphrase.result;
|
|
||||||
repository = {
|
clientLoginModule =
|
||||||
path = "/opt/repos/A";
|
{ config, pkgs, ... }:
|
||||||
timerConfig = {
|
let
|
||||||
OnCalendar = "00:00:00";
|
cfg = config.test.login;
|
||||||
RandomizedDelaySec = "5h";
|
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 = {
|
||||||
shb.hardcodedsecret.backupPassphrase = {
|
request = config.shb.restic.instances."testinstance".settings.passphrase.request;
|
||||||
request = config.shb.restic.instances."testinstance".settings.passphrase.request;
|
settings.content = "PassPhrase";
|
||||||
settings.content = "PassPhrase";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
certs =
|
||||||
domain = "*.${config.test.domain}";
|
{ config, ... }:
|
||||||
group = "nginx";
|
{
|
||||||
|
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 ];
|
sso =
|
||||||
systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
|
ssl:
|
||||||
};
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../modules/blocks/authelia.nix
|
||||||
|
];
|
||||||
|
|
||||||
ldap = { config, pkgs, ... }: {
|
networking.hosts = {
|
||||||
imports = [
|
"127.0.0.1" = [ "${config.shb.authelia.subdomain}.${config.shb.authelia.domain}" ];
|
||||||
../modules/blocks/lldap.nix
|
};
|
||||||
];
|
|
||||||
|
|
||||||
networking.hosts = {
|
shb.authelia = {
|
||||||
"127.0.0.1" = [ "ldap.${config.test.domain}" ];
|
enable = true;
|
||||||
};
|
inherit (config.test) domain;
|
||||||
|
subdomain = "auth";
|
||||||
|
ssl = config.shb.certs.certs.selfsigned.n;
|
||||||
|
debug = true;
|
||||||
|
|
||||||
shb.hardcodedsecret.ldapUserPassword = {
|
ldapHostname = "127.0.0.1";
|
||||||
request = config.shb.lldap.ldapUserPassword.request;
|
ldapPort = config.shb.lldap.ldapPort;
|
||||||
settings.content = "ldapUserPassword";
|
dcdomain = config.shb.lldap.dcdomain;
|
||||||
};
|
|
||||||
shb.hardcodedsecret.jwtSecret = {
|
|
||||||
request = config.shb.lldap.jwtSecret.request;
|
|
||||||
settings.content = "jwtSecrets";
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.lldap = {
|
secrets = {
|
||||||
enable = true;
|
jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result;
|
||||||
inherit (config.test) domain;
|
ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result;
|
||||||
subdomain = "ldap";
|
sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result;
|
||||||
ldapPort = 3890;
|
storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result;
|
||||||
webUIListenPort = 17170;
|
identityProvidersOIDCHMACSecret.result =
|
||||||
dcdomain = "dc=example,dc=com";
|
config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result;
|
||||||
ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result;
|
identityProvidersOIDCIssuerPrivateKey.result =
|
||||||
jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result;
|
config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.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 = {
|
shb.hardcodedsecret.autheliaJwtSecret = {
|
||||||
user_group = {};
|
request = config.shb.authelia.secrets.jwtSecret.request;
|
||||||
admin_group = {};
|
settings.content = "jwtSecret";
|
||||||
other_group = {};
|
};
|
||||||
|
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";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,75 @@
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
let
|
let
|
||||||
contracts = pkgs.callPackage ../../modules/contracts {};
|
contracts = pkgs.callPackage ../../modules/contracts { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
restic_root = contracts.test.backup {
|
restic_root = contracts.test.backup {
|
||||||
name = "restic_root";
|
name = "restic_root";
|
||||||
username = "root";
|
username = "root";
|
||||||
providerRoot = [ "shb" "restic" "instances" "mytest" ];
|
providerRoot = [
|
||||||
|
"shb"
|
||||||
|
"restic"
|
||||||
|
"instances"
|
||||||
|
"mytest"
|
||||||
|
];
|
||||||
modules = [
|
modules = [
|
||||||
../../modules/blocks/restic.nix
|
../../modules/blocks/restic.nix
|
||||||
../../modules/blocks/hardcodedsecret.nix
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
];
|
];
|
||||||
settings = { repository, config, ... }: {
|
settings =
|
||||||
enable = true;
|
{ repository, config, ... }:
|
||||||
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
|
{
|
||||||
repository = {
|
enable = true;
|
||||||
path = repository;
|
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
|
||||||
timerConfig = {
|
repository = {
|
||||||
OnCalendar = "00:00:00";
|
path = repository;
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "00:00:00";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
extraConfig =
|
||||||
extraConfig = { username, config, ... }: {
|
{ username, config, ... }:
|
||||||
shb.hardcodedsecret.passphrase = {
|
{
|
||||||
request = config.shb.restic.instances."mytest".settings.passphrase.request;
|
shb.hardcodedsecret.passphrase = {
|
||||||
settings.content = "passphrase";
|
request = config.shb.restic.instances."mytest".settings.passphrase.request;
|
||||||
|
settings.content = "passphrase";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
restic_nonroot = contracts.test.backup {
|
restic_nonroot = contracts.test.backup {
|
||||||
name = "restic_nonroot";
|
name = "restic_nonroot";
|
||||||
username = "me";
|
username = "me";
|
||||||
providerRoot = [ "shb" "restic" "instances" "mytest" ];
|
providerRoot = [
|
||||||
|
"shb"
|
||||||
|
"restic"
|
||||||
|
"instances"
|
||||||
|
"mytest"
|
||||||
|
];
|
||||||
modules = [
|
modules = [
|
||||||
../../modules/blocks/restic.nix
|
../../modules/blocks/restic.nix
|
||||||
../../modules/blocks/hardcodedsecret.nix
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
];
|
];
|
||||||
settings = { repository, config, ... }: {
|
settings =
|
||||||
enable = true;
|
{ repository, config, ... }:
|
||||||
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
|
{
|
||||||
repository = {
|
enable = true;
|
||||||
path = repository;
|
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
|
||||||
timerConfig = {
|
repository = {
|
||||||
OnCalendar = "00:00:00";
|
path = repository;
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "00:00:00";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
extraConfig =
|
||||||
extraConfig = { username, config, ... }: {
|
{ username, config, ... }:
|
||||||
shb.hardcodedsecret.passphrase = {
|
{
|
||||||
request = config.shb.restic.instances."mytest".settings.passphrase.request;
|
shb.hardcodedsecret.passphrase = {
|
||||||
settings.content = "passphrase";
|
request = config.shb.restic.instances."mytest".settings.passphrase.request;
|
||||||
|
settings.content = "passphrase";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,51 @@
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
let
|
let
|
||||||
contracts = pkgs.callPackage ../../modules/contracts {};
|
contracts = pkgs.callPackage ../../modules/contracts { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
restic_postgres = contracts.test.databasebackup {
|
restic_postgres = contracts.test.databasebackup {
|
||||||
name = "restic_postgres";
|
name = "restic_postgres";
|
||||||
requesterRoot = [ "shb" "postgresql" "databasebackup" ];
|
requesterRoot = [
|
||||||
providerRoot = [ "shb" "restic" "databases" "postgresql" ];
|
"shb"
|
||||||
|
"postgresql"
|
||||||
|
"databasebackup"
|
||||||
|
];
|
||||||
|
providerRoot = [
|
||||||
|
"shb"
|
||||||
|
"restic"
|
||||||
|
"databases"
|
||||||
|
"postgresql"
|
||||||
|
];
|
||||||
modules = [
|
modules = [
|
||||||
../../modules/blocks/postgresql.nix
|
../../modules/blocks/postgresql.nix
|
||||||
../../modules/blocks/restic.nix
|
../../modules/blocks/restic.nix
|
||||||
../../modules/blocks/hardcodedsecret.nix
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
];
|
];
|
||||||
settings = { repository, config, ... }: {
|
settings =
|
||||||
enable = true;
|
{ repository, config, ... }:
|
||||||
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
|
{
|
||||||
repository = {
|
enable = true;
|
||||||
path = repository;
|
passphrase.result = config.shb.hardcodedsecret.passphrase.result;
|
||||||
timerConfig = {
|
repository = {
|
||||||
OnCalendar = "00:00:00";
|
path = repository;
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "00:00:00";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
extraConfig =
|
||||||
extraConfig = { config, database, ... }: {
|
{ config, database, ... }:
|
||||||
shb.postgresql.ensures = [
|
{
|
||||||
{
|
shb.postgresql.ensures = [
|
||||||
inherit database;
|
{
|
||||||
username = database;
|
inherit database;
|
||||||
}
|
username = database;
|
||||||
];
|
}
|
||||||
shb.hardcodedsecret.passphrase = {
|
];
|
||||||
request = config.shb.restic.databases.postgresql.settings.passphrase.request;
|
shb.hardcodedsecret.passphrase = {
|
||||||
settings.content = "passphrase";
|
request = config.shb.restic.databases.postgresql.settings.passphrase.request;
|
||||||
|
settings.content = "passphrase";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
let
|
let
|
||||||
contracts = pkgs.callPackage ../../modules/contracts {};
|
contracts = pkgs.callPackage ../../modules/contracts { };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
hardcoded_root_root = contracts.test.secret {
|
hardcoded_root_root = contracts.test.secret {
|
||||||
name = "hardcoded";
|
name = "hardcoded";
|
||||||
modules = [ ../../modules/blocks/hardcodedsecret.nix ];
|
modules = [ ../../modules/blocks/hardcodedsecret.nix ];
|
||||||
configRoot = [ "shb" "hardcodedsecret" ];
|
configRoot = [
|
||||||
|
"shb"
|
||||||
|
"hardcodedsecret"
|
||||||
|
];
|
||||||
settingsCfg = secret: {
|
settingsCfg = secret: {
|
||||||
content = secret;
|
content = secret;
|
||||||
};
|
};
|
||||||
|
|
@ -15,7 +18,10 @@ in
|
||||||
hardcoded_user_group = contracts.test.secret {
|
hardcoded_user_group = contracts.test.secret {
|
||||||
name = "hardcoded";
|
name = "hardcoded";
|
||||||
modules = [ ../../modules/blocks/hardcodedsecret.nix ];
|
modules = [ ../../modules/blocks/hardcodedsecret.nix ];
|
||||||
configRoot = [ "shb" "hardcodedsecret" ];
|
configRoot = [
|
||||||
|
"shb"
|
||||||
|
"hardcodedsecret"
|
||||||
|
];
|
||||||
settingsCfg = secret: {
|
settingsCfg = secret: {
|
||||||
content = secret;
|
content = secret;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,31 @@
|
||||||
{ pkgs, lib, ... }:
|
{ pkgs, lib, ... }:
|
||||||
let
|
let
|
||||||
anyOpt = default: lib.mkOption {
|
anyOpt =
|
||||||
type = lib.types.anything;
|
default:
|
||||||
inherit default;
|
lib.mkOption {
|
||||||
};
|
type = lib.types.anything;
|
||||||
|
inherit default;
|
||||||
|
};
|
||||||
|
|
||||||
testConfig = m:
|
testConfig =
|
||||||
|
m:
|
||||||
let
|
let
|
||||||
cfg = (lib.evalModules {
|
cfg =
|
||||||
specialArgs = { inherit pkgs; };
|
(lib.evalModules {
|
||||||
modules = [
|
specialArgs = { inherit pkgs; };
|
||||||
{
|
modules = [
|
||||||
options = {
|
{
|
||||||
systemd = anyOpt {};
|
options = {
|
||||||
services = anyOpt {};
|
systemd = anyOpt { };
|
||||||
};
|
services = anyOpt { };
|
||||||
}
|
};
|
||||||
../../modules/blocks/davfs.nix
|
}
|
||||||
m
|
../../modules/blocks/davfs.nix
|
||||||
];
|
m
|
||||||
}).config;
|
];
|
||||||
in {
|
}).config;
|
||||||
|
in
|
||||||
|
{
|
||||||
inherit (cfg) systemd services;
|
inherit (cfg) systemd services;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
@ -28,8 +33,8 @@ in
|
||||||
testDavfsNoOptions = {
|
testDavfsNoOptions = {
|
||||||
expected = {
|
expected = {
|
||||||
services.davfs2.enable = false;
|
services.davfs2.enable = false;
|
||||||
systemd.mounts = [];
|
systemd.mounts = [ ];
|
||||||
};
|
};
|
||||||
expr = testConfig {};
|
expr = testConfig { };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,12 @@ in
|
||||||
c = "%SECRET_${root}C%";
|
c = "%SECRET_${root}C%";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
(item "") // {
|
(item "")
|
||||||
nestedAttr = item "NESTEDATTR_";
|
// {
|
||||||
nestedList = [ (item "NESTEDLIST_0_") ];
|
nestedAttr = item "NESTEDATTR_";
|
||||||
doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ];
|
nestedList = [ (item "NESTEDLIST_0_") ];
|
||||||
};
|
doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ];
|
||||||
|
};
|
||||||
expr =
|
expr =
|
||||||
let
|
let
|
||||||
item = {
|
item = {
|
||||||
|
|
@ -33,13 +34,14 @@ in
|
||||||
c.other = "other";
|
c.other = "other";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.shb.withReplacements (
|
lib.shb.withReplacements (
|
||||||
item // {
|
item
|
||||||
nestedAttr = item;
|
// {
|
||||||
nestedList = [ item ];
|
nestedAttr = item;
|
||||||
doubleNestedList = [ { n = item; } ];
|
nestedList = [ item ];
|
||||||
}
|
doubleNestedList = [ { n = item; } ];
|
||||||
);
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
testLibWithReplacementsRootList = {
|
testLibWithReplacementsRootList = {
|
||||||
|
|
@ -51,12 +53,12 @@ in
|
||||||
c = "%SECRET_${root}C%";
|
c = "%SECRET_${root}C%";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
[
|
[
|
||||||
(item "0_")
|
(item "0_")
|
||||||
(item "1_")
|
(item "1_")
|
||||||
[ (item "2_0_") ]
|
[ (item "2_0_") ]
|
||||||
[ { n = (item "3_0_N_"); } ]
|
[ { n = (item "3_0_N_"); } ]
|
||||||
];
|
];
|
||||||
expr =
|
expr =
|
||||||
let
|
let
|
||||||
item = {
|
item = {
|
||||||
|
|
@ -68,12 +70,12 @@ in
|
||||||
c.other = "other";
|
c.other = "other";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
lib.shb.withReplacements [
|
lib.shb.withReplacements [
|
||||||
item
|
item
|
||||||
item
|
item
|
||||||
[ item ]
|
[ item ]
|
||||||
[ { n = item; } ]
|
[ { n = item; } ]
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
testLibGetReplacements = {
|
testLibGetReplacements = {
|
||||||
|
|
@ -84,10 +86,10 @@ in
|
||||||
(nameValuePair "%SECRET_${root}C%" "prefix-$(cat /path/C)-suffix")
|
(nameValuePair "%SECRET_${root}C%" "prefix-$(cat /path/C)-suffix")
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
(secrets "") ++
|
(secrets "")
|
||||||
(secrets "DOUBLENESTEDLIST_0_N_") ++
|
++ (secrets "DOUBLENESTEDLIST_0_N_")
|
||||||
(secrets "NESTEDATTR_") ++
|
++ (secrets "NESTEDATTR_")
|
||||||
(secrets "NESTEDLIST_0_");
|
++ (secrets "NESTEDLIST_0_");
|
||||||
expr =
|
expr =
|
||||||
let
|
let
|
||||||
item = {
|
item = {
|
||||||
|
|
@ -99,13 +101,16 @@ in
|
||||||
c.other = "other";
|
c.other = "other";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
map lib.shb.genReplacement (lib.shb.getReplacements (
|
map lib.shb.genReplacement (
|
||||||
item // {
|
lib.shb.getReplacements (
|
||||||
|
item
|
||||||
|
// {
|
||||||
nestedAttr = item;
|
nestedAttr = item;
|
||||||
nestedList = [ item ];
|
nestedList = [ item ];
|
||||||
doubleNestedList = [ { n = item; } ];
|
doubleNestedList = [ { n = item; } ];
|
||||||
}
|
}
|
||||||
));
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
testParseXML = {
|
testParseXML = {
|
||||||
|
|
@ -119,10 +124,10 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
expr = lib.shb.parseXML ''
|
expr = lib.shb.parseXML ''
|
||||||
<a>
|
<a>
|
||||||
<b>1</b>
|
<b>1</b>
|
||||||
<c><d>1</d></c>
|
<c><d>1</d></c>
|
||||||
</a>
|
</a>
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,163 +4,206 @@ let
|
||||||
loginUrl = "/UI/Login";
|
loginUrl = "/UI/Login";
|
||||||
|
|
||||||
# TODO: Test login
|
# TODO: Test login
|
||||||
commonTestScript = appname: cfgPathFn: lib.shb.mkScripts {
|
commonTestScript =
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.arr.${appname}.ssl);
|
appname: cfgPathFn:
|
||||||
waitForServices = { ... }: [
|
lib.shb.mkScripts {
|
||||||
"${appname}.service"
|
hasSSL = { node, ... }: !(isNull node.config.shb.arr.${appname}.ssl);
|
||||||
"nginx.service"
|
waitForServices =
|
||||||
];
|
{ ... }:
|
||||||
waitForPorts = { node, ... }: [
|
[
|
||||||
node.config.shb.arr.${appname}.settings.Port
|
"${appname}.service"
|
||||||
];
|
"nginx.service"
|
||||||
extraScript = { node, fqdn, proto_fqdn, ... }: let
|
];
|
||||||
shbapp = node.config.shb.arr.${appname};
|
waitForPorts =
|
||||||
cfgPath = cfgPathFn shbapp;
|
{ node, ... }:
|
||||||
apiKey = if (shbapp.settings ? ApiKey) then "01234567890123456789" else null;
|
[
|
||||||
in ''
|
node.config.shb.arr.${appname}.settings.Port
|
||||||
# These curl requests still return a 200 even with sso redirect.
|
];
|
||||||
with subtest("health"):
|
extraScript =
|
||||||
response = curl(client, """{"code":%{response_code}}""", "${fqdn}${healthUrl}")
|
{
|
||||||
print("response =", response)
|
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:
|
if response['code'] != 200:
|
||||||
raise Exception(f"Code is {response['code']}")
|
raise Exception(f"Code is {response['code']}")
|
||||||
|
|
||||||
with subtest("login"):
|
with subtest("login"):
|
||||||
response = curl(client, """{"code":%{response_code}}""", "${fqdn}${loginUrl}")
|
response = curl(client, """{"code":%{response_code}}""", "${fqdn}${loginUrl}")
|
||||||
|
|
||||||
if response['code'] != 200:
|
if response['code'] != 200:
|
||||||
raise Exception(f"Code is {response['code']}")
|
raise Exception(f"Code is {response['code']}")
|
||||||
'' + lib.optionalString (apiKey != null) ''
|
''
|
||||||
|
+ lib.optionalString (apiKey != null) ''
|
||||||
|
|
||||||
with subtest("apikey"):
|
with subtest("apikey"):
|
||||||
config = server.succeed("cat ${cfgPath}")
|
config = server.succeed("cat ${cfgPath}")
|
||||||
if "${apiKey}" not in config:
|
if "${apiKey}" not in config:
|
||||||
raise Exception(f"Unexpected API Key. Want '${apiKey}', got '{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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.arr.${appname} = {
|
basic =
|
||||||
enable = true;
|
appname:
|
||||||
inherit (config.test) subdomain domain;
|
{ config, ... }:
|
||||||
|
{
|
||||||
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 = {
|
|
||||||
imports = [
|
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 = [
|
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 {
|
nodes.client = {
|
||||||
name = "arr_${appname}_backup";
|
imports = [
|
||||||
|
(clientLogin appname)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
nodes.server = {
|
||||||
|
imports = [
|
||||||
|
(basic appname)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
testScript = (commonTestScript appname cfgPathFn).access;
|
||||||
imports = [
|
|
||||||
(basic appname)
|
|
||||||
(lib.shb.backup config.shb.arr.${appname}.backup)
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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, ...}: {
|
nodes.client = { };
|
||||||
shb.arr.${appname} = {
|
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
httpsTest = appname: cfgPathFn: lib.shb.runNixOSTest {
|
testScript = (commonTestScript appname cfgPathFn).backup;
|
||||||
name = "arr_${appname}_https";
|
|
||||||
|
|
||||||
nodes.server = { config, pkgs, ... }: {
|
|
||||||
imports = [
|
|
||||||
(basic appname)
|
|
||||||
lib.shb.certs
|
|
||||||
(https appname)
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
https =
|
||||||
|
appname:
|
||||||
testScript = (commonTestScript appname cfgPathFn).access;
|
{ config, ... }:
|
||||||
};
|
{
|
||||||
|
shb.arr.${appname} = {
|
||||||
sso = appname: { config, ...}: {
|
ssl = config.shb.certs.certs.selfsigned.n;
|
||||||
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 = {};
|
httpsTest =
|
||||||
|
appname: cfgPathFn:
|
||||||
|
lib.shb.runNixOSTest {
|
||||||
|
name = "arr_${appname}_https";
|
||||||
|
|
||||||
testScript = (commonTestScript appname cfgPathFn).access.override {
|
nodes.server =
|
||||||
redirectSSO = true;
|
{ 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";
|
radarrCfgFn = cfg: "${cfg.dataDir}/config.xml";
|
||||||
sonarrCfgFn = cfg: "${cfg.dataDir}/config.xml";
|
sonarrCfgFn = cfg: "${cfg.dataDir}/config.xml";
|
||||||
|
|
@ -170,33 +213,33 @@ let
|
||||||
jackettCfgFn = cfg: "${cfg.dataDir}/ServerConfig.json";
|
jackettCfgFn = cfg: "${cfg.dataDir}/ServerConfig.json";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
radarr_basic = basicTest "radarr" radarrCfgFn;
|
radarr_basic = basicTest "radarr" radarrCfgFn;
|
||||||
radarr_backup = backupTest "radarr" radarrCfgFn;
|
radarr_backup = backupTest "radarr" radarrCfgFn;
|
||||||
radarr_https = httpsTest "radarr" radarrCfgFn;
|
radarr_https = httpsTest "radarr" radarrCfgFn;
|
||||||
radarr_sso = ssoTest "radarr" radarrCfgFn;
|
radarr_sso = ssoTest "radarr" radarrCfgFn;
|
||||||
|
|
||||||
sonarr_basic = basicTest "sonarr" sonarrCfgFn;
|
sonarr_basic = basicTest "sonarr" sonarrCfgFn;
|
||||||
sonarr_backup = backupTest "sonarr" sonarrCfgFn;
|
sonarr_backup = backupTest "sonarr" sonarrCfgFn;
|
||||||
sonarr_https = httpsTest "sonarr" sonarrCfgFn;
|
sonarr_https = httpsTest "sonarr" sonarrCfgFn;
|
||||||
sonarr_sso = ssoTest "sonarr" sonarrCfgFn;
|
sonarr_sso = ssoTest "sonarr" sonarrCfgFn;
|
||||||
|
|
||||||
bazarr_basic = basicTest "bazarr" bazarrCfgFn;
|
bazarr_basic = basicTest "bazarr" bazarrCfgFn;
|
||||||
bazarr_backup = backupTest "bazarr" bazarrCfgFn;
|
bazarr_backup = backupTest "bazarr" bazarrCfgFn;
|
||||||
bazarr_https = httpsTest "bazarr" bazarrCfgFn;
|
bazarr_https = httpsTest "bazarr" bazarrCfgFn;
|
||||||
bazarr_sso = ssoTest "bazarr" bazarrCfgFn;
|
bazarr_sso = ssoTest "bazarr" bazarrCfgFn;
|
||||||
|
|
||||||
readarr_basic = basicTest "readarr" readarrCfgFn;
|
readarr_basic = basicTest "readarr" readarrCfgFn;
|
||||||
readarr_backup = backupTest "readarr" readarrCfgFn;
|
readarr_backup = backupTest "readarr" readarrCfgFn;
|
||||||
readarr_https = httpsTest "readarr" readarrCfgFn;
|
readarr_https = httpsTest "readarr" readarrCfgFn;
|
||||||
readarr_sso = ssoTest "readarr" readarrCfgFn;
|
readarr_sso = ssoTest "readarr" readarrCfgFn;
|
||||||
|
|
||||||
lidarr_basic = basicTest "lidarr" lidarrCfgFn;
|
lidarr_basic = basicTest "lidarr" lidarrCfgFn;
|
||||||
lidarr_backup = backupTest "lidarr" lidarrCfgFn;
|
lidarr_backup = backupTest "lidarr" lidarrCfgFn;
|
||||||
lidarr_https = httpsTest "lidarr" lidarrCfgFn;
|
lidarr_https = httpsTest "lidarr" lidarrCfgFn;
|
||||||
lidarr_sso = ssoTest "lidarr" lidarrCfgFn;
|
lidarr_sso = ssoTest "lidarr" lidarrCfgFn;
|
||||||
|
|
||||||
jackett_basic = basicTest "jackett" jackettCfgFn;
|
jackett_basic = basicTest "jackett" jackettCfgFn;
|
||||||
jackett_backup = backupTest "jackett" jackettCfgFn;
|
jackett_backup = backupTest "jackett" jackettCfgFn;
|
||||||
jackett_https = httpsTest "jackett" jackettCfgFn;
|
jackett_https = httpsTest "jackett" jackettCfgFn;
|
||||||
jackett_sso = ssoTest "jackett" jackettCfgFn;
|
jackett_sso = ssoTest "jackett" jackettCfgFn;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,89 +2,106 @@
|
||||||
let
|
let
|
||||||
commonTestScript = lib.shb.accessScript {
|
commonTestScript = lib.shb.accessScript {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.audiobookshelf.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.audiobookshelf.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"audiobookshelf.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"audiobookshelf.service"
|
||||||
waitForPorts = { node, ... }: [
|
"nginx.service"
|
||||||
node.config.shb.audiobookshelf.webPort
|
];
|
||||||
];
|
waitForPorts =
|
||||||
|
{ node, ... }:
|
||||||
|
[
|
||||||
|
node.config.shb.audiobookshelf.webPort
|
||||||
|
];
|
||||||
# TODO: Test login
|
# TODO: Test login
|
||||||
# extraScript = { ... }: ''
|
# extraScript = { ... }: ''
|
||||||
# '';
|
# '';
|
||||||
};
|
};
|
||||||
|
|
||||||
basic = { config, ... }: {
|
basic =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
lib.shb.baseModule
|
{
|
||||||
../../modules/services/audiobookshelf.nix
|
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'))"
|
|
||||||
# ]; }
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
https = { config, ... }: {
|
test = {
|
||||||
shb.audiobookshelf = {
|
subdomain = "a";
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
};
|
||||||
};
|
shb.audiobookshelf = {
|
||||||
};
|
|
||||||
|
|
||||||
sso = { config, ... }: {
|
|
||||||
shb.audiobookshelf = {
|
|
||||||
sso = {
|
|
||||||
enable = true;
|
enable = true;
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
inherit (config.test) subdomain domain;
|
||||||
sharedSecret.result = config.shb.hardcodedsecret.audiobookshelfSSOPassword.result;
|
|
||||||
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia.result;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.hardcodedsecret.audiobookshelfSSOPassword = {
|
clientLogin =
|
||||||
request = config.shb.audiobookshelf.sso.sharedSecret.request;
|
{ config, ... }:
|
||||||
settings.content = "ssoPassword";
|
{
|
||||||
|
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 = {
|
https =
|
||||||
request = config.shb.audiobookshelf.sso.sharedSecretForAuthelia.request;
|
{ config, ... }:
|
||||||
settings.content = "ssoPassword";
|
{
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -116,7 +133,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript;
|
testScript = commonTestScript;
|
||||||
};
|
};
|
||||||
|
|
@ -124,18 +141,20 @@ in
|
||||||
sso = lib.shb.runNixOSTest {
|
sso = lib.shb.runNixOSTest {
|
||||||
name = "audiobookshelf-sso";
|
name = "audiobookshelf-sso";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
https
|
||||||
sso
|
lib.shb.ldap
|
||||||
];
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
};
|
sso
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript;
|
testScript = commonTestScript;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,146 +2,169 @@
|
||||||
let
|
let
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.deluge.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.deluge.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"nginx.service"
|
{ ... }:
|
||||||
"deluged.service"
|
[
|
||||||
"delugeweb.service"
|
"nginx.service"
|
||||||
];
|
"deluged.service"
|
||||||
waitForPorts = { node, ... }: [
|
"delugeweb.service"
|
||||||
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()"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
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, ... }: {
|
prometheusTestScript =
|
||||||
shb.deluge = {
|
{ nodes, ... }:
|
||||||
prometheusScraperPassword.result = config.shb.hardcodedsecret."scraper".result;
|
''
|
||||||
};
|
server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.deluge.port})
|
||||||
shb.hardcodedsecret."scraper" = {
|
with subtest("prometheus"):
|
||||||
request = config.shb.deluge.prometheusScraperPassword.request;
|
response = server.succeed(
|
||||||
settings.content = "scraperpw";
|
"curl -sSf "
|
||||||
};
|
+ " http://localhost:${toString nodes.server.services.prometheus.exporters.deluge.port}/metrics"
|
||||||
};
|
)
|
||||||
|
print(response)
|
||||||
|
'';
|
||||||
|
|
||||||
https = { config, ...}: {
|
basic =
|
||||||
shb.deluge = {
|
{ config, ... }:
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
{
|
||||||
};
|
imports = [
|
||||||
};
|
lib.shb.baseModule
|
||||||
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
|
../../modules/services/deluge.nix
|
||||||
|
];
|
||||||
|
|
||||||
sso = { config, ... }: {
|
test = {
|
||||||
shb.deluge = {
|
subdomain = "d";
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
};
|
||||||
|
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -164,14 +187,16 @@ in
|
||||||
backup = lib.shb.runNixOSTest {
|
backup = lib.shb.runNixOSTest {
|
||||||
name = "deluge_backup";
|
name = "deluge_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(lib.shb.backup config.shb.deluge.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.deluge.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -187,27 +212,29 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = lib.shb.runNixOSTest {
|
sso = lib.shb.runNixOSTest {
|
||||||
name = "deluge_sso";
|
name = "deluge_sso";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
https
|
||||||
sso
|
lib.shb.ldap
|
||||||
];
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
};
|
sso
|
||||||
|
];
|
||||||
nodes.client = {};
|
};
|
||||||
|
|
||||||
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access.override {
|
testScript = commonTestScript.access.override {
|
||||||
redirectSSO = true;
|
redirectSSO = true;
|
||||||
};
|
};
|
||||||
|
|
@ -225,10 +252,8 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = inputs:
|
testScript = inputs: (commonTestScript.access inputs) + (prometheusTestScript inputs);
|
||||||
(commonTestScript.access inputs)
|
|
||||||
+ (prometheusTestScript inputs);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,150 +4,182 @@ let
|
||||||
|
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.forgejo.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.forgejo.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"forgejo.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"forgejo.service"
|
||||||
waitForUnixSocket = { node, ... }: [
|
"nginx.service"
|
||||||
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'))"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
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, ... }: {
|
basic =
|
||||||
shb.forgejo = {
|
{ config, ... }:
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
{
|
||||||
};
|
imports = [
|
||||||
};
|
lib.shb.baseModule
|
||||||
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
|
../../modules/services/forgejo.nix
|
||||||
|
];
|
||||||
|
|
||||||
ldap = { config, ... }: {
|
test = {
|
||||||
shb.forgejo = {
|
subdomain = "f";
|
||||||
ldap = {
|
};
|
||||||
|
|
||||||
|
shb.forgejo = {
|
||||||
enable = true;
|
enable = true;
|
||||||
host = "127.0.0.1";
|
inherit (config.test) subdomain domain;
|
||||||
port = config.shb.lldap.ldapPort;
|
|
||||||
dcdomain = config.shb.lldap.dcdomain;
|
|
||||||
adminPassword.result = config.shb.hardcodedsecret.forgejoLdapUserPassword.result;
|
|
||||||
waitForSystemdServices = [ "lldap.service" ];
|
|
||||||
|
|
||||||
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 = {
|
clientLogin =
|
||||||
request = config.shb.forgejo.ldap.adminPassword.request;
|
{ config, ... }:
|
||||||
settings.content = "ldapUserPassword";
|
{
|
||||||
};
|
imports = [
|
||||||
};
|
lib.shb.baseModule
|
||||||
|
lib.shb.clientLoginModule
|
||||||
|
];
|
||||||
|
test = {
|
||||||
|
subdomain = "f";
|
||||||
|
};
|
||||||
|
|
||||||
sso = { config, ... }: {
|
test.login = {
|
||||||
shb.forgejo = {
|
startUrl = "http://${config.test.fqdn}/user/login";
|
||||||
sso = {
|
usernameFieldLabelRegex = "Username or email address";
|
||||||
enable = true;
|
passwordFieldLabelRegex = "Password";
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
loginButtonNameRegex = "[sS]ign [iI]n";
|
||||||
sharedSecret.result = config.shb.hardcodedsecret.forgejoSSOPassword.result;
|
testLoginWith = [
|
||||||
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.result;
|
{
|
||||||
|
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 = {
|
https =
|
||||||
request = config.shb.forgejo.sso.sharedSecret.request;
|
{ config, ... }:
|
||||||
settings.content = "ssoPassword";
|
{
|
||||||
|
shb.forgejo = {
|
||||||
|
ssl = config.shb.certs.certs.selfsigned.n;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.hardcodedsecret.forgejoSSOPasswordAuthelia = {
|
ldap =
|
||||||
request = config.shb.forgejo.sso.sharedSecretForAuthelia.request;
|
{ config, ... }:
|
||||||
settings.content = "ssoPassword";
|
{
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -170,14 +202,16 @@ in
|
||||||
backup = lib.shb.runNixOSTest {
|
backup = lib.shb.runNixOSTest {
|
||||||
name = "forgejo_backup";
|
name = "forgejo_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(lib.shb.backup config.shb.forgejo.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.forgejo.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -193,7 +227,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
@ -208,71 +242,100 @@ in
|
||||||
ldap
|
ldap
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {
|
nodes.client = {
|
||||||
imports = [
|
imports = [
|
||||||
({ config, ... }: {
|
(
|
||||||
imports = [
|
{ config, ... }:
|
||||||
lib.shb.baseModule
|
{
|
||||||
lib.shb.clientLoginModule
|
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()"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
})
|
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;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = lib.shb.runNixOSTest {
|
sso = lib.shb.runNixOSTest {
|
||||||
name = "forgejo_sso";
|
name = "forgejo_sso";
|
||||||
|
|
||||||
nodes.server = { config, pkgs, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
https
|
||||||
sso
|
lib.shb.ldap
|
||||||
];
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
};
|
sso
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,65 +2,83 @@
|
||||||
let
|
let
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.grocy.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.grocy.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"phpfpm-grocy.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"phpfpm-grocy.service"
|
||||||
waitForUnixSocket = { node, ... }: [
|
"nginx.service"
|
||||||
node.config.services.phpfpm.pools.grocy.socket
|
];
|
||||||
];
|
waitForUnixSocket =
|
||||||
};
|
{ node, ... }:
|
||||||
|
[
|
||||||
basic = { config, ... }: {
|
node.config.services.phpfpm.pools.grocy.socket
|
||||||
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, ...}: {
|
basic =
|
||||||
shb.grocy = {
|
{ config, ... }:
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
{
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -91,7 +109,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,59 +2,71 @@
|
||||||
let
|
let
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.hledger.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.hledger.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"hledger-web.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"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')"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
https = { config, ... }: {
|
basic =
|
||||||
shb.hledger = {
|
{ config, ... }:
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
{
|
||||||
};
|
imports = [
|
||||||
};
|
lib.shb.baseModule
|
||||||
|
../../modules/services/hledger.nix
|
||||||
|
];
|
||||||
|
|
||||||
sso = { config, ... }: {
|
test = {
|
||||||
shb.hledger = {
|
subdomain = "h";
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
};
|
||||||
|
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -78,14 +90,16 @@ in
|
||||||
backup = lib.shb.runNixOSTest {
|
backup = lib.shb.runNixOSTest {
|
||||||
name = "hledger_backup";
|
name = "hledger_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(lib.shb.backup config.shb.hledger.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.hledger.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -101,7 +115,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
@ -109,18 +123,20 @@ in
|
||||||
sso = lib.shb.runNixOSTest {
|
sso = lib.shb.runNixOSTest {
|
||||||
name = "hledger_sso";
|
name = "hledger_sso";
|
||||||
|
|
||||||
nodes.server = { config, pkgs, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
https
|
||||||
sso
|
lib.shb.ldap
|
||||||
];
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
};
|
sso
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access.override {
|
testScript = commonTestScript.access.override {
|
||||||
redirectSSO = true;
|
redirectSSO = true;
|
||||||
|
|
|
||||||
|
|
@ -2,89 +2,103 @@
|
||||||
let
|
let
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.home-assistant.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.home-assistant.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"home-assistant.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"home-assistant.service"
|
||||||
waitForPorts = { node, ... }: [
|
"nginx.service"
|
||||||
8123
|
];
|
||||||
];
|
waitForPorts =
|
||||||
};
|
{ node, ... }:
|
||||||
|
[
|
||||||
basic = { config, ... }: {
|
8123
|
||||||
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)"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
https = { config, ...}: {
|
basic =
|
||||||
shb.home-assistant = {
|
{ config, ... }:
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
{
|
||||||
};
|
imports = [
|
||||||
};
|
lib.shb.baseModule
|
||||||
|
../../modules/services/home-assistant.nix
|
||||||
|
];
|
||||||
|
|
||||||
ldap = { config, ... }: {
|
test = {
|
||||||
shb.home-assistant = {
|
subdomain = "ha";
|
||||||
ldap = {
|
};
|
||||||
|
|
||||||
|
shb.home-assistant = {
|
||||||
enable = true;
|
enable = true;
|
||||||
host = "127.0.0.1";
|
inherit (config.test) subdomain domain;
|
||||||
port = config.shb.lldap.webUIListenPort;
|
|
||||||
userGroup = "homeassistant_user";
|
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
|
# Not yet supported
|
||||||
#
|
#
|
||||||
|
|
@ -95,58 +109,60 @@ let
|
||||||
# };
|
# };
|
||||||
# };
|
# };
|
||||||
|
|
||||||
voice = { config, ... }: {
|
voice =
|
||||||
# For now, verifying the packages can build is good enough.
|
{ config, ... }:
|
||||||
environment.systemPackages = [
|
{
|
||||||
config.services.wyoming.piper.package
|
# For now, verifying the packages can build is good enough.
|
||||||
config.services.wyoming.openwakeword.package
|
environment.systemPackages = [
|
||||||
config.services.wyoming.faster-whisper.package
|
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
|
# TODO: enable this back. The issue id the services cannot talk to the internet
|
||||||
# to download the models so they fail to start..
|
# to download the models so they fail to start..
|
||||||
# shb.home-assistant.voice.text-to-speech = {
|
# shb.home-assistant.voice.text-to-speech = {
|
||||||
# "fr" = {
|
# "fr" = {
|
||||||
# enable = true;
|
# enable = true;
|
||||||
# voice = "fr-siwis-medium";
|
# voice = "fr-siwis-medium";
|
||||||
# uri = "tcp://0.0.0.0:10200";
|
# uri = "tcp://0.0.0.0:10200";
|
||||||
# speaker = 0;
|
# speaker = 0;
|
||||||
# };
|
# };
|
||||||
# "en" = {
|
# "en" = {
|
||||||
# enable = true;
|
# enable = true;
|
||||||
# voice = "en_GB-alba-medium";
|
# voice = "en_GB-alba-medium";
|
||||||
# uri = "tcp://0.0.0.0:10201";
|
# uri = "tcp://0.0.0.0:10201";
|
||||||
# speaker = 0;
|
# speaker = 0;
|
||||||
# };
|
# };
|
||||||
# };
|
# };
|
||||||
# shb.home-assistant.voice.speech-to-text = {
|
# shb.home-assistant.voice.speech-to-text = {
|
||||||
# "tiny-fr" = {
|
# "tiny-fr" = {
|
||||||
# enable = true;
|
# enable = true;
|
||||||
# model = "base-int8";
|
# model = "base-int8";
|
||||||
# language = "fr";
|
# language = "fr";
|
||||||
# uri = "tcp://0.0.0.0:10300";
|
# uri = "tcp://0.0.0.0:10300";
|
||||||
# device = "cpu";
|
# device = "cpu";
|
||||||
# };
|
# };
|
||||||
# "tiny-en" = {
|
# "tiny-en" = {
|
||||||
# enable = true;
|
# enable = true;
|
||||||
# model = "base-int8";
|
# model = "base-int8";
|
||||||
# language = "en";
|
# language = "en";
|
||||||
# uri = "tcp://0.0.0.0:10301";
|
# uri = "tcp://0.0.0.0:10301";
|
||||||
# device = "cpu";
|
# device = "cpu";
|
||||||
# };
|
# };
|
||||||
# };
|
# };
|
||||||
# shb.home-assistant.voice.wakeword = {
|
# shb.home-assistant.voice.wakeword = {
|
||||||
# enable = true;
|
# enable = true;
|
||||||
# uri = "tcp://127.0.0.1:10400";
|
# uri = "tcp://127.0.0.1:10400";
|
||||||
# preloadModels = [
|
# preloadModels = [
|
||||||
# "alexa"
|
# "alexa"
|
||||||
# "hey_jarvis"
|
# "hey_jarvis"
|
||||||
# "hey_mycroft"
|
# "hey_mycroft"
|
||||||
# "hey_rhasspy"
|
# "hey_rhasspy"
|
||||||
# "ok_nabu"
|
# "ok_nabu"
|
||||||
# ];
|
# ];
|
||||||
# };
|
# };
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -169,14 +185,16 @@ in
|
||||||
backup = lib.shb.runNixOSTest {
|
backup = lib.shb.runNixOSTest {
|
||||||
name = "homeassistant_backup";
|
name = "homeassistant_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(lib.shb.backup config.shb.home-assistant.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.home-assistant.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -192,24 +210,24 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
ldap = lib.shb.runNixOSTest {
|
ldap = lib.shb.runNixOSTest {
|
||||||
name = "homeassistant_ldap";
|
name = "homeassistant_ldap";
|
||||||
|
|
||||||
nodes.server = {
|
nodes.server = {
|
||||||
imports = [
|
imports = [
|
||||||
basic
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.ldap
|
||||||
ldap
|
ldap
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -218,7 +236,7 @@ in
|
||||||
# sso = lib.shb.runNixOSTest {
|
# sso = lib.shb.runNixOSTest {
|
||||||
# name = "vaultwarden_sso";
|
# name = "vaultwarden_sso";
|
||||||
#
|
#
|
||||||
# nodes.server = lib.mkMerge [
|
# nodes.server = lib.mkMerge [
|
||||||
# basic
|
# basic
|
||||||
# (lib.shb.certs domain)
|
# (lib.shb.certs domain)
|
||||||
# https
|
# https
|
||||||
|
|
@ -235,16 +253,16 @@ in
|
||||||
|
|
||||||
voice = lib.shb.runNixOSTest {
|
voice = lib.shb.runNixOSTest {
|
||||||
name = "homeassistant_voice";
|
name = "homeassistant_voice";
|
||||||
|
|
||||||
nodes.server = {
|
nodes.server = {
|
||||||
imports = [
|
imports = [
|
||||||
basic
|
basic
|
||||||
voice
|
voice
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,117 +5,138 @@ let
|
||||||
|
|
||||||
commonTestScript = lib.shb.accessScript {
|
commonTestScript = lib.shb.accessScript {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.immich.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.immich.ssl);
|
||||||
waitForServices = { ... }: [ "immich-server.service" "postgresql.service" "nginx.service" ];
|
waitForServices =
|
||||||
waitForPorts = { ... }: [ 2283 80 ];
|
{ ... }:
|
||||||
|
[
|
||||||
|
"immich-server.service"
|
||||||
|
"postgresql.service"
|
||||||
|
"nginx.service"
|
||||||
|
];
|
||||||
|
waitForPorts =
|
||||||
|
{ ... }:
|
||||||
|
[
|
||||||
|
2283
|
||||||
|
80
|
||||||
|
];
|
||||||
waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}" ];
|
waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
base = { config, ... }: {
|
base =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
lib.shb.baseModule
|
{
|
||||||
../../modules/services/immich.nix
|
imports = [
|
||||||
];
|
lib.shb.baseModule
|
||||||
|
../../modules/services/immich.nix
|
||||||
|
];
|
||||||
|
|
||||||
virtualisation.memorySize = 4096;
|
virtualisation.memorySize = 4096;
|
||||||
virtualisation.cores = 2;
|
virtualisation.cores = 2;
|
||||||
|
|
||||||
test = {
|
test = {
|
||||||
inherit subdomain domain;
|
inherit subdomain domain;
|
||||||
|
};
|
||||||
|
|
||||||
|
shb.immich = {
|
||||||
|
enable = true;
|
||||||
|
inherit subdomain domain;
|
||||||
|
|
||||||
|
debug = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Required for tests
|
||||||
|
environment.systemPackages = [ pkgs.curl ];
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.immich = {
|
basic =
|
||||||
enable = true;
|
{ config, ... }:
|
||||||
inherit subdomain domain;
|
{
|
||||||
|
imports = [ base ];
|
||||||
|
|
||||||
debug = true;
|
test.hasSSL = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Required for tests
|
https =
|
||||||
environment.systemPackages = [ pkgs.curl ];
|
{ config, ... }:
|
||||||
};
|
{
|
||||||
|
imports = [
|
||||||
|
base
|
||||||
|
lib.shb.certs
|
||||||
|
];
|
||||||
|
|
||||||
basic = { config, ... }: {
|
test.hasSSL = true;
|
||||||
imports = [ base ];
|
shb.immich.ssl = config.shb.certs.certs.selfsigned.n;
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.hardcodedsecret.immichSSOSecret = {
|
backup =
|
||||||
request = config.shb.immich.sso.sharedSecret.request;
|
{ config, ... }:
|
||||||
settings.content = "immichSSOSecret";
|
{
|
||||||
|
imports = [
|
||||||
|
https
|
||||||
|
(lib.shb.backup config.shb.immich.backup)
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.hardcodedsecret.immichSSOSecretAuthelia = {
|
sso =
|
||||||
request = config.shb.immich.sso.sharedSecretForAuthelia.request;
|
{ config, ... }:
|
||||||
settings.content = "immichSSOSecret";
|
{
|
||||||
};
|
imports = [
|
||||||
|
https
|
||||||
|
lib.shb.ldap
|
||||||
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
|
];
|
||||||
|
|
||||||
# Configure LDAP groups for group-based access control
|
shb.immich.sso = {
|
||||||
shb.lldap.ensureGroups.immich_user = {};
|
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 = {
|
shb.hardcodedsecret.immichSSOSecret = {
|
||||||
email = "immich_user@example.com";
|
request = config.shb.immich.sso.sharedSecret.request;
|
||||||
groups = [ "immich_user" ];
|
settings.content = "immichSSOSecret";
|
||||||
password.result = config.shb.hardcodedsecret.ldapImmichUserPassword.result;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
shb.lldap.ensureUsers.regular_test_user = {
|
shb.hardcodedsecret.immichSSOSecretAuthelia = {
|
||||||
email = "regular_user@example.com";
|
request = config.shb.immich.sso.sharedSecretForAuthelia.request;
|
||||||
groups = [ ];
|
settings.content = "immichSSOSecret";
|
||||||
password.result = config.shb.hardcodedsecret.ldapRegularUserPassword.result;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
shb.hardcodedsecret.ldapImmichUserPassword = {
|
# Configure LDAP groups for group-based access control
|
||||||
request = config.shb.lldap.ensureUsers.immich_test_user.password.request;
|
shb.lldap.ensureGroups.immich_user = { };
|
||||||
settings.content = "immich_user_password";
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.hardcodedsecret.ldapRegularUserPassword = {
|
shb.lldap.ensureUsers.immich_test_user = {
|
||||||
request = config.shb.lldap.ensureUsers.regular_test_user.password.request;
|
email = "immich_user@example.com";
|
||||||
settings.content = "regular_user_password";
|
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
|
in
|
||||||
{
|
{
|
||||||
basic = pkgs.nixosTest {
|
basic = pkgs.nixosTest {
|
||||||
name = "immich-basic";
|
name = "immich-basic";
|
||||||
|
|
||||||
nodes.server = basic;
|
nodes.server = basic;
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript;
|
testScript = commonTestScript;
|
||||||
};
|
};
|
||||||
|
|
@ -124,7 +145,7 @@ in
|
||||||
name = "immich-https";
|
name = "immich-https";
|
||||||
|
|
||||||
nodes.server = https;
|
nodes.server = https;
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript;
|
testScript = commonTestScript;
|
||||||
};
|
};
|
||||||
|
|
@ -133,13 +154,21 @@ in
|
||||||
name = "immich-backup";
|
name = "immich-backup";
|
||||||
|
|
||||||
nodes.server = backup;
|
nodes.server = backup;
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = (lib.shb.mkScripts {
|
testScript =
|
||||||
hasSSL = args: !(isNull args.node.config.shb.immich.ssl);
|
(lib.shb.mkScripts {
|
||||||
waitForServices = args: [ "immich-server.service" "postgresql.service" "nginx.service" ];
|
hasSSL = args: !(isNull args.node.config.shb.immich.ssl);
|
||||||
waitForPorts = args: [ 2283 80 ];
|
waitForServices = args: [
|
||||||
waitForUrls = args: [ "${args.proto_fqdn}" ];
|
"immich-server.service"
|
||||||
}).backup;
|
"postgresql.service"
|
||||||
|
"nginx.service"
|
||||||
|
];
|
||||||
|
waitForPorts = args: [
|
||||||
|
2283
|
||||||
|
80
|
||||||
|
];
|
||||||
|
waitForUrls = args: [ "${args.proto_fqdn}" ];
|
||||||
|
}).backup;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,164 +4,185 @@ let
|
||||||
|
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"jellyfin.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"jellyfin.service"
|
||||||
waitForPorts = { node, ... }: [
|
"nginx.service"
|
||||||
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)"
|
|
||||||
# ]; }
|
|
||||||
];
|
];
|
||||||
};
|
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, ... }: {
|
basic =
|
||||||
shb.jellyfin = {
|
{ config, ... }:
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
{
|
||||||
};
|
imports = [
|
||||||
test = {
|
lib.shb.baseModule
|
||||||
hasSSL = true;
|
../../modules/services/jellyfin.nix
|
||||||
};
|
];
|
||||||
};
|
test = {
|
||||||
|
subdomain = "j";
|
||||||
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 = {
|
shb.jellyfin = {
|
||||||
request = config.shb.jellyfin.ldap.adminPassword.request;
|
|
||||||
settings.content = "ldapUserPassword";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sso = { config, ... }: {
|
|
||||||
shb.jellyfin = {
|
|
||||||
sso = {
|
|
||||||
enable = true;
|
enable = true;
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
inherit (config.test) subdomain domain;
|
||||||
sharedSecret.result = config.shb.hardcodedsecret.jellyfinSSOPassword.result;
|
inherit port;
|
||||||
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result;
|
admin = {
|
||||||
|
username = "jellyfin";
|
||||||
|
password.result = config.shb.hardcodedsecret.jellyfinAdminPassword.result;
|
||||||
|
};
|
||||||
|
debug = true;
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
shb.hardcodedsecret.jellyfinSSOPassword = {
|
shb.hardcodedsecret.jellyfinAdminPassword = {
|
||||||
request = config.shb.jellyfin.sso.sharedSecret.request;
|
request = config.shb.jellyfin.admin.password.request;
|
||||||
settings.content = "ssoPassword";
|
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 = [
|
environment.systemPackages = [
|
||||||
pkgs.sqlite
|
pkgs.sqlite
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
inherit nodes;
|
clientLogin =
|
||||||
inherit testScript;
|
{ 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
|
in
|
||||||
{
|
{
|
||||||
basic = jellyfinTest "basic" {
|
basic = jellyfinTest "basic" {
|
||||||
|
|
@ -174,20 +195,22 @@ in
|
||||||
|
|
||||||
# Client login does not work without SSL.
|
# Client login does not work without SSL.
|
||||||
# At least, I couldn't make it work.
|
# At least, I couldn't make it work.
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
backup = jellyfinTest "backup" {
|
backup = jellyfinTest "backup" {
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(lib.shb.backup config.shb.jellyfin.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.jellyfin.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -201,12 +224,14 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = { config, lib, ... }: {
|
nodes.client =
|
||||||
imports = [
|
{ config, lib, ... }:
|
||||||
lib.shb.baseModule
|
{
|
||||||
clientLogin
|
imports = [
|
||||||
];
|
lib.shb.baseModule
|
||||||
};
|
clientLogin
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
@ -219,25 +244,27 @@ in
|
||||||
ldap
|
ldap
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = jellyfinTest "sso" {
|
sso = jellyfinTest "sso" {
|
||||||
nodes.server = { config, pkgs, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
https
|
||||||
sso
|
lib.shb.ldap
|
||||||
];
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
};
|
sso
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,144 +3,182 @@ let
|
||||||
nextauthSecret = "nextauthSecret";
|
nextauthSecret = "nextauthSecret";
|
||||||
oidcSecret = "oidcSecret";
|
oidcSecret = "oidcSecret";
|
||||||
|
|
||||||
testLib = pkgs.callPackage ../common.nix {};
|
testLib = pkgs.callPackage ../common.nix { };
|
||||||
|
|
||||||
commonTestScript = testLib.mkScripts {
|
commonTestScript = testLib.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.karakeep.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.karakeep.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"karakeep-init.service"
|
{ ... }:
|
||||||
"karakeep-browser.service"
|
[
|
||||||
"karakeep-web.service"
|
"karakeep-init.service"
|
||||||
"karakeep-workers.service"
|
"karakeep-browser.service"
|
||||||
"nginx.service"
|
"karakeep-web.service"
|
||||||
];
|
"karakeep-workers.service"
|
||||||
waitForPorts = { node, ... }: [
|
"nginx.service"
|
||||||
node.config.shb.karakeep.port
|
];
|
||||||
];
|
waitForPorts =
|
||||||
};
|
{ node, ... }:
|
||||||
|
[
|
||||||
basic = { config, ... }: {
|
node.config.shb.karakeep.port
|
||||||
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)"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = { config, ... }: {
|
basic =
|
||||||
shb.karakeep = {
|
{ config, ... }:
|
||||||
sso = {
|
{
|
||||||
enable = true;
|
imports = [
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
testLib.baseModule
|
||||||
clientID = "karakeep";
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
|
../../modules/blocks/lldap.nix
|
||||||
|
../../modules/services/karakeep.nix
|
||||||
|
];
|
||||||
|
|
||||||
sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result;
|
test = {
|
||||||
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result;
|
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 = {
|
https =
|
||||||
request = config.shb.karakeep.sso.sharedSecret.request;
|
{ config, ... }:
|
||||||
settings.content = oidcSecret;
|
{
|
||||||
|
shb.karakeep = {
|
||||||
|
ssl = config.shb.certs.certs.selfsigned.n;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
shb.hardcodedsecret.oidcAutheliaSecret = {
|
|
||||||
request = config.shb.karakeep.sso.sharedSecretForAuthelia.request;
|
ldap =
|
||||||
settings.content = oidcSecret;
|
{ 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
|
in
|
||||||
{
|
{
|
||||||
basic = pkgs.testers.runNixOSTest {
|
basic = pkgs.testers.runNixOSTest {
|
||||||
name = "karakeep_basic";
|
name = "karakeep_basic";
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
nodes.server = {
|
nodes.server = {
|
||||||
imports = [
|
imports = [
|
||||||
basic
|
basic
|
||||||
|
|
@ -153,14 +191,16 @@ in
|
||||||
backup = pkgs.testers.runNixOSTest {
|
backup = pkgs.testers.runNixOSTest {
|
||||||
name = "karakeep_backup";
|
name = "karakeep_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(testLib.backup config.shb.karakeep.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(testLib.backup config.shb.karakeep.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -168,7 +208,7 @@ in
|
||||||
https = pkgs.testers.runNixOSTest {
|
https = pkgs.testers.runNixOSTest {
|
||||||
name = "karakeep_https";
|
name = "karakeep_https";
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
nodes.server = {
|
nodes.server = {
|
||||||
imports = [
|
imports = [
|
||||||
basic
|
basic
|
||||||
|
|
@ -191,19 +231,21 @@ in
|
||||||
|
|
||||||
virtualisation.memorySize = 4096;
|
virtualisation.memorySize = 4096;
|
||||||
};
|
};
|
||||||
nodes.server = { config, pkgs, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
basic
|
{
|
||||||
testLib.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
testLib.ldap
|
testLib.certs
|
||||||
ldap
|
https
|
||||||
(testLib.sso config.shb.certs.certs.selfsigned.n)
|
testLib.ldap
|
||||||
sso
|
ldap
|
||||||
];
|
(testLib.sso config.shb.certs.certs.selfsigned.n)
|
||||||
|
sso
|
||||||
|
];
|
||||||
|
|
||||||
virtualisation.memorySize = 4096;
|
virtualisation.memorySize = 4096;
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,43 +4,51 @@ let
|
||||||
|
|
||||||
commonTestScript = lib.shb.accessScript {
|
commonTestScript = lib.shb.accessScript {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.monitoring.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.monitoring.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"grafana.service"
|
{ ... }:
|
||||||
];
|
[
|
||||||
waitForPorts = { node, ... }: [
|
"grafana.service"
|
||||||
node.config.shb.monitoring.grafanaPort
|
];
|
||||||
];
|
waitForPorts =
|
||||||
|
{ node, ... }:
|
||||||
|
[
|
||||||
|
node.config.shb.monitoring.grafanaPort
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
basic = { config, ... }: {
|
basic =
|
||||||
test = {
|
{ config, ... }:
|
||||||
subdomain = "g";
|
{
|
||||||
|
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 = {
|
https =
|
||||||
enable = true;
|
{ config, ... }:
|
||||||
inherit (config.test) subdomain domain;
|
{
|
||||||
|
shb.monitoring = {
|
||||||
grafanaPort = 3000;
|
ssl = config.shb.certs.certs.selfsigned.n;
|
||||||
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";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
https = { config, ...}: {
|
|
||||||
shb.monitoring = {
|
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -54,7 +62,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript;
|
testScript = commonTestScript;
|
||||||
};
|
};
|
||||||
|
|
@ -72,7 +80,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript;
|
testScript = commonTestScript;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,267 +6,339 @@ let
|
||||||
|
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.nextcloud.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.nextcloud.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"phpfpm-nextcloud.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"phpfpm-nextcloud.service"
|
||||||
waitForUnixSocket = { node, ... }: [
|
"nginx.service"
|
||||||
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()"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
waitForUnixSocket =
|
||||||
};
|
{ node, ... }:
|
||||||
|
[
|
||||||
clientLdapLogin = { config, ... }: {
|
node.config.services.phpfpm.pools.nextcloud.socket
|
||||||
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()"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
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, ... }: {
|
basic =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
lib.shb.baseModule
|
{
|
||||||
lib.shb.clientLoginModule
|
imports = [
|
||||||
];
|
lib.shb.baseModule
|
||||||
virtualisation.memorySize = 4096;
|
../../modules/services/nextcloud-server.nix
|
||||||
|
|
||||||
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, ...}: {
|
test = {
|
||||||
shb.nextcloud = {
|
subdomain = "n";
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
};
|
||||||
|
|
||||||
externalFqdn = lib.mkForce null;
|
shb.nextcloud = {
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
ldap = { config, ... }: {
|
|
||||||
shb.nextcloud = {
|
|
||||||
apps.ldap = {
|
|
||||||
enable = true;
|
enable = true;
|
||||||
host = "127.0.0.1";
|
inherit (config.test) subdomain domain;
|
||||||
port = config.shb.lldap.ldapPort;
|
|
||||||
dcdomain = config.shb.lldap.dcdomain;
|
dataDir = "/var/lib/nextcloud";
|
||||||
adminName = "admin";
|
tracing = null;
|
||||||
adminPassword.result = config.shb.hardcodedsecret.nextcloudLdapUserPassword.result;
|
defaultPhoneRegion = "US";
|
||||||
userGroup = "user_group";
|
|
||||||
|
# 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 = {
|
shb.nextcloud = {
|
||||||
apps.ldap = {
|
apps.ldap = {
|
||||||
|
|
@ -297,17 +369,19 @@ let
|
||||||
request = config.shb.nextcloud.apps.sso.secretForAuthelia.request;
|
request = config.shb.nextcloud.apps.sso.secretForAuthelia.request;
|
||||||
settings.content = oidcSecret;
|
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 = {
|
externalstorage = {
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
|
|
@ -323,43 +397,50 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
memories = { config, ...}: {
|
memories =
|
||||||
systemd.tmpfiles.rules = [
|
{ config, ... }:
|
||||||
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
|
{
|
||||||
];
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
|
||||||
|
];
|
||||||
|
|
||||||
shb.nextcloud = {
|
shb.nextcloud = {
|
||||||
apps.memories.enable = true;
|
apps.memories.enable = true;
|
||||||
apps.memories.vaapi = true;
|
apps.memories.vaapi = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
recognize = { config, ...}: {
|
recognize =
|
||||||
systemd.tmpfiles.rules = [
|
{ config, ... }:
|
||||||
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
|
{
|
||||||
];
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '/srv/nextcloud' 0750 nextcloud nextcloud - -"
|
||||||
|
];
|
||||||
|
|
||||||
shb.nextcloud = {
|
shb.nextcloud = {
|
||||||
apps.recognize.enable = true;
|
apps.recognize.enable = true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
prometheus = { config, ... }: {
|
prometheus =
|
||||||
shb.nextcloud = {
|
{ config, ... }:
|
||||||
phpFpmPrometheusExporter.enable = true;
|
{
|
||||||
|
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_unix_socket("${nodes.server.services.phpfpm.pools.nextcloud.socket}")
|
||||||
server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.php-fpm.port})
|
server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.php-fpm.port})
|
||||||
with subtest("prometheus"):
|
with subtest("prometheus"):
|
||||||
response = server.succeed(
|
response = server.succeed(
|
||||||
"curl -sSf "
|
"curl -sSf "
|
||||||
+ " http://localhost:${toString nodes.server.services.prometheus.exporters.php-fpm.port}/metrics"
|
+ " http://localhost:${toString nodes.server.services.prometheus.exporters.php-fpm.port}/metrics"
|
||||||
)
|
)
|
||||||
print(response)
|
print(response)
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -389,44 +470,53 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access.override {
|
testScript = commonTestScript.access.override {
|
||||||
extraScript = { node, fqdn, proto_fqdn, ... }: ''
|
extraScript =
|
||||||
import time
|
{
|
||||||
|
node,
|
||||||
|
fqdn,
|
||||||
|
proto_fqdn,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
''
|
||||||
|
import time
|
||||||
|
|
||||||
def find_in_logs(unit, text):
|
def find_in_logs(unit, text):
|
||||||
return server.systemctl("status {}".format(unit))[1].find(text) != -1
|
return server.systemctl("status {}".format(unit))[1].find(text) != -1
|
||||||
|
|
||||||
with subtest("cron job succeeds"):
|
with subtest("cron job succeeds"):
|
||||||
# This call does not block until the service is done.
|
# This call does not block until the service is done.
|
||||||
server.succeed("systemctl start nextcloud-cron.service&")
|
server.succeed("systemctl start nextcloud-cron.service&")
|
||||||
|
|
||||||
# If the service failed, then we're not happy.
|
# If the service failed, then we're not happy.
|
||||||
status = "active"
|
status = "active"
|
||||||
while status == "active":
|
while status == "active":
|
||||||
status = server.get_unit_info("nextcloud-cron")["ActiveState"]
|
status = server.get_unit_info("nextcloud-cron")["ActiveState"]
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
if status != "inactive":
|
if status != "inactive":
|
||||||
raise Exception("Cron job did not finish correctly")
|
raise Exception("Cron job did not finish correctly")
|
||||||
|
|
||||||
if not find_in_logs("nextcloud-cron", "nextcloud-cron.service: Deactivated successfully."):
|
if not find_in_logs("nextcloud-cron", "nextcloud-cron.service: Deactivated successfully."):
|
||||||
raise Exception("Nextcloud cron job did not finish successfully.")
|
raise Exception("Nextcloud cron job did not finish successfully.")
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
backup = lib.shb.runNixOSTest {
|
backup = lib.shb.runNixOSTest {
|
||||||
name = "nextcloud_backup";
|
name = "nextcloud_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(lib.shb.backup config.shb.nextcloud.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.nextcloud.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -442,7 +532,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
# TODO: Test login
|
# TODO: Test login
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
|
|
@ -460,7 +550,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
@ -477,7 +567,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
@ -514,77 +604,89 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
ldap = lib.shb.runNixOSTest {
|
ldap = lib.shb.runNixOSTest {
|
||||||
name = "nextcloud_ldap";
|
name = "nextcloud_ldap";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
ldap
|
https
|
||||||
];
|
lib.shb.ldap
|
||||||
};
|
ldap
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {
|
nodes.client = {
|
||||||
imports = [
|
imports = [
|
||||||
clientLdapLogin
|
clientLdapLogin
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = lib.shb.runNixOSTest {
|
sso = lib.shb.runNixOSTest {
|
||||||
name = "nextcloud_sso";
|
name = "nextcloud_sso";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
https
|
||||||
sso
|
lib.shb.ldap
|
||||||
({ config, ... }: {
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
networking.hosts = {
|
sso
|
||||||
"127.0.0.1" = [ config.test.fqdn ];
|
(
|
||||||
};
|
{ config, ... }:
|
||||||
})
|
{
|
||||||
];
|
networking.hosts = {
|
||||||
};
|
"127.0.0.1" = [ config.test.fqdn ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {
|
nodes.client = {
|
||||||
imports = [
|
imports = [
|
||||||
clientSsoLogin
|
clientSsoLogin
|
||||||
({ config, ... }: {
|
(
|
||||||
networking.hosts = {
|
{ config, ... }:
|
||||||
"192.168.1.2" = [ config.test.fqdn ];
|
{
|
||||||
};
|
networking.hosts = {
|
||||||
})
|
"192.168.1.2" = [ config.test.fqdn ];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
||||||
prometheus = lib.shb.runNixOSTest {
|
prometheus = lib.shb.runNixOSTest {
|
||||||
name = "nextcloud_prometheus";
|
name = "nextcloud_prometheus";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
prometheus
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
prometheus
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = prometheusTestScript;
|
testScript = prometheusTestScript;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,131 +4,169 @@ let
|
||||||
|
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.open-webui.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.open-webui.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"open-webui.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"open-webui.service"
|
||||||
waitForPorts = { node, ... }: [
|
"nginx.service"
|
||||||
node.config.shb.open-webui.port
|
];
|
||||||
];
|
waitForPorts =
|
||||||
};
|
{ node, ... }:
|
||||||
|
[
|
||||||
basic = { config, ... }: {
|
node.config.shb.open-webui.port
|
||||||
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()"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = { config, ... }: {
|
basic =
|
||||||
shb.open-webui = {
|
{ config, ... }:
|
||||||
sso = {
|
{
|
||||||
enable = true;
|
imports = [
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
lib.shb.baseModule
|
||||||
clientID = "open-webui";
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
|
../../modules/services/open-webui.nix
|
||||||
|
];
|
||||||
|
|
||||||
sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result;
|
test = {
|
||||||
sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result;
|
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 = {
|
https =
|
||||||
request = config.shb.open-webui.sso.sharedSecret.request;
|
{ config, ... }:
|
||||||
settings.content = oidcSecret;
|
{
|
||||||
|
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;
|
ldap =
|
||||||
settings.content = oidcSecret;
|
{ 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
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
name = "open-webui_basic";
|
name = "open-webui_basic";
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
nodes.server = {
|
nodes.server = {
|
||||||
imports = [
|
imports = [
|
||||||
basic
|
basic
|
||||||
|
|
@ -141,14 +179,16 @@ in
|
||||||
backup = lib.shb.runNixOSTest {
|
backup = lib.shb.runNixOSTest {
|
||||||
name = "open-webui_backup";
|
name = "open-webui_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(lib.shb.backup config.shb.open-webui.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.open-webui.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -156,7 +196,7 @@ in
|
||||||
https = lib.shb.runNixOSTest {
|
https = lib.shb.runNixOSTest {
|
||||||
name = "open-webui_https";
|
name = "open-webui_https";
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
nodes.server = {
|
nodes.server = {
|
||||||
imports = [
|
imports = [
|
||||||
basic
|
basic
|
||||||
|
|
@ -176,17 +216,19 @@ in
|
||||||
clientLoginSso
|
clientLoginSso
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
nodes.server = { config, pkgs, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
ldap
|
https
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
lib.shb.ldap
|
||||||
sso
|
ldap
|
||||||
];
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
};
|
sso
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,134 +2,178 @@
|
||||||
let
|
let
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.pinchflat.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.pinchflat.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"pinchflat.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"pinchflat.service"
|
||||||
waitForPorts = { node, ... }: [
|
"nginx.service"
|
||||||
node.config.shb.pinchflat.port
|
];
|
||||||
];
|
waitForPorts =
|
||||||
};
|
{ node, ... }:
|
||||||
|
[
|
||||||
basic = { config, ... }: {
|
node.config.shb.pinchflat.port
|
||||||
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()"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
https = { config, ... }: {
|
basic =
|
||||||
shb.pinchflat = {
|
{ config, ... }:
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
{
|
||||||
};
|
imports = [
|
||||||
};
|
lib.shb.baseModule
|
||||||
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
|
../../modules/services/pinchflat.nix
|
||||||
|
];
|
||||||
|
|
||||||
ldap = { config, ... }: {
|
test = {
|
||||||
shb.pinchflat = {
|
subdomain = "p";
|
||||||
ldap = {
|
};
|
||||||
|
|
||||||
|
shb.pinchflat = {
|
||||||
enable = true;
|
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, ... }: {
|
clientLogin =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
lib.shb.baseModule
|
{
|
||||||
lib.shb.clientLoginModule
|
imports = [
|
||||||
];
|
lib.shb.baseModule
|
||||||
test = {
|
lib.shb.clientLoginModule
|
||||||
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'))"
|
|
||||||
]; }
|
|
||||||
];
|
];
|
||||||
};
|
test = {
|
||||||
};
|
subdomain = "p";
|
||||||
|
};
|
||||||
|
|
||||||
sso = { config, ... }: {
|
test.login = {
|
||||||
shb.pinchflat = {
|
startUrl = "http://${config.test.fqdn}";
|
||||||
sso = {
|
# There is no login without SSO integration.
|
||||||
enable = true;
|
testLoginWith = [
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
{
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -152,14 +196,16 @@ in
|
||||||
backup = lib.shb.runNixOSTest {
|
backup = lib.shb.runNixOSTest {
|
||||||
name = "pinchflat_backup";
|
name = "pinchflat_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
basic
|
{
|
||||||
(lib.shb.backup config.shb.pinchflat.backup)
|
imports = [
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.pinchflat.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
@ -191,17 +237,19 @@ in
|
||||||
clientLoginSso
|
clientLoginSso
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
nodes.server = { config, pkgs, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, pkgs, ... }:
|
||||||
basic
|
{
|
||||||
lib.shb.certs
|
imports = [
|
||||||
https
|
basic
|
||||||
lib.shb.ldap
|
lib.shb.certs
|
||||||
ldap
|
https
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
lib.shb.ldap
|
||||||
sso
|
ldap
|
||||||
];
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
};
|
sso
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
testScript = commonTestScript.access.override {
|
testScript = commonTestScript.access.override {
|
||||||
redirectSSO = true;
|
redirectSSO = true;
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,18 @@
|
||||||
let
|
let
|
||||||
commonTestScript = lib.shb.mkScripts {
|
commonTestScript = lib.shb.mkScripts {
|
||||||
hasSSL = { node, ... }: !(isNull node.config.shb.vaultwarden.ssl);
|
hasSSL = { node, ... }: !(isNull node.config.shb.vaultwarden.ssl);
|
||||||
waitForServices = { ... }: [
|
waitForServices =
|
||||||
"vaultwarden.service"
|
{ ... }:
|
||||||
"nginx.service"
|
[
|
||||||
];
|
"vaultwarden.service"
|
||||||
waitForPorts = { node, ... }: [
|
"nginx.service"
|
||||||
8222
|
];
|
||||||
5432
|
waitForPorts =
|
||||||
];
|
{ node, ... }:
|
||||||
|
[
|
||||||
|
8222
|
||||||
|
5432
|
||||||
|
];
|
||||||
# to get the get token test to succeed we need:
|
# to get the get token test to succeed we need:
|
||||||
# 1. add group Vaultwarden_admin to LLDAP
|
# 1. add group Vaultwarden_admin to LLDAP
|
||||||
# 2. add an Authelia user with to that group
|
# 2. add an Authelia user with to that group
|
||||||
|
|
@ -17,58 +21,64 @@ let
|
||||||
# 4. go to the Vaultwarden /admin endpoint
|
# 4. go to the Vaultwarden /admin endpoint
|
||||||
# 5. create a Vaultwarden user
|
# 5. create a Vaultwarden user
|
||||||
# 6. now login with that new user to Vaultwarden
|
# 6. now login with that new user to Vaultwarden
|
||||||
extraScript = { node, proto_fqdn, ... }: ''
|
extraScript =
|
||||||
with subtest("prelogin"):
|
{ node, proto_fqdn, ... }:
|
||||||
response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """
|
''
|
||||||
{"email": "me@example.com"}
|
with subtest("prelogin"):
|
||||||
"""))
|
response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """
|
||||||
print(response)
|
{"email": "me@example.com"}
|
||||||
if 'kdf' not in response:
|
"""))
|
||||||
raise Exception("Unrecognized response: {}".format(response))
|
print(response)
|
||||||
|
if 'kdf' not in response:
|
||||||
|
raise Exception("Unrecognized response: {}".format(response))
|
||||||
|
|
||||||
with subtest("get token"):
|
with subtest("get token"):
|
||||||
response = curl(client, "", "${proto_fqdn}/identity/connect/token", data=unline_with("", """
|
response = curl(client, "", "${proto_fqdn}/identity/connect/token", data=unline_with("", """
|
||||||
scope=api%20offline_access
|
scope=api%20offline_access
|
||||||
&client_id=web
|
&client_id=web
|
||||||
&deviceType=10
|
&deviceType=10
|
||||||
&deviceIdentifier=a60323bf-4686-4b4d-96e0-3c241fa5581c
|
&deviceIdentifier=a60323bf-4686-4b4d-96e0-3c241fa5581c
|
||||||
&deviceName=firefox
|
&deviceName=firefox
|
||||||
&grant_type=password&username=me
|
&grant_type=password&username=me
|
||||||
&password=mypassword
|
&password=mypassword
|
||||||
"""))
|
"""))
|
||||||
print(response)
|
print(response)
|
||||||
if response["message"] != "Username or password is incorrect. Try again":
|
if response["message"] != "Username or password is incorrect. Try again":
|
||||||
raise Exception("Unrecognized response: {}".format(response))
|
raise Exception("Unrecognized response: {}".format(response))
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
basic = { config, ... }: {
|
basic =
|
||||||
test = {
|
{ config, ... }:
|
||||||
subdomain = "v";
|
{
|
||||||
|
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 = {
|
https =
|
||||||
enable = true;
|
{ config, ... }:
|
||||||
inherit (config.test) subdomain domain;
|
{
|
||||||
|
shb.vaultwarden = {
|
||||||
port = 8222;
|
ssl = config.shb.certs.certs.selfsigned.n;
|
||||||
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 ];
|
|
||||||
# };
|
|
||||||
};
|
|
||||||
|
|
||||||
https = { config, ... }: {
|
|
||||||
shb.vaultwarden = {
|
|
||||||
ssl = config.shb.certs.certs.selfsigned.n;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Not yet supported
|
# Not yet supported
|
||||||
# ldap = { config, ... }: {
|
# ldap = { config, ... }: {
|
||||||
|
|
@ -78,11 +88,13 @@ let
|
||||||
# # };
|
# # };
|
||||||
# };
|
# };
|
||||||
|
|
||||||
sso = { config, ... }: {
|
sso =
|
||||||
shb.vaultwarden = {
|
{ config, ... }:
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
{
|
||||||
|
shb.vaultwarden = {
|
||||||
|
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
basic = lib.shb.runNixOSTest {
|
basic = lib.shb.runNixOSTest {
|
||||||
|
|
@ -97,7 +109,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
@ -116,7 +128,7 @@ in
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access;
|
testScript = commonTestScript.access;
|
||||||
};
|
};
|
||||||
|
|
@ -126,7 +138,7 @@ in
|
||||||
# ldap = lib.shb.runNixOSTest {
|
# ldap = lib.shb.runNixOSTest {
|
||||||
# name = "vaultwarden_ldap";
|
# name = "vaultwarden_ldap";
|
||||||
#
|
#
|
||||||
# nodes.server = lib.mkMerge [
|
# nodes.server = lib.mkMerge [
|
||||||
# lib.shb.baseModule
|
# lib.shb.baseModule
|
||||||
# ../../modules/blocks/hardcodedsecret.nix
|
# ../../modules/blocks/hardcodedsecret.nix
|
||||||
# ../../modules/services/vaultwarden.nix
|
# ../../modules/services/vaultwarden.nix
|
||||||
|
|
@ -142,56 +154,64 @@ in
|
||||||
sso = lib.shb.runNixOSTest {
|
sso = lib.shb.runNixOSTest {
|
||||||
name = "vaultwarden_sso";
|
name = "vaultwarden_sso";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
lib.shb.baseModule
|
{
|
||||||
../../modules/blocks/hardcodedsecret.nix
|
imports = [
|
||||||
../../modules/services/vaultwarden.nix
|
lib.shb.baseModule
|
||||||
lib.shb.certs
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
basic
|
../../modules/services/vaultwarden.nix
|
||||||
https
|
lib.shb.certs
|
||||||
lib.shb.ldap
|
basic
|
||||||
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
https
|
||||||
sso
|
lib.shb.ldap
|
||||||
];
|
(lib.shb.sso config.shb.certs.certs.selfsigned.n)
|
||||||
};
|
sso
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.access.override {
|
testScript = commonTestScript.access.override {
|
||||||
waitForPorts = { node, ... }: [
|
waitForPorts =
|
||||||
8222
|
{ node, ... }:
|
||||||
5432
|
[
|
||||||
9091
|
8222
|
||||||
];
|
5432
|
||||||
extraScript = { node, proto_fqdn, ... }: ''
|
9091
|
||||||
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")
|
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:
|
if response['code'] != 200:
|
||||||
raise Exception(f"Code is {response['code']}")
|
raise Exception(f"Code is {response['code']}")
|
||||||
if response['auth_host'] != "auth.${node.config.test.domain}":
|
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']}")
|
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":
|
if response['auth_query'] != "rd=${proto_fqdn}/admin":
|
||||||
raise Exception(f"auth query should be rd=${proto_fqdn}/admin but is {response['auth_query']}")
|
raise Exception(f"auth query should be rd=${proto_fqdn}/admin but is {response['auth_query']}")
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
backup = lib.shb.runNixOSTest {
|
backup = lib.shb.runNixOSTest {
|
||||||
name = "vaultwarden_backup";
|
name = "vaultwarden_backup";
|
||||||
|
|
||||||
nodes.server = { config, ... }: {
|
nodes.server =
|
||||||
imports = [
|
{ config, ... }:
|
||||||
lib.shb.baseModule
|
{
|
||||||
../../modules/blocks/hardcodedsecret.nix
|
imports = [
|
||||||
../../modules/services/vaultwarden.nix
|
lib.shb.baseModule
|
||||||
basic
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
(lib.shb.backup config.shb.vaultwarden.backup)
|
../../modules/services/vaultwarden.nix
|
||||||
];
|
basic
|
||||||
};
|
(lib.shb.backup config.shb.vaultwarden.backup)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
nodes.client = {};
|
nodes.client = { };
|
||||||
|
|
||||||
testScript = commonTestScript.backup;
|
testScript = commonTestScript.backup;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue