diff --git a/demo/homeassistant/configuration.nix b/demo/homeassistant/configuration.nix index 9faed8f..fc43f60 100644 --- a/demo/homeassistant/configuration.nix +++ b/demo/homeassistant/configuration.nix @@ -5,17 +5,17 @@ let targetPort = 2222; in { - imports = - [ # Include the results of the hardware scan. - ./hardware-configuration.nix - ]; + imports = [ + # Include the results of the hardware scan. + ./hardware-configuration.nix + ]; boot.loader.grub.enable = true; boot.kernelModules = [ "kvm-intel" ]; system.stateVersion = "22.11"; # Options above are generate by running nixos-generate-config on the VM. - + # Needed otherwise deploy will say system won't be able to boot. boot.loader.grub.device = "/dev/vdb"; # Needed to avoid getting into not available disk space in /boot. @@ -26,7 +26,10 @@ in # Options above are needed to deploy in a VM. - nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; # We need to create the user we will deploy with. users.users.${targetUser} = { @@ -41,9 +44,11 @@ in # The user we're deploying with must be able to run sudo without password. security.sudo.extraRules = [ - { users = [ targetUser ]; + { + users = [ targetUser ]; commands = [ - { command = "ALL"; + { + command = "ALL"; options = [ "NOPASSWD" ]; } ]; diff --git a/demo/homeassistant/flake.nix b/demo/homeassistant/flake.nix index d171423..5e67887 100644 --- a/demo/homeassistant/flake.nix +++ b/demo/homeassistant/flake.nix @@ -6,148 +6,161 @@ sops-nix.url = "github:Mic92/sops-nix"; }; - outputs = inputs@{ self, selfhostblocks, sops-nix }: + outputs = + inputs@{ + self, + selfhostblocks, + sops-nix, + }: let system = "x86_64-linux"; nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs; inherit (selfhostblocks.lib.${system}) pkgs; - basic = { config, ... }: { - imports = [ - ./configuration.nix - selfhostblocks.nixosModules.authelia - selfhostblocks.nixosModules.home-assistant - selfhostblocks.nixosModules.sops - selfhostblocks.nixosModules.ssl - sops-nix.nixosModules.default - ]; + basic = + { config, ... }: + { + imports = [ + ./configuration.nix + selfhostblocks.nixosModules.authelia + selfhostblocks.nixosModules.home-assistant + selfhostblocks.nixosModules.sops + selfhostblocks.nixosModules.ssl + sops-nix.nixosModules.default + ]; - sops.defaultSopsFile = ./secrets.yaml; + sops.defaultSopsFile = ./secrets.yaml; - shb.home-assistant = { - enable = true; - domain = "example.com"; - subdomain = "ha"; - config = { - name = "SHB Home Assistant"; - country.source = config.shb.sops.secret."home-assistant/country".result.path; - latitude.source = config.shb.sops.secret."home-assistant/latitude".result.path; - longitude.source = config.shb.sops.secret."home-assistant/longitude".result.path; - time_zone.source = config.shb.sops.secret."home-assistant/time_zone".result.path; - unit_system = "metric"; + shb.home-assistant = { + enable = true; + domain = "example.com"; + subdomain = "ha"; + config = { + name = "SHB Home Assistant"; + country.source = config.shb.sops.secret."home-assistant/country".result.path; + latitude.source = config.shb.sops.secret."home-assistant/latitude".result.path; + longitude.source = config.shb.sops.secret."home-assistant/longitude".result.path; + time_zone.source = config.shb.sops.secret."home-assistant/time_zone".result.path; + unit_system = "metric"; + }; + }; + shb.sops.secret."home-assistant/country".request = { + mode = "0440"; + owner = "hass"; + group = "hass"; + restartUnits = [ "home-assistant.service" ]; + }; + shb.sops.secret."home-assistant/latitude".request = { + mode = "0440"; + owner = "hass"; + group = "hass"; + restartUnits = [ "home-assistant.service" ]; + }; + shb.sops.secret."home-assistant/longitude".request = { + mode = "0440"; + owner = "hass"; + group = "hass"; + restartUnits = [ "home-assistant.service" ]; + }; + shb.sops.secret."home-assistant/time_zone".request = { + mode = "0440"; + owner = "hass"; + group = "hass"; + restartUnits = [ "home-assistant.service" ]; + }; + + nixpkgs.config.permittedInsecurePackages = [ + "openssl-1.1.1w" + ]; + }; + + ldap = + { config, ... }: + { + shb.lldap = { + enable = true; + domain = "example.com"; + subdomain = "ldap"; + ldapPort = 3890; + webUIListenPort = 17170; + dcdomain = "dc=example,dc=com"; + ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result; + jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result; + }; + shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request; + shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request; + + shb.home-assistant.ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.lldap.webUIListenPort; + userGroup = "homeassistant_user"; }; }; - shb.sops.secret."home-assistant/country".request = { - mode = "0440"; - owner = "hass"; - group = "hass"; - restartUnits = [ "home-assistant.service" ]; - }; - shb.sops.secret."home-assistant/latitude".request = { - mode = "0440"; - owner = "hass"; - group = "hass"; - restartUnits = [ "home-assistant.service" ]; - }; - shb.sops.secret."home-assistant/longitude".request = { - mode = "0440"; - owner = "hass"; - group = "hass"; - restartUnits = [ "home-assistant.service" ]; - }; - shb.sops.secret."home-assistant/time_zone".request = { - mode = "0440"; - owner = "hass"; - group = "hass"; - restartUnits = [ "home-assistant.service" ]; - }; - - nixpkgs.config.permittedInsecurePackages = [ - "openssl-1.1.1w" - ]; - }; - - ldap = { config, ... }: { - shb.lldap = { - enable = true; - domain = "example.com"; - subdomain = "ldap"; - ldapPort = 3890; - webUIListenPort = 17170; - dcdomain = "dc=example,dc=com"; - ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result; - jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result; - }; - shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request; - shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request; - - shb.home-assistant.ldap = { - enable = true; - host = "127.0.0.1"; - port = config.shb.lldap.webUIListenPort; - userGroup = "homeassistant_user"; - }; - }; sopsConfig = { sops.age.keyFile = "/etc/sops/my_key"; environment.etc."sops/my_key".source = ./keys.txt; }; in - { - nixosConfigurations = { - basic = pkgs.nixosSystem { - system = "x86_64-linux"; - modules = [ - basic - sopsConfig - ]; - }; - - ldap = pkgs.nixosSystem { - system = "x86_64-linux"; - modules = [ - basic - ldap - sopsConfig - ]; - }; + { + nixosConfigurations = { + basic = pkgs.nixosSystem { + system = "x86_64-linux"; + modules = [ + basic + sopsConfig + ]; }; - colmena = { - meta = { - nixpkgs = import nixpkgs' { - system = "x86_64-linux"; - }; - specialArgs = inputs; - }; - - basic = { config, ... }: { - imports = [ - basic - ]; - - # Used by colmena to know which target host to deploy to. - deployment = { - targetHost = "example"; - targetUser = "nixos"; - targetPort = 2222; - }; - }; - - ldap = { config, ... }: { - imports = [ - basic - ldap - ]; - - # Used by colmena to know which target host to deploy to. - deployment = { - targetHost = "example"; - targetUser = "nixos"; - targetPort = 2222; - }; - }; + ldap = pkgs.nixosSystem { + system = "x86_64-linux"; + modules = [ + basic + ldap + sopsConfig + ]; }; }; + + colmena = { + meta = { + nixpkgs = import nixpkgs' { + system = "x86_64-linux"; + }; + specialArgs = inputs; + }; + + basic = + { config, ... }: + { + imports = [ + basic + ]; + + # Used by colmena to know which target host to deploy to. + deployment = { + targetHost = "example"; + targetUser = "nixos"; + targetPort = 2222; + }; + }; + + ldap = + { config, ... }: + { + imports = [ + basic + ldap + ]; + + # Used by colmena to know which target host to deploy to. + deployment = { + targetHost = "example"; + targetUser = "nixos"; + targetPort = 2222; + }; + }; + }; + }; } diff --git a/demo/homeassistant/hardware-configuration.nix b/demo/homeassistant/hardware-configuration.nix index b94f3e7..569631e 100644 --- a/demo/homeassistant/hardware-configuration.nix +++ b/demo/homeassistant/hardware-configuration.nix @@ -3,52 +3,65 @@ # Do not modify this file! It was generated by ‘nixos-generate-config’ # and may be overwritten by future invocations. Please make changes # to /etc/nixos/configuration.nix instead. -{ config, lib, pkgs, modulesPath, ... }: +{ + config, + lib, + pkgs, + modulesPath, + ... +}: { - imports = - [ (modulesPath + "/profiles/qemu-guest.nix") - ]; + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + ]; - boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ]; + boot.initrd.availableKernelModules = [ + "ata_piix" + "uhci_hcd" + "virtio_pci" + "floppy" + "sr_mod" + "virtio_blk" + ]; boot.initrd.kernelModules = [ ]; boot.kernelModules = [ "kvm-intel" ]; boot.extraModulePackages = [ ]; - fileSystems."/" = - { device = "/dev/vda"; - fsType = "ext4"; - }; + fileSystems."/" = { + device = "/dev/vda"; + fsType = "ext4"; + }; - fileSystems."/nix/.ro-store" = - { device = "nix-store"; - fsType = "9p"; - }; + fileSystems."/nix/.ro-store" = { + device = "nix-store"; + fsType = "9p"; + }; - fileSystems."/nix/.rw-store" = - { device = "tmpfs"; - fsType = "tmpfs"; - }; + fileSystems."/nix/.rw-store" = { + device = "tmpfs"; + fsType = "tmpfs"; + }; - fileSystems."/tmp/shared" = - { device = "shared"; - fsType = "9p"; - }; + fileSystems."/tmp/shared" = { + device = "shared"; + fsType = "9p"; + }; - fileSystems."/tmp/xchg" = - { device = "xchg"; - fsType = "9p"; - }; + fileSystems."/tmp/xchg" = { + device = "xchg"; + fsType = "9p"; + }; - fileSystems."/nix/store" = - { device = "overlay"; - fsType = "overlay"; - }; + fileSystems."/nix/store" = { + device = "overlay"; + fsType = "overlay"; + }; - fileSystems."/boot" = - { device = "/dev/vdb2"; - fsType = "vfat"; - }; + fileSystems."/boot" = { + device = "/dev/vdb2"; + fsType = "vfat"; + }; swapDevices = [ ]; diff --git a/demo/nextcloud/configuration.nix b/demo/nextcloud/configuration.nix index 15d8c8c..b5df90e 100644 --- a/demo/nextcloud/configuration.nix +++ b/demo/nextcloud/configuration.nix @@ -5,17 +5,17 @@ let targetPort = 2222; in { - imports = - [ # Include the results of the hardware scan. - ./hardware-configuration.nix - ]; + imports = [ + # Include the results of the hardware scan. + ./hardware-configuration.nix + ]; boot.loader.grub.enable = true; boot.kernelModules = [ "kvm-intel" ]; system.stateVersion = "22.11"; # Options above are generate by running nixos-generate-config on the VM. - + # Needed otherwise deploy will say system won't be able to boot. boot.loader.grub.device = "/dev/vdb"; # Needed to avoid getting into not available disk space in /boot. @@ -27,7 +27,10 @@ in # Options above are needed to deploy in a VM. - nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; # We need to create the user we will deploy with. users.users.${targetUser} = { @@ -42,9 +45,11 @@ in # The user we're deploying with must be able to run sudo without password. security.sudo.extraRules = [ - { users = [ targetUser ]; + { + users = [ targetUser ]; commands = [ - { command = "ALL"; + { + command = "ALL"; options = [ "NOPASSWD" ]; } ]; diff --git a/demo/nextcloud/flake.nix b/demo/nextcloud/flake.nix index 7c02947..f46ca01 100644 --- a/demo/nextcloud/flake.nix +++ b/demo/nextcloud/flake.nix @@ -6,236 +6,256 @@ sops-nix.url = "github:Mic92/sops-nix"; }; - outputs = inputs@{ self, selfhostblocks, sops-nix }: + outputs = + inputs@{ + self, + selfhostblocks, + sops-nix, + }: let system = "x86_64-linux"; nixpkgs' = selfhostblocks.lib.${system}.patchedNixpkgs; inherit (selfhostblocks.lib.${system}) pkgs; - basic = { config, ... }: { - imports = [ - ./configuration.nix - selfhostblocks.nixosModules.authelia - selfhostblocks.nixosModules.nextcloud-server - selfhostblocks.nixosModules.nginx - selfhostblocks.nixosModules.sops - selfhostblocks.nixosModules.ssl - sops-nix.nixosModules.default - ]; + basic = + { config, ... }: + { + imports = [ + ./configuration.nix + selfhostblocks.nixosModules.authelia + selfhostblocks.nixosModules.nextcloud-server + selfhostblocks.nixosModules.nginx + selfhostblocks.nixosModules.sops + selfhostblocks.nixosModules.ssl + sops-nix.nixosModules.default + ]; - sops.defaultSopsFile = ./secrets.yaml; + sops.defaultSopsFile = ./secrets.yaml; - shb.nextcloud = { - enable = true; - domain = "example.com"; - subdomain = "n"; - dataDir = "/var/lib/nextcloud"; - tracing = null; - defaultPhoneRegion = "US"; + shb.nextcloud = { + enable = true; + domain = "example.com"; + subdomain = "n"; + dataDir = "/var/lib/nextcloud"; + tracing = null; + defaultPhoneRegion = "US"; - # This option is only needed because we do not access Nextcloud at the default port in the VM. - port = 8080; + # This option is only needed because we do not access Nextcloud at the default port in the VM. + port = 8080; - adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result; + adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result; - apps = { - previewgenerator.enable = true; - }; - }; - shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request; - - # Set to true for more debug info with `journalctl -f -u nginx`. - shb.nginx.accessLog = true; - shb.nginx.debugLog = false; - }; - - ldap = { config, ... }: { - shb.lldap = { - enable = true; - domain = "example.com"; - subdomain = "ldap"; - ldapPort = 3890; - webUIListenPort = 17170; - dcdomain = "dc=example,dc=com"; - ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result; - jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result; - }; - shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request; - shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request; - - shb.nextcloud.apps.ldap = { - enable = true; - host = "127.0.0.1"; - port = config.shb.lldap.ldapPort; - dcdomain = config.shb.lldap.dcdomain; - adminName = "admin"; - adminPassword.result = config.shb.sops.secret."nextcloud/ldap_admin_password".result; - userGroup = "nextcloud_user"; - }; - shb.sops.secret."nextcloud/ldap_admin_password" = { - request = config.shb.nextcloud.apps.ldap.adminPassword.request; - settings.key = "lldap/user_password"; - }; - }; - - sso = { config, lib, ... }: { - shb.certs = { - cas.selfsigned.myca = { - name = "My CA"; - }; - certs.selfsigned = { - n = { - ca = config.shb.certs.cas.selfsigned.myca; - domain = "*.example.com"; - group = "nginx"; + apps = { + previewgenerator.enable = true; }; }; - }; - shb.nextcloud = { - port = lib.mkForce null; - ssl = config.shb.certs.certs.selfsigned.n; - }; - shb.lldap.ssl = config.shb.certs.certs.selfsigned.n; + shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request; - services.dnsmasq = { - enable = true; - settings = { - domain-needed = true; - # no-resolv = true; - bogus-priv = true; - address = - map (hostname: "/${hostname}/127.0.0.1") [ + # Set to true for more debug info with `journalctl -f -u nginx`. + shb.nginx.accessLog = true; + shb.nginx.debugLog = false; + }; + + ldap = + { config, ... }: + { + shb.lldap = { + enable = true; + domain = "example.com"; + subdomain = "ldap"; + ldapPort = 3890; + webUIListenPort = 17170; + dcdomain = "dc=example,dc=com"; + ldapUserPassword.result = config.shb.sops.secret."lldap/user_password".result; + jwtSecret.result = config.shb.sops.secret."lldap/jwt_secret".result; + }; + shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPassword.request; + shb.sops.secret."lldap/jwt_secret".request = config.shb.lldap.jwtSecret.request; + + shb.nextcloud.apps.ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.lldap.ldapPort; + dcdomain = config.shb.lldap.dcdomain; + adminName = "admin"; + adminPassword.result = config.shb.sops.secret."nextcloud/ldap_admin_password".result; + userGroup = "nextcloud_user"; + }; + shb.sops.secret."nextcloud/ldap_admin_password" = { + request = config.shb.nextcloud.apps.ldap.adminPassword.request; + settings.key = "lldap/user_password"; + }; + }; + + sso = + { config, lib, ... }: + { + shb.certs = { + cas.selfsigned.myca = { + name = "My CA"; + }; + certs.selfsigned = { + n = { + ca = config.shb.certs.cas.selfsigned.myca; + domain = "*.example.com"; + group = "nginx"; + }; + }; + }; + shb.nextcloud = { + port = lib.mkForce null; + ssl = config.shb.certs.certs.selfsigned.n; + }; + shb.lldap.ssl = config.shb.certs.certs.selfsigned.n; + + services.dnsmasq = { + enable = true; + settings = { + domain-needed = true; + # no-resolv = true; + bogus-priv = true; + address = map (hostname: "/${hostname}/127.0.0.1") [ "example.com" "n.example.com" "ldap.example.com" "auth.example.com" ]; + }; + }; + + shb.authelia = { + enable = true; + domain = "example.com"; + subdomain = "auth"; + ssl = config.shb.certs.certs.selfsigned.n; + ldapPort = config.shb.lldap.ldapPort; + ldapHostname = "127.0.0.1"; + dcdomain = config.shb.lldap.dcdomain; + + secrets = { + jwtSecret.result = config.shb.sops.secret."authelia/jwt_secret".result; + ldapAdminPassword.result = config.shb.sops.secret."authelia/ldap_admin_password".result; + sessionSecret.result = config.shb.sops.secret."authelia/session_secret".result; + storageEncryptionKey.result = config.shb.sops.secret."authelia/storage_encryption_key".result; + identityProvidersOIDCHMACSecret.result = config.shb.sops.secret."authelia/hmac_secret".result; + identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secret."authelia/private_key".result; + }; + }; + shb.sops.secret."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request; + shb.sops.secret."authelia/ldap_admin_password" = { + request = config.shb.authelia.secrets.ldapAdminPassword.request; + settings.key = "lldap/user_password"; + }; + shb.sops.secret."authelia/session_secret".request = + config.shb.authelia.secrets.sessionSecret.request; + shb.sops.secret."authelia/storage_encryption_key".request = + config.shb.authelia.secrets.storageEncryptionKey.request; + shb.sops.secret."authelia/hmac_secret".request = + config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; + shb.sops.secret."authelia/private_key".request = + config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; + + shb.nextcloud.apps.sso = { + enable = true; + endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + clientID = "nextcloud"; + fallbackDefaultAuth = true; + + secret.result = config.shb.sops.secret."nextcloud/sso/secret".result; + secretForAuthelia.result = config.shb.sops.secret."authelia/nextcloud_sso_secret".result; + }; + shb.sops.secret."nextcloud/sso/secret".request = config.shb.nextcloud.apps.sso.secret.request; + shb.sops.secret."authelia/nextcloud_sso_secret" = { + request = config.shb.nextcloud.apps.sso.secretForAuthelia.request; + settings.key = "nextcloud/sso/secret"; }; }; - shb.authelia = { - enable = true; - domain = "example.com"; - subdomain = "auth"; - ssl = config.shb.certs.certs.selfsigned.n; - ldapPort = config.shb.lldap.ldapPort; - ldapHostname = "127.0.0.1"; - dcdomain = config.shb.lldap.dcdomain; - - secrets = { - jwtSecret.result = config.shb.sops.secret."authelia/jwt_secret".result; - ldapAdminPassword.result = config.shb.sops.secret."authelia/ldap_admin_password".result; - sessionSecret.result = config.shb.sops.secret."authelia/session_secret".result; - storageEncryptionKey.result = config.shb.sops.secret."authelia/storage_encryption_key".result; - identityProvidersOIDCHMACSecret.result = config.shb.sops.secret."authelia/hmac_secret".result; - identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secret."authelia/private_key".result; - }; - }; - shb.sops.secret."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request; - shb.sops.secret."authelia/ldap_admin_password" = { - request = config.shb.authelia.secrets.ldapAdminPassword.request; - settings.key = "lldap/user_password"; - }; - shb.sops.secret."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request; - shb.sops.secret."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request; - shb.sops.secret."authelia/hmac_secret".request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; - shb.sops.secret."authelia/private_key".request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; - - shb.nextcloud.apps.sso = { - enable = true; - endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - clientID = "nextcloud"; - fallbackDefaultAuth = true; - - secret.result = config.shb.sops.secret."nextcloud/sso/secret".result; - secretForAuthelia.result = config.shb.sops.secret."authelia/nextcloud_sso_secret".result; - }; - shb.sops.secret."nextcloud/sso/secret".request = config.shb.nextcloud.apps.sso.secret.request; - shb.sops.secret."authelia/nextcloud_sso_secret" = { - request = config.shb.nextcloud.apps.sso.secretForAuthelia.request; - settings.key = "nextcloud/sso/secret"; - }; - }; - sopsConfig = { sops.age.keyFile = "/etc/sops/my_key"; environment.etc."sops/my_key".source = ./keys.txt; }; in - { - nixosConfigurations = { - basic = pkgs.nixosSystem { - system = "x86_64-linux"; - modules = [ - sopsConfig - basic - ]; - }; - ldap = pkgs.nixosSystem { - system = "x86_64-linux"; - modules = [ - sopsConfig - basic - ldap - ]; - }; - sso = pkgs.nixosSystem { - system = "x86_64-linux"; - modules = [ - sopsConfig - basic - ldap - sso - ]; - }; + { + nixosConfigurations = { + basic = pkgs.nixosSystem { + system = "x86_64-linux"; + modules = [ + sopsConfig + basic + ]; }; - - colmena = { - meta = { - nixpkgs = import nixpkgs' { - system = "x86_64-linux"; - }; - specialArgs = inputs; - }; - - basic = { config, ... }: { - imports = [ - basic - ]; - - deployment = { - targetHost = "example"; - targetUser = "nixos"; - targetPort = 2222; - }; - }; - - ldap = { config, ... }: { - imports = [ - basic - ldap - ]; - - deployment = { - targetHost = "example"; - targetUser = "nixos"; - targetPort = 2222; - }; - }; - - sso = { config, ... }: { - imports = [ - basic - ldap - sso - ]; - - deployment = { - targetHost = "example"; - targetUser = "nixos"; - targetPort = 2222; - }; - }; + ldap = pkgs.nixosSystem { + system = "x86_64-linux"; + modules = [ + sopsConfig + basic + ldap + ]; + }; + sso = pkgs.nixosSystem { + system = "x86_64-linux"; + modules = [ + sopsConfig + basic + ldap + sso + ]; }; }; + + colmena = { + meta = { + nixpkgs = import nixpkgs' { + system = "x86_64-linux"; + }; + specialArgs = inputs; + }; + + basic = + { config, ... }: + { + imports = [ + basic + ]; + + deployment = { + targetHost = "example"; + targetUser = "nixos"; + targetPort = 2222; + }; + }; + + ldap = + { config, ... }: + { + imports = [ + basic + ldap + ]; + + deployment = { + targetHost = "example"; + targetUser = "nixos"; + targetPort = 2222; + }; + }; + + sso = + { config, ... }: + { + imports = [ + basic + ldap + sso + ]; + + deployment = { + targetHost = "example"; + targetUser = "nixos"; + targetPort = 2222; + }; + }; + }; + }; } diff --git a/demo/nextcloud/hardware-configuration.nix b/demo/nextcloud/hardware-configuration.nix index b94f3e7..569631e 100644 --- a/demo/nextcloud/hardware-configuration.nix +++ b/demo/nextcloud/hardware-configuration.nix @@ -3,52 +3,65 @@ # Do not modify this file! It was generated by ‘nixos-generate-config’ # and may be overwritten by future invocations. Please make changes # to /etc/nixos/configuration.nix instead. -{ config, lib, pkgs, modulesPath, ... }: +{ + config, + lib, + pkgs, + modulesPath, + ... +}: { - imports = - [ (modulesPath + "/profiles/qemu-guest.nix") - ]; + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + ]; - boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "floppy" "sr_mod" "virtio_blk" ]; + boot.initrd.availableKernelModules = [ + "ata_piix" + "uhci_hcd" + "virtio_pci" + "floppy" + "sr_mod" + "virtio_blk" + ]; boot.initrd.kernelModules = [ ]; boot.kernelModules = [ "kvm-intel" ]; boot.extraModulePackages = [ ]; - fileSystems."/" = - { device = "/dev/vda"; - fsType = "ext4"; - }; + fileSystems."/" = { + device = "/dev/vda"; + fsType = "ext4"; + }; - fileSystems."/nix/.ro-store" = - { device = "nix-store"; - fsType = "9p"; - }; + fileSystems."/nix/.ro-store" = { + device = "nix-store"; + fsType = "9p"; + }; - fileSystems."/nix/.rw-store" = - { device = "tmpfs"; - fsType = "tmpfs"; - }; + fileSystems."/nix/.rw-store" = { + device = "tmpfs"; + fsType = "tmpfs"; + }; - fileSystems."/tmp/shared" = - { device = "shared"; - fsType = "9p"; - }; + fileSystems."/tmp/shared" = { + device = "shared"; + fsType = "9p"; + }; - fileSystems."/tmp/xchg" = - { device = "xchg"; - fsType = "9p"; - }; + fileSystems."/tmp/xchg" = { + device = "xchg"; + fsType = "9p"; + }; - fileSystems."/nix/store" = - { device = "overlay"; - fsType = "overlay"; - }; + fileSystems."/nix/store" = { + device = "overlay"; + fsType = "overlay"; + }; - fileSystems."/boot" = - { device = "/dev/vdb2"; - fsType = "vfat"; - }; + fileSystems."/boot" = { + device = "/dev/vdb2"; + fsType = "vfat"; + }; swapDevices = [ ]; diff --git a/docs/default.nix b/docs/default.nix index 21a7a33..7b84b36 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -1,40 +1,49 @@ # Taken nearly verbatim from https://github.com/nix-community/home-manager/pull/4673 # Read these docs online at https://shb.skarabox.com. -{ pkgs -, buildPackages -, lib -, nmdsrc -, stdenv -, documentation-highlighter -, nixos-render-docs +{ + pkgs, + buildPackages, + lib, + nmdsrc, + stdenv, + documentation-highlighter, + nixos-render-docs, -, release -, allModules + release, + allModules, -, version ? builtins.readFile ../VERSION -, substituteVersionIn + version ? builtins.readFile ../VERSION, + substituteVersionIn, -, modules + modules, }: let shbPath = toString ./..; - gitHubDeclaration = user: repo: subpath: - let urlRef = "main"; - end = if subpath == "" then "" else "/" + subpath; - in { + gitHubDeclaration = + user: repo: subpath: + let + urlRef = "main"; + end = if subpath == "" then "" else "/" + subpath; + in + { url = "https://github.com/${user}/${repo}/blob/${urlRef}${end}"; name = "<${repo}${end}>"; }; ghRoot = (gitHubDeclaration "ibizaman" "selfhostblocks" "").url; - buildOptionsDocs = { modules, filterOptionPath ? null }: args: + buildOptionsDocs = + { + modules, + filterOptionPath ? null, + }: + args: let config = { _module.check = false; - _module.args = {}; + _module.args = { }; system.stateVersion = "22.11"; }; @@ -52,41 +61,56 @@ let }; options = lib.setAttrByPath filterOptionPath (lib.getAttrFromPath filterOptionPath eval.options); - in buildPackages.nixosOptionsDoc ({ - inherit options; + in + buildPackages.nixosOptionsDoc ( + { + inherit options; - transformOptions = opt: - opt // { - # Clean up declaration sites to not refer to the Home Manager - # source tree. - declarations = map (decl: - gitHubDeclaration "ibizaman" "selfhostblocks" - (lib.removePrefix "/" (lib.removePrefix shbPath (toString decl)))) opt.declarations; - }; - } // builtins.removeAttrs args [ "includeModuleSystemOptions" ]); + transformOptions = + opt: + opt + // { + # Clean up declaration sites to not refer to the Home Manager + # source tree. + declarations = map ( + decl: + gitHubDeclaration "ibizaman" "selfhostblocks" ( + lib.removePrefix "/" (lib.removePrefix shbPath (toString decl)) + ) + ) opt.declarations; + }; + } + // builtins.removeAttrs args [ "includeModuleSystemOptions" ] + ); scrubbedModule = { _module.args.pkgs = lib.mkForce (nmd.scrubDerivations "pkgs" pkgs); _module.check = false; }; - allOptionsDocs = paths: (buildOptionsDocs - { - modules = paths ++ allModules ++ [ scrubbedModule ]; - filterOptionPath = [ "shb" ]; - } - { - variablelistId = "selfhostblocks-options"; - }).optionsJSON; + allOptionsDocs = + paths: + (buildOptionsDocs + { + modules = paths ++ allModules ++ [ scrubbedModule ]; + filterOptionPath = [ "shb" ]; + } + { + variablelistId = "selfhostblocks-options"; + } + ).optionsJSON; - individualModuleOptionsDocs = filterOptionPath: paths: (buildOptionsDocs - { - modules = paths ++ [ scrubbedModule ]; - inherit filterOptionPath; - } - { - variablelistId = "selfhostblocks-options"; - }).optionsJSON; + individualModuleOptionsDocs = + filterOptionPath: paths: + (buildOptionsDocs + { + modules = paths ++ [ scrubbedModule ]; + inherit filterOptionPath; + } + { + variablelistId = "selfhostblocks-options"; + } + ).optionsJSON; nmd = import nmdsrc { inherit lib; @@ -94,15 +118,15 @@ let # `nmd` uses to work around the broken stylesheets in # `docbook-xsl-ns`, so we restore the patched version here. pkgs = pkgs // { - docbook-xsl-ns = - pkgs.docbook-xsl-ns.override { withManOptDedupPatch = true; }; + docbook-xsl-ns = pkgs.docbook-xsl-ns.override { withManOptDedupPatch = true; }; }; }; outputPath = "share/doc/selfhostblocks"; manpage-urls = pkgs.writeText "manpage-urls.json" ''{}''; -in stdenv.mkDerivation { +in +stdenv.mkDerivation { name = "self-host-blocks-manual"; nativeBuildInputs = [ nixos-render-docs ]; @@ -135,28 +159,41 @@ in stdenv.mkDerivation { ${nmdsrc}/static/highlightjs/highlight.load.js '' - + lib.concatStringsSep "\n" (map (m: '' - substituteInPlace ${m} --replace '@VERSION@' ${version} - '') substituteVersionIn) + + lib.concatStringsSep "\n" ( + map (m: '' + substituteInPlace ${m} --replace '@VERSION@' ${version} + '') substituteVersionIn + ) + '' substituteInPlace ./options.md \ --replace \ '@OPTIONS_JSON@' \ - ${allOptionsDocs [ - (pkgs.path + "/nixos/modules/services/misc/forgejo.nix") - ]}/share/doc/nixos/options.json + ${ + allOptionsDocs [ + (pkgs.path + "/nixos/modules/services/misc/forgejo.nix") + ] + }/share/doc/nixos/options.json '' - + lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cfg': - let - cfg = if builtins.isAttrs cfg' then cfg' else { module = cfg'; }; - module = if builtins.isList cfg.module then cfg.module else [ cfg.module ]; - optionRoot = cfg.optionRoot or [ "shb" (lib.last (lib.splitString "/" name)) ]; - in '' - substituteInPlace ./modules/${name}/docs/default.md \ - --replace-fail \ - '@OPTIONS_JSON@' \ - ${individualModuleOptionsDocs optionRoot module}/share/doc/nixos/options.json - '') modules) + + lib.concatStringsSep "\n" ( + lib.mapAttrsToList ( + name: cfg': + let + cfg = if builtins.isAttrs cfg' then cfg' else { module = cfg'; }; + module = if builtins.isList cfg.module then cfg.module else [ cfg.module ]; + optionRoot = + cfg.optionRoot or [ + "shb" + (lib.last (lib.splitString "/" name)) + ]; + in + '' + substituteInPlace ./modules/${name}/docs/default.md \ + --replace-fail \ + '@OPTIONS_JSON@' \ + ${individualModuleOptionsDocs optionRoot module}/share/doc/nixos/options.json + '' + ) modules + ) + '' find . -name "*.md" -print0 | \ while IFS= read -r -d ''' f; do diff --git a/flake.nix b/flake.nix index 094b082..841d3fd 100644 --- a/flake.nix +++ b/flake.nix @@ -11,66 +11,89 @@ }; }; - outputs = inputs@{ self, nixpkgs, nix-flake-tests, flake-utils, nmdsrc, ... }: flake-utils.lib.eachDefaultSystem (system: - let - originPkgs = nixpkgs.legacyPackages.${system}; - shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [ - # Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged. - ./patches/lldap.patch + outputs = + inputs@{ + self, + nixpkgs, + nix-flake-tests, + flake-utils, + nmdsrc, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + originPkgs = nixpkgs.legacyPackages.${system}; + shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [ + # Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged. + ./patches/lldap.patch - # Leaving commented out as an example. - # (originPkgs.fetchpatch { - # url = "https://github.com/NixOS/nixpkgs/pull/317107.patch"; - # hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk="; - # }) - ]; - patchNixpkgs = { - nixpkgs, - patches, - system, - }: nixpkgs.legacyPackages.${system}.applyPatches { - name = "nixpkgs-patched"; - src = nixpkgs; - inherit patches; - }; - patchedNixpkgs = (patchNixpkgs { - nixpkgs = inputs.nixpkgs; - patches = shbPatches; - inherit system; - }); - pkgs = import patchedNixpkgs { - inherit system; - config.allowUnfree = true; - overlays = [ - (final: prev: { - lib = prev.lib // { - shb = self.lib.${system}; - evalModules = args: ((prev.lib.makeOverridable prev.lib.evalModules) args).override (prevAttrs: { - specialArgs = (prevAttrs.specialArgs or {}) // { inherit (pkgs) lib; }; - }); - }; - nixosSystem = args: ((prev.lib.makeOverridable (import "${patchedNixpkgs}/nixos/lib/eval-config.nix")) args).override (prevAttrs: { - inherit (pkgs) lib; - }); - }) + # Leaving commented out as an example. + # (originPkgs.fetchpatch { + # url = "https://github.com/NixOS/nixpkgs/pull/317107.patch"; + # hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk="; + # }) ]; - }; + patchNixpkgs = + { + nixpkgs, + patches, + system, + }: + nixpkgs.legacyPackages.${system}.applyPatches { + name = "nixpkgs-patched"; + src = nixpkgs; + inherit patches; + }; + patchedNixpkgs = ( + patchNixpkgs { + nixpkgs = inputs.nixpkgs; + patches = shbPatches; + inherit system; + } + ); + pkgs = import patchedNixpkgs { + inherit system; + config.allowUnfree = true; + overlays = [ + (final: prev: { + lib = prev.lib // { + shb = self.lib.${system}; + evalModules = + args: + ((prev.lib.makeOverridable prev.lib.evalModules) args).override (prevAttrs: { + specialArgs = (prevAttrs.specialArgs or { }) // { + inherit (pkgs) lib; + }; + }); + }; + nixosSystem = + args: + ((prev.lib.makeOverridable (import "${patchedNixpkgs}/nixos/lib/eval-config.nix")) args).override + (prevAttrs: { + inherit (pkgs) lib; + }); + }) + ]; + }; - # The contract dummies are used to show options for contracts. - contractDummyModules = [ - modules/contracts/backup/dummyModule.nix - modules/contracts/ssl/dummyModule.nix - ]; - in + # The contract dummies are used to show options for contracts. + contractDummyModules = [ + modules/contracts/backup/dummyModule.nix + modules/contracts/ssl/dummyModule.nix + ]; + in { formatter = pkgs.nixfmt-tree; packages.manualHtml = pkgs.callPackage ./docs { inherit nmdsrc; - allModules = self.nixosModules.default.imports - ++ [ - self.nixosModules.sops - ] ++ contractDummyModules; + allModules = + self.nixosModules.default.imports + ++ [ + self.nixosModules.sops + ] + ++ contractDummyModules; release = builtins.readFile ./VERSION; substituteVersionIn = [ @@ -82,7 +105,10 @@ "blocks/lldap" = ./modules/blocks/lldap.nix; "blocks/ssl" = { module = ./modules/blocks/ssl.nix; - optionRoot = [ "shb" "certs" ]; + optionRoot = [ + "shb" + "certs" + ]; }; "blocks/mitmdump" = ./modules/blocks/mitmdump.nix; "blocks/monitoring" = ./modules/blocks/monitoring.nix; @@ -99,154 +125,196 @@ "services/karakeep" = ./modules/services/karakeep.nix; "services/nextcloud-server" = { module = ./modules/services/nextcloud-server.nix; - optionRoot = [ "shb" "nextcloud" ]; + optionRoot = [ + "shb" + "nextcloud" + ]; }; "services/open-webui" = ./modules/services/open-webui.nix; "services/pinchflat" = ./modules/services/pinchflat.nix; "services/vaultwarden" = ./modules/services/vaultwarden.nix; "contracts/backup" = { module = ./modules/contracts/backup/dummyModule.nix; - optionRoot = [ "shb" "contracts" "backup" ]; + optionRoot = [ + "shb" + "contracts" + "backup" + ]; }; "contracts/databasebackup" = { module = ./modules/contracts/databasebackup/dummyModule.nix; - optionRoot = [ "shb" "contracts" "databasebackup" ]; + optionRoot = [ + "shb" + "contracts" + "databasebackup" + ]; }; "contracts/secret" = { module = ./modules/contracts/secret/dummyModule.nix; - optionRoot = [ "shb" "contracts" "secret" ]; + optionRoot = [ + "shb" + "contracts" + "secret" + ]; }; "contracts/ssl" = { module = ./modules/contracts/ssl/dummyModule.nix; - optionRoot = [ "shb" "contracts" "ssl" ]; + optionRoot = [ + "shb" + "contracts" + "ssl" + ]; }; }; }; # Documentation redirect generation tool - scans HTML files for anchor mappings - packages.generateRedirects = - let - # Python patch to inject redirect collector - pythonPatch = pkgs.writeText "nixos-render-docs-patch.py" '' - # Load redirect collector patch - try: - import sys, os - sys.path.insert(0, os.path.dirname(__file__) + '/..') - import missing_refs_collector - except Exception as e: - print(f"Warning: Failed to load redirect collector: {e}", file=sys.stderr) - ''; - - # Patched nixos-render-docs that collects redirects during HTML generation - nixos-render-docs-patched = pkgs.writeShellApplication { - name = "nixos-render-docs"; - runtimeInputs = [ pkgs.nixos-render-docs ]; - text = '' - TEMP_DIR=$(mktemp -d); trap 'rm -rf "$TEMP_DIR"' EXIT - - cp -r ${pkgs.nixos-render-docs}/${pkgs.python3.sitePackages}/nixos_render_docs "$TEMP_DIR/" - chmod -R +w "$TEMP_DIR" - cp ${./docs/generate-redirects-nixos-render-docs.py} "$TEMP_DIR/missing_refs_collector.py" - echo '{}' > "$TEMP_DIR/empty_redirects.json" - cat ${pythonPatch} >> "$TEMP_DIR/nixos_render_docs/__init__.py" - - ARGS=() - while [[ $# -gt 0 ]]; do - case $1 in - --redirects) ARGS+=("$1" "$TEMP_DIR/empty_redirects.json"); shift 2 ;; - *) ARGS+=("$1"); shift ;; - esac - done - - export PYTHONPATH="$TEMP_DIR:''${PYTHONPATH:-}" - nixos-render-docs "''${ARGS[@]}" + packages.generateRedirects = + let + # Python patch to inject redirect collector + pythonPatch = pkgs.writeText "nixos-render-docs-patch.py" '' + # Load redirect collector patch + try: + import sys, os + sys.path.insert(0, os.path.dirname(__file__) + '/..') + import missing_refs_collector + except Exception as e: + print(f"Warning: Failed to load redirect collector: {e}", file=sys.stderr) ''; - }; - in - (self.packages.${system}.manualHtml.override { - nixos-render-docs = nixos-render-docs-patched; - }).overrideAttrs (old: { - installPhase = '' - ${old.installPhase} - ln -sf share/doc/selfhostblocks/redirects.json $out/redirects.json - ''; - }); + + # Patched nixos-render-docs that collects redirects during HTML generation + nixos-render-docs-patched = pkgs.writeShellApplication { + name = "nixos-render-docs"; + runtimeInputs = [ pkgs.nixos-render-docs ]; + text = '' + TEMP_DIR=$(mktemp -d); trap 'rm -rf "$TEMP_DIR"' EXIT + + cp -r ${pkgs.nixos-render-docs}/${pkgs.python3.sitePackages}/nixos_render_docs "$TEMP_DIR/" + chmod -R +w "$TEMP_DIR" + cp ${./docs/generate-redirects-nixos-render-docs.py} "$TEMP_DIR/missing_refs_collector.py" + echo '{}' > "$TEMP_DIR/empty_redirects.json" + cat ${pythonPatch} >> "$TEMP_DIR/nixos_render_docs/__init__.py" + + ARGS=() + while [[ $# -gt 0 ]]; do + case $1 in + --redirects) ARGS+=("$1" "$TEMP_DIR/empty_redirects.json"); shift 2 ;; + *) ARGS+=("$1"); shift ;; + esac + done + + export PYTHONPATH="$TEMP_DIR:''${PYTHONPATH:-}" + nixos-render-docs "''${ARGS[@]}" + ''; + }; + in + (self.packages.${system}.manualHtml.override { + nixos-render-docs = nixos-render-docs-patched; + }).overrideAttrs + (old: { + installPhase = '' + ${old.installPhase} + ln -sf share/doc/selfhostblocks/redirects.json $out/redirects.json + ''; + }); lib = - (pkgs.callPackage ./lib {}) - // (pkgs.callPackage ./test/common.nix {}) + (pkgs.callPackage ./lib { }) + // (pkgs.callPackage ./test/common.nix { }) // { - contracts = pkgs.callPackage ./modules/contracts {}; + contracts = pkgs.callPackage ./modules/contracts { }; patches = shbPatches; inherit patchNixpkgs patchedNixpkgs pkgs; }; checks = let - inherit (pkgs.lib) foldl foldlAttrs removeAttrs mergeAttrs optionalAttrs; + inherit (pkgs.lib) + foldl + foldlAttrs + removeAttrs + mergeAttrs + optionalAttrs + ; - importFiles = files: - map (m: pkgs.callPackage m {}) files; + importFiles = files: map (m: pkgs.callPackage m { }) files; - mergeTests = foldl mergeAttrs {}; + mergeTests = foldl mergeAttrs { }; - flattenAttrs = root: attrset: foldlAttrs (acc: name: value: acc // { - "${root}_${name}" = value; - }) {} attrset; + flattenAttrs = + root: attrset: + foldlAttrs ( + acc: name: value: + acc + // { + "${root}_${name}" = value; + } + ) { } attrset; - vm_test = name: path: flattenAttrs "vm_${name}" ( - removeAttrs (pkgs.callPackage path {}) [ "override" "overrideDerivation" ] - ); - in (optionalAttrs (system == "x86_64-linux") ({ - modules = pkgs.lib.shb.check { - inherit pkgs; - tests = - mergeTests (importFiles [ + vm_test = + name: path: + flattenAttrs "vm_${name}" ( + removeAttrs (pkgs.callPackage path { }) [ + "override" + "overrideDerivation" + ] + ); + in + (optionalAttrs (system == "x86_64-linux") ( + { + modules = pkgs.lib.shb.check { + inherit pkgs; + tests = mergeTests (importFiles [ ./test/modules/davfs.nix # TODO: Make this not use IFD ./test/modules/lib.nix ]); - }; + }; - # TODO: Make this not use IFD - lib = nix-flake-tests.lib.check { - inherit pkgs; - tests = pkgs.callPackage ./test/modules/lib.nix {}; - }; - } - // (vm_test "arr" ./test/services/arr.nix) - // (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix) - // (vm_test "deluge" ./test/services/deluge.nix) - // (vm_test "forgejo" ./test/services/forgejo.nix) - // (vm_test "grocy" ./test/services/grocy.nix) - // (vm_test "hledger" ./test/services/hledger.nix) - // (vm_test "immich" ./test/services/immich.nix) - // (vm_test "homeassistant" ./test/services/home-assistant.nix) - // (vm_test "jellyfin" ./test/services/jellyfin.nix) - // (vm_test "karakeep" ./test/services/karakeep.nix) - // (vm_test "monitoring" ./test/services/monitoring.nix) - // (vm_test "nextcloud" ./test/services/nextcloud.nix) - // (vm_test "open-webui" ./test/services/open-webui.nix) - // (vm_test "pinchflat" ./test/services/pinchflat.nix) - // (vm_test "vaultwarden" ./test/services/vaultwarden.nix) + # TODO: Make this not use IFD + lib = nix-flake-tests.lib.check { + inherit pkgs; + tests = pkgs.callPackage ./test/modules/lib.nix { }; + }; + } + // (vm_test "arr" ./test/services/arr.nix) + // (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix) + // (vm_test "deluge" ./test/services/deluge.nix) + // (vm_test "forgejo" ./test/services/forgejo.nix) + // (vm_test "grocy" ./test/services/grocy.nix) + // (vm_test "hledger" ./test/services/hledger.nix) + // (vm_test "immich" ./test/services/immich.nix) + // (vm_test "homeassistant" ./test/services/home-assistant.nix) + // (vm_test "jellyfin" ./test/services/jellyfin.nix) + // (vm_test "karakeep" ./test/services/karakeep.nix) + // (vm_test "monitoring" ./test/services/monitoring.nix) + // (vm_test "nextcloud" ./test/services/nextcloud.nix) + // (vm_test "open-webui" ./test/services/open-webui.nix) + // (vm_test "pinchflat" ./test/services/pinchflat.nix) + // (vm_test "vaultwarden" ./test/services/vaultwarden.nix) - // (vm_test "authelia" ./test/blocks/authelia.nix) - // (vm_test "lldap" ./test/blocks/lldap.nix) - // (vm_test "lib" ./test/blocks/lib.nix) - // (vm_test "mitmdump" ./test/blocks/mitmdump.nix) - // (vm_test "postgresql" ./test/blocks/postgresql.nix) - // (vm_test "restic" ./test/blocks/restic.nix) - // (vm_test "ssl" ./test/blocks/ssl.nix) + // (vm_test "authelia" ./test/blocks/authelia.nix) + // (vm_test "lldap" ./test/blocks/lldap.nix) + // (vm_test "lib" ./test/blocks/lib.nix) + // (vm_test "mitmdump" ./test/blocks/mitmdump.nix) + // (vm_test "postgresql" ./test/blocks/postgresql.nix) + // (vm_test "restic" ./test/blocks/restic.nix) + // (vm_test "ssl" ./test/blocks/ssl.nix) - // (vm_test "contracts-backup" ./test/contracts/backup.nix) - // (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix) - // (vm_test "contracts-secret" ./test/contracts/secret.nix) + // (vm_test "contracts-backup" ./test/contracts/backup.nix) + // (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix) + // (vm_test "contracts-secret" ./test/contracts/secret.nix) )); # To see the traces, run: # nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip - packages.playwright = - pkgs.callPackage ({ stdenvNoCC, makeWrapper, playwright }: stdenvNoCC.mkDerivation { + packages.playwright = pkgs.callPackage ( + { + stdenvNoCC, + makeWrapper, + playwright, + }: + stdenvNoCC.mkDerivation { name = "playwright"; src = playwright; @@ -261,97 +329,104 @@ --set PLAYWRIGHT_BROWSERS_PATH ${pkgs.playwright-driver.browsers} \ --set PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS true ''; - }) {}; + } + ) { }; # Run "nix run .#update-redirects" to regenerate docs/redirects.json apps.update-redirects = { type = "app"; - program = "${pkgs.writeShellApplication { - name = "update-redirects"; - runtimeInputs = [ pkgs.nix pkgs.jq ]; - text = '' - echo "=== SelfHostBlocks Redirects Updater ===" - echo "Generating fresh ./docs/redirects.json..." - - nix build .#generateRedirects || { echo "Error: Failed to generate redirects" >&2; exit 1; } - [[ -f result/redirects.json ]] || { echo "Error: Generated redirects file not found" >&2; exit 1; } - - echo "Generated $(jq 'keys | length' result/redirects.json) redirects" - - [[ -f docs/redirects.json ]] && cp docs/redirects.json docs/redirects.json.backup && echo "Created backup" - cp result/redirects.json docs/redirects.json - echo " Updated docs/redirects.json" - echo "To verify: nix build .#manualHtml" - ''; - }}/bin/update-redirects"; + program = "${ + pkgs.writeShellApplication { + name = "update-redirects"; + runtimeInputs = [ + pkgs.nix + pkgs.jq + ]; + text = '' + echo "=== SelfHostBlocks Redirects Updater ===" + echo "Generating fresh ./docs/redirects.json..." + + nix build .#generateRedirects || { echo "Error: Failed to generate redirects" >&2; exit 1; } + [[ -f result/redirects.json ]] || { echo "Error: Generated redirects file not found" >&2; exit 1; } + + echo "Generated $(jq 'keys | length' result/redirects.json) redirects" + + [[ -f docs/redirects.json ]] && cp docs/redirects.json docs/redirects.json.backup && echo "Created backup" + cp result/redirects.json docs/redirects.json + echo " Updated docs/redirects.json" + echo "To verify: nix build .#manualHtml" + ''; + } + }/bin/update-redirects"; }; } - ) // { - herculesCI.ciSystems = [ "x86_64-linux" ]; + ) + // { + herculesCI.ciSystems = [ "x86_64-linux" ]; - nixosModules.default = { - imports = [ - # blocks - self.nixosModules.authelia - self.nixosModules.davfs - self.nixosModules.hardcodedsecret - self.nixosModules.lldap - self.nixosModules.mitmdump - self.nixosModules.monitoring - self.nixosModules.nginx - self.nixosModules.postgresql - self.nixosModules.restic - self.nixosModules.ssl - self.nixosModules.tinyproxy - self.nixosModules.vpn - self.nixosModules.zfs + nixosModules.default = { + imports = [ + # blocks + self.nixosModules.authelia + self.nixosModules.davfs + self.nixosModules.hardcodedsecret + self.nixosModules.lldap + self.nixosModules.mitmdump + self.nixosModules.monitoring + self.nixosModules.nginx + self.nixosModules.postgresql + self.nixosModules.restic + self.nixosModules.ssl + self.nixosModules.tinyproxy + self.nixosModules.vpn + self.nixosModules.zfs - # services - self.nixosModules.arr - self.nixosModules.audiobookshelf - self.nixosModules.deluge - self.nixosModules.forgejo - self.nixosModules.grocy - self.nixosModules.hledger - self.nixosModules.immich - self.nixosModules.home-assistant - self.nixosModules.jellyfin - self.nixosModules.karakeep - self.nixosModules.nextcloud-server - self.nixosModules.open-webui - self.nixosModules.pinchflat - self.nixosModules.vaultwarden - ]; + # services + self.nixosModules.arr + self.nixosModules.audiobookshelf + self.nixosModules.deluge + self.nixosModules.forgejo + self.nixosModules.grocy + self.nixosModules.hledger + self.nixosModules.immich + self.nixosModules.home-assistant + self.nixosModules.jellyfin + self.nixosModules.karakeep + self.nixosModules.nextcloud-server + self.nixosModules.open-webui + self.nixosModules.pinchflat + self.nixosModules.vaultwarden + ]; + }; + + nixosModules.authelia = modules/blocks/authelia.nix; + nixosModules.davfs = modules/blocks/davfs.nix; + nixosModules.hardcodedsecret = modules/blocks/hardcodedsecret.nix; + nixosModules.lldap = modules/blocks/lldap.nix; + nixosModules.mitmdump = modules/blocks/mitmdump.nix; + nixosModules.monitoring = modules/blocks/monitoring.nix; + nixosModules.nginx = modules/blocks/nginx.nix; + nixosModules.postgresql = modules/blocks/postgresql.nix; + nixosModules.restic = modules/blocks/restic.nix; + nixosModules.ssl = modules/blocks/ssl.nix; + nixosModules.sops = modules/blocks/sops.nix; + nixosModules.tinyproxy = modules/blocks/tinyproxy.nix; + nixosModules.vpn = modules/blocks/vpn.nix; + nixosModules.zfs = modules/blocks/zfs.nix; + + nixosModules.arr = modules/services/arr.nix; + nixosModules.audiobookshelf = modules/services/audiobookshelf.nix; + nixosModules.deluge = modules/services/deluge.nix; + nixosModules.forgejo = modules/services/forgejo.nix; + nixosModules.grocy = modules/services/grocy.nix; + nixosModules.hledger = modules/services/hledger.nix; + nixosModules.immich = modules/services/immich.nix; + nixosModules.home-assistant = modules/services/home-assistant.nix; + nixosModules.jellyfin = modules/services/jellyfin.nix; + nixosModules.karakeep = modules/services/karakeep.nix; + nixosModules.nextcloud-server = modules/services/nextcloud-server.nix; + nixosModules.open-webui = modules/services/open-webui.nix; + nixosModules.pinchflat = modules/services/pinchflat.nix; + nixosModules.vaultwarden = modules/services/vaultwarden.nix; }; - - nixosModules.authelia = modules/blocks/authelia.nix; - nixosModules.davfs = modules/blocks/davfs.nix; - nixosModules.hardcodedsecret = modules/blocks/hardcodedsecret.nix; - nixosModules.lldap = modules/blocks/lldap.nix; - nixosModules.mitmdump = modules/blocks/mitmdump.nix; - nixosModules.monitoring = modules/blocks/monitoring.nix; - nixosModules.nginx = modules/blocks/nginx.nix; - nixosModules.postgresql = modules/blocks/postgresql.nix; - nixosModules.restic = modules/blocks/restic.nix; - nixosModules.ssl = modules/blocks/ssl.nix; - nixosModules.sops = modules/blocks/sops.nix; - nixosModules.tinyproxy = modules/blocks/tinyproxy.nix; - nixosModules.vpn = modules/blocks/vpn.nix; - nixosModules.zfs = modules/blocks/zfs.nix; - - nixosModules.arr = modules/services/arr.nix; - nixosModules.audiobookshelf = modules/services/audiobookshelf.nix; - nixosModules.deluge = modules/services/deluge.nix; - nixosModules.forgejo = modules/services/forgejo.nix; - nixosModules.grocy = modules/services/grocy.nix; - nixosModules.hledger = modules/services/hledger.nix; - nixosModules.immich = modules/services/immich.nix; - nixosModules.home-assistant = modules/services/home-assistant.nix; - nixosModules.jellyfin = modules/services/jellyfin.nix; - nixosModules.karakeep = modules/services/karakeep.nix; - nixosModules.nextcloud-server = modules/services/nextcloud-server.nix; - nixosModules.open-webui = modules/services/open-webui.nix; - nixosModules.pinchflat = modules/services/pinchflat.nix; - nixosModules.vaultwarden = modules/services/vaultwarden.nix; - }; } diff --git a/lib/default.nix b/lib/default.nix index 5590883..4dfac73 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -9,7 +9,14 @@ rec { # - resultPath is the location the config file should have on the filesystem. # - generator is a function taking two arguments name and value and returning path in the nix # nix store where the - replaceSecrets = { userConfig, resultPath, generator, user ? null, permissions ? "u=r,g=r,o=" }: + replaceSecrets = + { + userConfig, + resultPath, + generator, + user ? null, + permissions ? "u=r,g=r,o=", + }: let configWithTemplates = withReplacements userConfig; @@ -17,28 +24,47 @@ rec { replacements = getReplacements userConfig; in - replaceSecretsScript { - file = nonSecretConfigFile; - inherit resultPath replacements; - inherit user permissions; - }; + replaceSecretsScript { + file = nonSecretConfigFile; + inherit resultPath replacements; + inherit user permissions; + }; replaceSecretsFormatAdapter = format: format.generate; - replaceSecretsGeneratorAdapter = generator: name: value: pkgs.writeText "generator " (generator value); - toEnvVar = replaceSecretsGeneratorAdapter (v: (lib.generators.toINIWithGlobalSection {} { globalSection = v; })); + replaceSecretsGeneratorAdapter = + generator: name: value: + pkgs.writeText "generator " (generator value); + toEnvVar = replaceSecretsGeneratorAdapter ( + v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; }) + ); - template = file: newPath: replacements: replaceSecretsScript { - inherit file replacements; - resultPath = newPath; - }; + template = + file: newPath: replacements: + replaceSecretsScript { + inherit file replacements; + resultPath = newPath; + }; - genReplacement = secret: + genReplacement = + secret: let - t = { transform ? null, ... }: if isNull transform then x: x else transform; + t = + { + transform ? null, + ... + }: + if isNull transform then x: x else transform; in - lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})"); + lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})"); - replaceSecretsScript = { file, resultPath, replacements, user ? null, permissions ? "u=r,g=r,o=" }: + replaceSecretsScript = + { + file, + resultPath, + replacements, + user ? null, + permissions ? "u=r,g=r,o=", + }: let templatePath = resultPath + ".template"; @@ -47,15 +73,17 @@ rec { # step. Otherwise, the $(cat ...) commands inside the sed # replacements could fail but not fail individually but # not fail the whole script. - checkPermissions = concatMapStringsSep "\n" (pattern: "cat ${pattern.source} > /dev/null") replacements; + checkPermissions = concatMapStringsSep "\n" ( + pattern: "cat ${pattern.source} > /dev/null" + ) replacements; - sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (map genReplacement replacements); + sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") ( + map genReplacement replacements + ); - sedCmd = if replacements == [] - then "cat" - else "${pkgs.gnused}/bin/sed ${sedPatterns}"; + sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}"; in - '' + '' set -euo pipefail ${checkPermissions} @@ -64,12 +92,14 @@ rec { ln -fs ${file} ${templatePath} rm -f ${resultPath} touch ${resultPath} - '' + (lib.optionalString (user != null) '' + '' + + (lib.optionalString (user != null) '' chown ${user} ${resultPath} - '') + '' + '') + + '' ${sedCmd} ${templatePath} > ${resultPath} chmod ${permissions} ${resultPath} - ''; + ''; secretFileType = lib.types.submodule { options = { @@ -83,35 +113,34 @@ rec { description = "An optional function to transform the secret."; default = null; example = lib.literalExpression '' - v: "prefix-$${v}-suffix" + v: "prefix-$${v}-suffix" ''; }; }; }; - secretName = names: - "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%"; + secretName = + names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%"; - withReplacements = attrs: + withReplacements = + attrs: let - valueOrReplacement = name: value: - if !(builtins.isAttrs value && value ? "source") - then value - else secretName name; + valueOrReplacement = + name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name; in - mapAttrsRecursiveCond (v: ! v ? "source") valueOrReplacement attrs; + mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs; - getReplacements = attrs: + getReplacements = + attrs: let - addNameField = name: value: - if !(builtins.isAttrs value && value ? "source") - then value - else value // { name = name; }; + addNameField = + name: value: + if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; }; - secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs; + secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs; in - collect (v: builtins.isAttrs v && v ? "source") secretsWithName; - + collect (v: builtins.isAttrs v && v ? "source") secretsWithName; + # Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists. mapAttrsRecursiveCond = # A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set. @@ -121,20 +150,23 @@ rec { # Attribute set or list to recursively map over. set: let - recurse = path: val: - if builtins.isAttrs val && cond val - then lib.attrsets.mapAttrs (n: v: recurse (path ++ [n]) v) val - else if builtins.isList val && cond val - then lib.lists.imap0 (i: v: recurse (path ++ [(builtins.toString i)]) v) val - else f path val; - in recurse [] set; + recurse = + path: val: + if builtins.isAttrs val && cond val then + lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val + else if builtins.isList val && cond val then + lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val + else + f path val; + in + recurse [ ] set; # Like lib.attrsets.collect but also recurses on lists. collect = - # Given an attribute's value, determine if recursion should stop. - pred: - # The attribute set to recursively collect. - attrs: + # Given an attribute's value, determine if recursion should stop. + pred: + # The attribute set to recursively collect. + attrs: if pred attrs then [ attrs ] else if builtins.isAttrs attrs then @@ -142,111 +174,158 @@ rec { else if builtins.isList attrs then lib.lists.concatMap (collect pred) attrs else - []; + [ ]; - indent = i: str: lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str); + indent = + i: str: + lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str); # Generator for XML - formatXML = { - enclosingRoot ? null - }: { - type = with lib.types; let - valueType = nullOr (oneOf [ - bool - int - float - str - path - (attrsOf valueType) - (listOf valueType) - ]) // { - description = "XML value"; - }; - in valueType; + formatXML = + { + enclosingRoot ? null, + }: + { + type = + with lib.types; + let + valueType = + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) + // { + description = "XML value"; + }; + in + valueType; - generate = name: value: pkgs.callPackage ({ runCommand, python3 }: runCommand "config" { - value = builtins.toJSON ( - if enclosingRoot == null then - value - else - { ${enclosingRoot} = value; }); - passAsFile = [ "value" ]; - } (pkgs.writers.writePython3 "dict2xml" { - libraries = with python3.pkgs; [ python dict2xml ]; - } '' - import os - import json - from dict2xml import dict2xml + generate = + name: value: + pkgs.callPackage ( + { runCommand, python3 }: + runCommand "config" + { + value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; }); + passAsFile = [ "value" ]; + } + ( + pkgs.writers.writePython3 "dict2xml" + { + libraries = with python3.pkgs; [ + python + dict2xml + ]; + } + '' + import os + import json + from dict2xml import dict2xml - with open(os.environ["valuePath"]) as f: - content = json.loads(f.read()) - if content is None: - print("Could not parse env var valuePath as json") - os.exit(2) - with open(os.environ["out"], "w") as out: - out.write(dict2xml(content)) - '')) {}; + with open(os.environ["valuePath"]) as f: + content = json.loads(f.read()) + if content is None: + print("Could not parse env var valuePath as json") + os.exit(2) + with open(os.environ["out"], "w") as out: + out.write(dict2xml(content)) + '' + ) + ) { }; - }; + }; - parseXML = xml: + parseXML = + xml: let - xmlToJsonFile = pkgs.callPackage ({ runCommand, python3 }: runCommand "config" { - inherit xml; - passAsFile = [ "xml" ]; - } (pkgs.writers.writePython3 "xml2json" { - libraries = with python3.pkgs; [ python ]; - } '' - import os - import json - from collections import ChainMap - from xml.etree import ElementTree - - - def xml_to_dict_recursive(root): - all_descendants = list(root) - if len(all_descendants) == 0: - return {root.tag: root.text} - else: - merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants)) - return {root.tag: dict(merged_dict)} + xmlToJsonFile = pkgs.callPackage ( + { runCommand, python3 }: + runCommand "config" + { + inherit xml; + passAsFile = [ "xml" ]; + } + ( + pkgs.writers.writePython3 "xml2json" + { + libraries = with python3.pkgs; [ python ]; + } + '' + import os + import json + from collections import ChainMap + from xml.etree import ElementTree - with open(os.environ["xmlPath"]) as f: - root = ElementTree.XML(f.read()) - xml = xml_to_dict_recursive(root) - j = json.dumps(xml) + def xml_to_dict_recursive(root): + all_descendants = list(root) + if len(all_descendants) == 0: + return {root.tag: root.text} + else: + merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants)) + return {root.tag: dict(merged_dict)} - with open(os.environ["out"], "w") as out: - out.write(j) - '')) {}; + + with open(os.environ["xmlPath"]) as f: + root = ElementTree.XML(f.read()) + xml = xml_to_dict_recursive(root) + j = json.dumps(xml) + + with open(os.environ["out"], "w") as out: + out.write(j) + '' + ) + ) { }; in - builtins.fromJSON (builtins.readFile xmlToJsonFile); + builtins.fromJSON (builtins.readFile xmlToJsonFile); - renameAttrName = attrset: from: to: - (lib.attrsets.filterAttrs (name: v: name == from) attrset) // { + renameAttrName = + attrset: from: to: + (lib.attrsets.filterAttrs (name: v: name == from) attrset) + // { ${to} = attrset.${from}; }; # Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix # with a nicer diff display function. - check = { pkgs, tests }: + check = + { pkgs, tests }: let - formatValue = val: - if (builtins.isList val || builtins.isAttrs val) then builtins.toJSON val - else builtins.toString val; + formatValue = + val: + if (builtins.isList val || builtins.isAttrs val) then + builtins.toJSON val + else + builtins.toString val; - resultToString = { name, expected, result }: - builtins.readFile (pkgs.runCommand "nix-flake-tests-error" { - expected = formatValue expected; - result = formatValue result; - passAsFile = [ "expected" "result" ]; - } '' - echo "${name} failed (- expected, + result)" > $out - cp ''${expectedPath} ''${expectedPath}.json - cp ''${resultPath} ''${resultPath}.json - ${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out - ''); + resultToString = + { + name, + expected, + result, + }: + builtins.readFile ( + pkgs.runCommand "nix-flake-tests-error" + { + expected = formatValue expected; + result = formatValue result; + passAsFile = [ + "expected" + "result" + ]; + } + '' + echo "${name} failed (- expected, + result)" > $out + cp ''${expectedPath} ''${expectedPath}.json + cp ''${resultPath} ''${resultPath}.json + ${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out + '' + ); results = pkgs.lib.runTests tests; in @@ -255,8 +334,14 @@ rec { else pkgs.runCommand "nix-flake-tests-success" { } "echo > $out"; - - genConfigOutOfBandSystemd = { config, configLocation, generator, user ? null, permissions ? "u=r,g=r,o=" }: + genConfigOutOfBandSystemd = + { + config, + configLocation, + generator, + user ? null, + permissions ? "u=r,g=r,o=", + }: { loadCredentials = getLoadCredentials "source" config; preStart = lib.mkBefore (replaceSecrets { @@ -267,34 +352,35 @@ rec { }); }; - updateToLoadCredentials = sourceField: rootDir: attrs: + updateToLoadCredentials = + sourceField: rootDir: attrs: let hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; - valueOrLoadCredential = path: value: - if ! (hasPlaceholderField value) - then value - else value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; }; + valueOrLoadCredential = + path: value: + if !(hasPlaceholderField value) then + value + else + value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; }; in - mapAttrsRecursiveCond (v: ! (hasPlaceholderField v)) valueOrLoadCredential attrs; + mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs; - getLoadCredentials = sourceField: attrs: + getLoadCredentials = + sourceField: attrs: let hasPlaceholderField = v: isAttrs v && hasAttr sourceField v; - addPathField = path: value: - if ! (hasPlaceholderField value) - then value - else value // { inherit path; }; + addPathField = + path: value: if !(hasPlaceholderField value) then value else value // { inherit path; }; - secretsWithPath = mapAttrsRecursiveCond (v: ! (hasPlaceholderField v)) addPathField attrs; + secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs; allSecrets = collect (v: hasPlaceholderField v) secretsWithPath; - genLoadCredentials = secret: - "${concatStringsSep "_" secret.path}:${secret.${sourceField}}"; + genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}"; in - map genLoadCredentials allSecrets; + map genLoadCredentials allSecrets; anyNotNull = any (x: x != null); } diff --git a/modules/blocks/authelia.nix b/modules/blocks/authelia.nix index daf5f98..6b3775a 100644 --- a/modules/blocks/authelia.nix +++ b/modules/blocks/authelia.nix @@ -1,10 +1,16 @@ -{ config, options, pkgs, lib, ... }: +{ + config, + options, + pkgs, + lib, + ... +}: let cfg = config.shb.authelia; opt = options.shb.authelia; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}"; @@ -148,25 +154,25 @@ in extraOidcClaimsPolicies = lib.mkOption { description = "Extra OIDC claims policies."; type = lib.types.attrsOf lib.types.attrs; - default = {}; + default = { }; }; extraOidcScopes = lib.mkOption { description = "Extra OIDC scopes."; type = lib.types.attrsOf lib.types.attrs; - default = {}; + default = { }; }; extraOidcAuthorizationPolicies = lib.mkOption { description = "Extra OIDC authorization policies."; type = lib.types.attrsOf lib.types.attrs; - default = {}; + default = { }; }; extraDefinitions = lib.mkOption { description = "Extra definitions."; type = lib.types.attrsOf lib.types.attrs; - default = {}; + default = { }; }; oidcClients = lib.mkOption { @@ -178,79 +184,92 @@ in client_secret.source = pkgs.writeText "dummy.secret" "dummy_client_secret"; public = false; authorization_policy = "one_factor"; - redirect_uris = []; + redirect_uris = [ ]; } ]; - type = lib.types.listOf (lib.types.submodule { - freeformType = lib.types.attrsOf lib.types.anything; + type = lib.types.listOf ( + lib.types.submodule { + freeformType = lib.types.attrsOf lib.types.anything; - options = { - client_id = lib.mkOption { - type = lib.types.str; - description = "Unique identifier of the OIDC client."; + options = { + client_id = lib.mkOption { + type = lib.types.str; + description = "Unique identifier of the OIDC client."; + }; + + client_name = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "Human readable description of the OIDC client."; + default = null; + }; + + client_secret = lib.mkOption { + type = lib.shb.secretFileType; + description = '' + File containing the shared secret with the OIDC client. + + Generate with: + + ``` + nix run nixpkgs#authelia -- \ + crypto hash generate pbkdf2 \ + --variant sha512 \ + --random \ + --random.length 72 \ + --random.charset rfc3986 + ``` + ''; + }; + + public = lib.mkOption { + type = lib.types.bool; + description = "If the OIDC client is public or not."; + default = false; + apply = v: if v then "true" else "false"; + }; + + authorization_policy = lib.mkOption { + type = lib.types.enum ( + [ + "one_factor" + "two_factor" + ] + ++ lib.attrNames cfg.extraOidcAuthorizationPolicies + ); + description = "Require one factor (password) or two factor (device) authentication."; + default = "one_factor"; + }; + + redirect_uris = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "List of uris that are allowed to be redirected to."; + }; + + scopes = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims"; + example = [ + "openid" + "profile" + "email" + "groups" + ]; + default = [ ]; + }; + + claims_policy = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = '' + Claim policy. + + Defaults to 'default' to provide a backwards compatible experience. + Read [this document](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter) for more information. + ''; + default = "default"; + }; }; - - client_name = lib.mkOption { - type = lib.types.nullOr lib.types.str; - description = "Human readable description of the OIDC client."; - default = null; - }; - - client_secret = lib.mkOption { - type = lib.shb.secretFileType; - description = '' - File containing the shared secret with the OIDC client. - - Generate with: - - ``` - nix run nixpkgs#authelia -- \ - crypto hash generate pbkdf2 \ - --variant sha512 \ - --random \ - --random.length 72 \ - --random.charset rfc3986 - ``` - ''; - }; - - public = lib.mkOption { - type = lib.types.bool; - description = "If the OIDC client is public or not."; - default = false; - apply = v: if v then "true" else "false"; - }; - - authorization_policy = lib.mkOption { - type = lib.types.enum ([ "one_factor" "two_factor" ] ++ lib.attrNames cfg.extraOidcAuthorizationPolicies); - description = "Require one factor (password) or two factor (device) authentication."; - default = "one_factor"; - }; - - redirect_uris = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = "List of uris that are allowed to be redirected to."; - }; - - scopes = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims"; - example = [ "openid" "profile" "email" "groups" ]; - default = []; - }; - - claims_policy = lib.mkOption { - type = lib.types.nullOr lib.types.str; - description = '' - Claim policy. - - Defaults to 'default' to provide a backwards compatible experience. - Read [this document](https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter) for more information. - ''; - default = "default"; - }; - }; - }); + } + ); }; smtp = lib.mkOption { @@ -263,50 +282,52 @@ in default = "/tmp/authelia-notifications"; type = lib.types.oneOf [ lib.types.str - (lib.types.nullOr (lib.types.submodule { - options = { - from_address = lib.mkOption { - type = lib.types.str; - description = "SMTP address from which the emails originate."; - example = "authelia@mydomain.com"; - }; - from_name = lib.mkOption { - type = lib.types.str; - description = "SMTP name from which the emails originate."; - default = "Authelia"; - }; - host = lib.mkOption { - type = lib.types.str; - description = "SMTP host to send the emails to."; - }; - port = lib.mkOption { - type = lib.types.port; - description = "SMTP port to send the emails to."; - default = 25; - }; - username = lib.mkOption { - type = lib.types.str; - description = "Username to connect to the SMTP host."; - }; - password = lib.mkOption { - description = "File containing the password to connect to the SMTP host."; - type = lib.types.submodule { - options = contracts.secret.mkRequester { - mode = "0400"; - owner = cfg.autheliaUser; - restartUnits = [ "authelia-${fqdn}" ]; + (lib.types.nullOr ( + lib.types.submodule { + options = { + from_address = lib.mkOption { + type = lib.types.str; + description = "SMTP address from which the emails originate."; + example = "authelia@mydomain.com"; + }; + from_name = lib.mkOption { + type = lib.types.str; + description = "SMTP name from which the emails originate."; + default = "Authelia"; + }; + host = lib.mkOption { + type = lib.types.str; + description = "SMTP host to send the emails to."; + }; + port = lib.mkOption { + type = lib.types.port; + description = "SMTP port to send the emails to."; + default = 25; + }; + username = lib.mkOption { + type = lib.types.str; + description = "Username to connect to the SMTP host."; + }; + password = lib.mkOption { + description = "File containing the password to connect to the SMTP host."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = cfg.autheliaUser; + restartUnits = [ "authelia-${fqdn}" ]; + }; }; }; }; - }; - })) + } + )) ]; }; rules = lib.mkOption { type = lib.types.listOf lib.types.anything; description = "Rule based clients"; - default = []; + default = [ ]; }; mount = lib.mkOption { @@ -324,8 +345,12 @@ in ``` ''; readOnly = true; - default = { path = "/var/lib/authelia-authelia.${cfg.domain}"; }; - defaultText = { path = "/var/lib/authelia-authelia.example.com"; }; + default = { + path = "/var/lib/authelia-authelia.${cfg.domain}"; + }; + defaultText = { + path = "/var/lib/authelia-authelia.example.com"; + }; }; mountRedis = lib.mkOption { @@ -343,7 +368,9 @@ in ``` ''; readOnly = true; - default = { path = "/var/lib/redis-authelia"; }; + default = { + path = "/var/lib/redis-authelia"; + }; }; debug = lib.mkOption { @@ -370,7 +397,7 @@ in # Overriding the user name so we don't allow any weird characters anywhere. For example, postgres users do not accept the '.'. users = { - groups.${autheliaCfg.user} = {}; + groups.${autheliaCfg.user} = { }; users.${autheliaCfg.user} = { isSystemUser = true; group = autheliaCfg.user; @@ -395,7 +422,9 @@ in AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path; AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path; - AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (toString cfg.smtp.password.result.path); + AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) ( + toString cfg.smtp.password.result.path + ); }; settings = { server.address = "tcp://127.0.0.1:${toString listenPort}"; @@ -428,10 +457,12 @@ in # Inspired from https://www.authelia.com/configuration/session/introduction/ and https://www.authelia.com/configuration/session/redis session = { name = "authelia_session"; - cookies = [{ - domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}"; - authelia_url = "https://${cfg.subdomain}.${cfg.domain}"; - }]; + cookies = [ + { + domain = if isNull cfg.port then cfg.domain else "${cfg.domain}:${toString cfg.port}"; + authelia_url = "https://${cfg.subdomain}.${cfg.domain}"; + } + ]; same_site = "lax"; expiration = "1h"; inactivity = "5m"; @@ -468,7 +499,11 @@ in networks = [ { name = "internal"; - networks = [ "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/18" ]; + networks = [ + "10.0.0.0/8" + "172.16.0.0/12" + "192.168.0.0/18" + ]; } ]; rules = [ @@ -479,7 +514,8 @@ in "^/api/.*" ]; } - ] ++ cfg.rules; + ] + ++ cfg.rules; }; telemetry = { metrics = { @@ -489,17 +525,25 @@ in }; log.level = if cfg.debug then "debug" else "info"; - } // { + } + // { identity_providers.oidc = { claims_policies = { # This default claim should go away at some point. # https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter - default.id_token = [ "email" "preferred_username" "name" "groups" ]; - } // cfg.extraOidcClaimsPolicies; + default.id_token = [ + "email" + "preferred_username" + "name" + "groups" + ]; + } + // cfg.extraOidcClaimsPolicies; scopes = cfg.extraOidcScopes; authorization_policies = cfg.extraOidcAuthorizationPolicies; }; - } // lib.optionalAttrs (cfg.extraDefinitions != {}) { + } + // lib.optionalAttrs (cfg.extraDefinitions != { }) { definitions = cfg.extraDefinitions; }; @@ -508,18 +552,22 @@ in systemd.services."authelia-${fqdn}".preStart = let - mkCfg = clients: + mkCfg = + clients: lib.shb.replaceSecrets { userConfig = { identity_providers.oidc.clients = clients; }; resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml"; - generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML {}); + generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { }); }; in - lib.mkBefore (mkCfg cfg.oidcClients + '' - ${pkgs.bash}/bin/bash -c '(while ! ${pkgs.netcat-openbsd}/bin/nc -z -v -w1 ${cfg.ldapHostname} ${toString cfg.ldapPort}; do echo "Waiting for port ${cfg.ldapHostname}:${toString cfg.ldapPort} to open..."; sleep 2; done); sleep 2' - ''); + lib.mkBefore ( + mkCfg cfg.oidcClients + + '' + ${pkgs.bash}/bin/bash -c '(while ! ${pkgs.netcat-openbsd}/bin/nc -z -v -w1 ${cfg.ldapHostname} ${toString cfg.ldapPort}; do echo "Waiting for port ${cfg.ldapHostname}:${toString cfg.ldapPort} to open..."; sleep 2; done); sleep 2' + '' + ); services.nginx.virtualHosts.${fqdn} = { forceSSL = !(isNull cfg.ssl); @@ -554,7 +602,7 @@ in error_page 403 = /error/403; error_page 404 = /error/404; } - ''; + ''; locations."/api/verify".extraConfig = '' add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; @@ -567,7 +615,7 @@ in proxy_set_header Host $http_x_forwarded_host; proxy_pass http://127.0.0.1:9091; - ''; + ''; }; # I would like this to live outside of the Authelia module. @@ -579,7 +627,8 @@ in after = [ "authelia-${fqdn}.service" ]; enabledAddons = [ config.shb.mitmdump.addons.logger ]; extraArgs = [ - "--set" "verbose_pattern=/api" + "--set" + "verbose_pattern=/api" ]; }; @@ -600,7 +649,7 @@ in job_name = "authelia"; static_configs = [ { - targets = ["127.0.0.1:9959"]; + targets = [ "127.0.0.1:9959" ]; labels = { "hostname" = config.networking.hostName; "domain" = cfg.domain; @@ -610,17 +659,20 @@ in } ]; - systemd.targets."authelia-${fqdn}" = let - services = [ - "authelia-${fqdn}.service" - ] ++ lib.optionals cfg.debug [ - config.shb.mitmdump.instances."authelia-${fqdn}".serviceName - ]; - in { - after = services; - requires = services; + systemd.targets."authelia-${fqdn}" = + let + services = [ + "authelia-${fqdn}.service" + ] + ++ lib.optionals cfg.debug [ + config.shb.mitmdump.instances."authelia-${fqdn}".serviceName + ]; + in + { + after = services; + requires = services; - wantedBy = [ "multi-user.target" ]; - }; + wantedBy = [ "multi-user.target" ]; + }; }; } diff --git a/modules/blocks/borgbackup.nix b/modules/blocks/borgbackup.nix index 331bab6..d1a4101 100644 --- a/modules/blocks/borgbackup.nix +++ b/modules/blocks/borgbackup.nix @@ -1,4 +1,10 @@ -{ config, pkgs, lib, utils, ... }: +{ + config, + pkgs, + lib, + utils, + ... +}: let cfg = config.shb.borgbackup; @@ -17,7 +23,9 @@ let type = lib.types.path; }; - encryption_passcommand = "cat /run/secrets/borgmatic/passphrases/${if isNull instance.secretName then name else instance.secretName}"; + encryption_passcommand = "cat /run/secrets/borgmatic/passphrases/${ + if isNull instance.secretName then name else instance.secretName + }"; borg_keys_directory = "/run/secrets/borgmatic/keys"; sourceDirectories = lib.mkOption { @@ -28,7 +36,7 @@ let excludePatterns = lib.mkOption { description = "Exclude patterns."; type = lib.types.listOf lib.types.str; - default = []; + default = [ ]; }; secretName = lib.mkOption { @@ -39,33 +47,40 @@ let repositories = lib.mkOption { description = "Repositories to back this instance to."; - type = lib.types.nonEmptyListOf (lib.types.submodule { - options = { - path = lib.mkOption { - type = lib.types.str; - description = "Repository location"; - }; + type = lib.types.nonEmptyListOf ( + lib.types.submodule { + options = { + path = lib.mkOption { + type = lib.types.str; + description = "Repository location"; + }; - timerConfig = lib.mkOption { - type = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption; - default = { - OnCalendar = "daily"; - Persistent = true; - }; - description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.''; - example = { - OnCalendar = "00:05"; - RandomizedDelaySec = "5h"; - Persistent = true; + timerConfig = lib.mkOption { + type = lib.types.attrsOf utils.systemdUtils.unitOptions.unitOption; + default = { + OnCalendar = "daily"; + Persistent = true; + }; + description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.''; + example = { + OnCalendar = "00:05"; + RandomizedDelaySec = "5h"; + Persistent = true; + }; }; }; - }; - }); + } + ); }; retention = lib.mkOption { description = "Retention options."; - type = lib.types.attrsOf (lib.types.oneOf [ lib.types.int lib.types.nonEmptyStr ]); + type = lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.int + lib.types.nonEmptyStr + ] + ); default = { keep_within = "1d"; keep_hourly = 24; @@ -78,7 +93,7 @@ let consistency = lib.mkOption { description = "Consistency frequency options."; type = lib.types.attrsOf lib.types.nonEmptyStr; - default = {}; + default = { }; example = { repository = "2 weeks"; archives = "1 month"; @@ -87,19 +102,19 @@ let hooks = lib.mkOption { description = "Hooks to run before or after the backup."; - default = {}; + default = { }; type = lib.types.submodule { options = { beforeBackup = lib.mkOption { description = "Hooks to run before backup"; type = lib.types.listOf lib.types.str; - default = []; + default = [ ]; }; afterBackup = lib.mkOption { description = "Hooks to run after backup"; type = lib.types.listOf lib.types.str; - default = []; + default = [ ]; }; }; }; @@ -113,7 +128,8 @@ let }; }; - repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (lib.strings.removePrefix "/" name); + repoSlugName = + name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (lib.strings.removePrefix "/" name); in { @@ -132,10 +148,12 @@ in instances = lib.mkOption { description = "Each instance is a backup setting"; - default = {}; - type = lib.types.attrsOf (lib.types.submodule { - options = instanceOptions; - }); + default = { }; + type = lib.types.attrsOf ( + lib.types.submodule { + options = instanceOptions; + } + ); }; borgServer = lib.mkOption { @@ -148,7 +166,7 @@ in # Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23 performance = lib.mkOption { description = "Reduce performance impact of backup jobs."; - default = {}; + default = { }; type = lib.types.submodule { options = { niceness = lib.mkOption { @@ -157,7 +175,11 @@ in default = 15; }; ioSchedulingClass = lib.mkOption { - type = lib.types.enum [ "idle" "best-effort" "realtime" ]; + type = lib.types.enum [ + "idle" + "best-effort" + "realtime" + ]; description = "ionice scheduling class, defaults to best-effort IO."; default = "best-effort"; }; @@ -171,10 +193,11 @@ in }; }; - config = lib.mkIf (cfg.instances != {}) ( + config = lib.mkIf (cfg.instances != { }) ( let enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances; - in lib.mkMerge [ + in + lib.mkMerge [ # Secrets configuration { users.users = { @@ -195,74 +218,101 @@ in sops.secrets = let - mkSopsSecret = name: instance: ( - [ - { - "${instance.backend}/passphrases/${if isNull instance.secretName then name else instance.secretName}" = { - sopsFile = instance.keySopsFile; - mode = "0440"; - owner = cfg.user; - group = cfg.group; - }; - } - ] ++ lib.optional ((lib.filter ({path, ...}: lib.strings.hasPrefix "s3" path) instance.repositories) != []) { - "${instance.backend}/environmentfiles/${if isNull instance.secretName then name else instance.secretName}" = { - sopsFile = instance.keySopsFile; - mode = "0440"; - owner = cfg.user; - group = cfg.group; - }; - } ++ lib.optionals (instance.backend == "borgmatic") (lib.flatten (map ({path, ...}: { - "${instance.backend}/keys/${repoSlugName path}" = { - key = "${instance.backend}/keys/${if isNull instance.secretName then name else instance.secretName}"; - sopsFile = instance.keySopsFile; - mode = "0440"; - owner = cfg.user; - group = cfg.group; - }; - }) instance.repositories)) - ); + mkSopsSecret = + name: instance: + ( + [ + { + "${instance.backend}/passphrases/${ + if isNull instance.secretName then name else instance.secretName + }" = + { + sopsFile = instance.keySopsFile; + mode = "0440"; + owner = cfg.user; + group = cfg.group; + }; + } + ] + ++ + lib.optional + ((lib.filter ({ path, ... }: lib.strings.hasPrefix "s3" path) instance.repositories) != [ ]) + { + "${instance.backend}/environmentfiles/${ + if isNull instance.secretName then name else instance.secretName + }" = + { + sopsFile = instance.keySopsFile; + mode = "0440"; + owner = cfg.user; + group = cfg.group; + }; + } + ++ lib.optionals (instance.backend == "borgmatic") ( + lib.flatten ( + map ( + { path, ... }: + { + "${instance.backend}/keys/${repoSlugName path}" = { + key = "${instance.backend}/keys/${ + if isNull instance.secretName then name else instance.secretName + }"; + sopsFile = instance.keySopsFile; + mode = "0440"; + owner = cfg.user; + group = cfg.group; + }; + } + ) instance.repositories + ) + ) + ); in - lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances)); + lib.mkMerge (lib.flatten (lib.attrsets.mapAttrsToList mkSopsSecret enabledInstances)); } # Borgmatic configuration { - systemd.timers.borgmatic = lib.mkIf (enabledInstances != {}) { + systemd.timers.borgmatic = lib.mkIf (enabledInstances != { }) { timerConfig = { OnCalendar = "hourly"; }; }; - systemd.services.borgmatic = lib.mkIf (enabledInstances != {}) { + systemd.services.borgmatic = lib.mkIf (enabledInstances != { }) { serviceConfig = { User = cfg.user; Group = cfg.group; ExecStartPre = [ "" ]; # Do not sleep before starting. - ExecStart = [ "" "${pkgs.borgmatic}/bin/borgmatic --verbosity -1 --syslog-verbosity 1" ]; + ExecStart = [ + "" + "${pkgs.borgmatic}/bin/borgmatic --verbosity -1 --syslog-verbosity 1" + ]; # For borgmatic, since we have only one service, we need to merge all environmentFile # from all instances. EnvironmentFile = lib.mapAttrsToList (name: value: value.environmentFile) enabledInstances; }; }; - systemd.packages = lib.mkIf (enabledInstances != {}) [ pkgs.borgmatic ]; + systemd.packages = lib.mkIf (enabledInstances != { }) [ pkgs.borgmatic ]; environment.systemPackages = ( lib.optionals cfg.borgServer [ pkgs.borgbackup ] - ++ lib.optionals (enabledInstances != {}) [ pkgs.borgbackup pkgs.borgmatic ] + ++ lib.optionals (enabledInstances != { }) [ + pkgs.borgbackup + pkgs.borgmatic + ] ); environment.etc = let mkSettings = name: instance: { - "borgmatic.d/${name}.yaml".text = lib.generators.toYAML {} { - location = - { - source_directories = instance.sourceDirectories; - repositories = map ({path, ...}: path) instance.repositories; - } - // (lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) { - excludePatterns = instance.excludePatterns; - }); + "borgmatic.d/${name}.yaml".text = lib.generators.toYAML { } { + location = { + source_directories = instance.sourceDirectories; + repositories = map ({ path, ... }: path) instance.repositories; + } + // (lib.attrsets.optionalAttrs (builtins.length instance.excludePatterns > 0) { + excludePatterns = instance.excludePatterns; + }); storage = { encryption_passcommand = "cat ${instance.encryptionKeyFile}"; @@ -276,7 +326,7 @@ in inherit name frequency; }; in - lib.attrsets.mapAttrsToList mkCheck instance.consistency; + lib.attrsets.mapAttrsToList mkCheck instance.consistency; # hooks = lib.mkMerge [ # lib.optionalAttrs (builtins.length instance.hooks.beforeBackup > 0) { @@ -289,7 +339,8 @@ in }; }; in - lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings enabledInstances); + lib.mkMerge (lib.attrsets.mapAttrsToList mkSettings enabledInstances); } - ]); + ] + ); } diff --git a/modules/blocks/davfs.nix b/modules/blocks/davfs.nix index 39fcfcb..046a92c 100644 --- a/modules/blocks/davfs.nix +++ b/modules/blocks/davfs.nix @@ -7,66 +7,68 @@ in options.shb.davfs = { mounts = lib.mkOption { description = "List of mounts."; - default = []; - type = lib.types.listOf (lib.types.submodule { - options = { - remoteUrl = lib.mkOption { - type = lib.types.str; - description = "Webdav endpoint to connect to."; - example = "https://my.domain.com/dav"; - }; + default = [ ]; + type = lib.types.listOf ( + lib.types.submodule { + options = { + remoteUrl = lib.mkOption { + type = lib.types.str; + description = "Webdav endpoint to connect to."; + example = "https://my.domain.com/dav"; + }; - mountPoint = lib.mkOption { - type = lib.types.str; - description = "Mount point to mount the webdav endpoint on."; - example = "/mnt"; - }; + mountPoint = lib.mkOption { + type = lib.types.str; + description = "Mount point to mount the webdav endpoint on."; + example = "/mnt"; + }; - username = lib.mkOption { - type = lib.types.str; - description = "Username to connect to the webdav endpoint."; - }; + username = lib.mkOption { + type = lib.types.str; + description = "Username to connect to the webdav endpoint."; + }; - passwordFile = lib.mkOption { - type = lib.types.str; - description = "Password to connect to the webdav endpoint."; - }; + passwordFile = lib.mkOption { + type = lib.types.str; + description = "Password to connect to the webdav endpoint."; + }; - uid = lib.mkOption { - type = lib.types.nullOr lib.types.int; - description = "User owner of the mount point."; - example = 1000; - default = null; - }; + uid = lib.mkOption { + type = lib.types.nullOr lib.types.int; + description = "User owner of the mount point."; + example = 1000; + default = null; + }; - gid = lib.mkOption { - type = lib.types.nullOr lib.types.int; - description = "Group owner of the mount point."; - example = 1000; - default = null; - }; + gid = lib.mkOption { + type = lib.types.nullOr lib.types.int; + description = "Group owner of the mount point."; + example = 1000; + default = null; + }; - fileMode = lib.mkOption { - type = lib.types.nullOr lib.types.str; - description = "File creation mode"; - example = "0664"; - default = null; - }; + fileMode = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "File creation mode"; + example = "0664"; + default = null; + }; - directoryMode = lib.mkOption { - type = lib.types.nullOr lib.types.str; - description = "Directory creation mode"; - example = "2775"; - default = null; - }; + directoryMode = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "Directory creation mode"; + example = "2775"; + default = null; + }; - automount = lib.mkOption { - type = lib.types.bool; - description = "Create a systemd automount unit"; - default = true; + automount = lib.mkOption { + type = lib.types.bool; + description = "Create a systemd automount unit"; + default = true; + }; }; - }; - }); + } + ); }; }; @@ -93,6 +95,6 @@ in mountConfig.TimeoutSec = 15; }; in - map mkMountCfg cfg.mounts; + map mkMountCfg cfg.mounts; }; } diff --git a/modules/blocks/hardcodedsecret.nix b/modules/blocks/hardcodedsecret.nix index 2c0ee84..cfbe68a 100644 --- a/modules/blocks/hardcodedsecret.nix +++ b/modules/blocks/hardcodedsecret.nix @@ -1,84 +1,102 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.shb.hardcodedsecret; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; inherit (lib) mapAttrs' mkOption nameValuePair; - inherit (lib.types) attrsOf nullOr str submodule; + inherit (lib.types) + attrsOf + nullOr + str + submodule + ; inherit (pkgs) writeText; in { options.shb.hardcodedsecret = mkOption { - default = {}; + default = { }; description = '' Hardcoded secrets. These should only be used in tests. ''; example = lib.literalExpression '' - { - mySecret = { - request = { - user = "me"; - mode = "0400"; - restartUnits = [ "myservice.service" ]; + { + mySecret = { + request = { + user = "me"; + mode = "0400"; + restartUnits = [ "myservice.service" ]; + }; + settings.content = "My Secret"; }; - settings.content = "My Secret"; - }; - } + } ''; - type = attrsOf (submodule ({ name, ... }: { - options = contracts.secret.mkProvider { - settings = mkOption { - description = '' - Settings specific to the hardcoded secret module. + type = attrsOf ( + submodule ( + { name, ... }: + { + options = contracts.secret.mkProvider { + settings = mkOption { + description = '' + Settings specific to the hardcoded secret module. - Give either `content` or `source`. - ''; + Give either `content` or `source`. + ''; - type = submodule { - options = { - content = mkOption { - type = nullOr str; - description = '' - Content of the secret as a string. + type = submodule { + options = { + content = mkOption { + type = nullOr str; + description = '' + Content of the secret as a string. - This will be stored in the nix store and should only be used for testing or maybe in dev. - ''; - default = null; - }; + This will be stored in the nix store and should only be used for testing or maybe in dev. + ''; + default = null; + }; - source = mkOption { - type = nullOr str; - description = '' - Source of the content of the secret as a path in the nix store. - ''; - default = null; + source = mkOption { + type = nullOr str; + description = '' + Source of the content of the secret as a path in the nix store. + ''; + default = null; + }; + }; }; }; - }; - }; - resultCfg = { - path = "/run/hardcodedsecrets/hardcodedsecret_${name}"; - }; - }; - })); + resultCfg = { + path = "/run/hardcodedsecrets/hardcodedsecret_${name}"; + }; + }; + } + ) + ); }; config = { - system.activationScripts = mapAttrs' (n: cfg': + system.activationScripts = mapAttrs' ( + n: cfg': let - source = if cfg'.settings.source != null - then cfg'.settings.source - else writeText "hardcodedsecret_${n}_content" cfg'.settings.content; + source = + if cfg'.settings.source != null then + cfg'.settings.source + else + writeText "hardcodedsecret_${n}_content" cfg'.settings.content; in - nameValuePair "hardcodedsecret_${n}" '' - mkdir -p "$(dirname "${cfg'.result.path}")" - touch "${cfg'.result.path}" - chmod ${cfg'.request.mode} "${cfg'.result.path}" - chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}" - cp ${source} "${cfg'.result.path}" - '' + nameValuePair "hardcodedsecret_${n}" '' + mkdir -p "$(dirname "${cfg'.result.path}")" + touch "${cfg'.result.path}" + chmod ${cfg'.request.mode} "${cfg'.result.path}" + chown ${cfg'.request.owner}:${cfg'.request.group} "${cfg'.result.path}" + cp ${source} "${cfg'.result.path}" + '' ) cfg; }; } diff --git a/modules/blocks/lldap.nix b/modules/blocks/lldap.nix index 8fd9649..fffff9a 100644 --- a/modules/blocks/lldap.nix +++ b/modules/blocks/lldap.nix @@ -1,9 +1,14 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.lldap; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; @@ -145,7 +150,9 @@ in ``` ''; readOnly = true; - default = { path = "/var/lib/lldap"; }; + default = { + path = "/var/lib/lldap"; + }; }; backup = lib.mkOption { @@ -327,7 +334,7 @@ in default = true; }; }; - + config = lib.mkIf cfg.enable { services.nginx = { @@ -340,10 +347,16 @@ in locations."/" = { extraConfig = '' proxy_set_header Host $host; - '' + (if isNull cfg.restrictAccessIPRange then "" else '' - allow ${cfg.restrictAccessIPRange}; - deny all; - ''); + '' + + ( + if isNull cfg.restrictAccessIPRange then + "" + else + '' + allow ${cfg.restrictAccessIPRange}; + deny all; + '' + ); proxyPass = "http://${toString config.services.lldap.settings.http_host}:${toString config.shb.lldap.webUIListenPort}/"; }; }; @@ -354,7 +367,7 @@ in group = "lldap"; isSystemUser = true; }; - users.groups.lldap = {}; + users.groups.lldap = { }; services.lldap = { enable = true; @@ -382,9 +395,13 @@ in }; inherit (cfg) ensureGroups ensureUserFields ensureGroupFields; - ensureUsers = lib.mapAttrs (n: v: (lib.removeAttrs v [ "password" ]) // { - "password_file" = toString v.password.result.path; - }) cfg.ensureUsers; + ensureUsers = lib.mapAttrs ( + n: v: + (lib.removeAttrs v [ "password" ]) + // { + "password_file" = toString v.password.result.path; + } + ) cfg.ensureUsers; }; shb.mitmdump.instances."lldap-web" = lib.mkIf cfg.debug { @@ -393,7 +410,8 @@ in after = [ "lldap.service" ]; enabledAddons = [ config.shb.mitmdump.addons.logger ]; extraArgs = [ - "--set" "verbose_pattern=/api" + "--set" + "verbose_pattern=/api" ]; }; }; diff --git a/modules/blocks/mitmdump.nix b/modules/blocks/mitmdump.nix index fea9e6a..93c8bfe 100644 --- a/modules/blocks/mitmdump.nix +++ b/modules/blocks/mitmdump.nix @@ -1,143 +1,163 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let - inherit (lib) mapAttrs' mkOption nameValuePair types; - inherit (types) attrsOf listOf port submodule str; + inherit (lib) + mapAttrs' + mkOption + nameValuePair + types + ; + inherit (types) + attrsOf + listOf + port + submodule + str + ; cfg = config.shb.mitmdump; - mitmdumpScript = pkgs.writers.writePython3Bin "mitmdump" - { - libraries = let - p = pkgs.python3Packages; - in [ - p.systemd - p.mitmproxy - ]; - flakeIgnore = [ "E501" ]; - } - '' - from systemd.daemon import notify - import argparse - import logging - import os - import subprocess - import socket - import sys - import time - - - logging.basicConfig(level=logging.INFO, format='%(message)s') - - - def wait_for_port(host, port, timeout=10): - deadline = time.time() + timeout - while time.time() < deadline: - try: - with socket.create_connection((host, port), timeout=0.5): - return True - except Exception: - time.sleep(0.1) - return False - - - def flatten(xss): - return [x for xs in xss for x in xs] - - - parser = argparse.ArgumentParser() - parser.add_argument("--listen_host", default="127.0.0.1", help="Host mitmdump will listen on") - parser.add_argument("--listen_port", required=True, help="Port mitmdump will listen on") - parser.add_argument("--upstream_host", default="http://127.0.0.1", help="Host mitmdump will connect to for upstream. Example: http://127.0.0.1 or https://otherhost") - parser.add_argument("--upstream_port", required=True, help="Port mitmdump will connect to for upstream") - args, rest = parser.parse_known_args() - - MITMDUMP_BIN = os.environ.get("MITMDUMP_BIN") - if MITMDUMP_BIN is None: - raise Exception("MITMDUMP_BIN env var must be set to the path of the mitmdump binary") - - logging.info(f"Waiting for upstream address '{args.upstream_host}:{args.upstream_port}' to be up.") - wait_for_port(args.upstream_host, args.upstream_port, timeout=10) - logging.info(f"Upstream address '{args.upstream_host}:{args.upstream_port}' is up.") - - proc = subprocess.Popen( + mitmdumpScript = + pkgs.writers.writePython3Bin "mitmdump" + { + libraries = + let + p = pkgs.python3Packages; + in [ - MITMDUMP_BIN, - "--listen-host", args.listen_host, - "-p", args.listen_port, - "--mode", f"reverse:{args.upstream_host}:{args.upstream_port}", - ] + rest, - stdout=sys.stdout, - stderr=sys.stderr, - ) - - logging.info(f"Waiting for mitmdump instance to start on port {args.listen_port}.") - if wait_for_port("127.0.0.1", args.listen_port, timeout=10): - logging.info(f"Mitmdump is started on port {args.listen_port}.") - notify("READY=1") - else: - proc.terminate() - exit(1) - - proc.wait() - ''; - - logger = toString (pkgs.writers.writeText "loggerAddon.py" - '' - import logging - from collections.abc import Sequence - from mitmproxy import ctx, http - import re + p.systemd + p.mitmproxy + ]; + flakeIgnore = [ "E501" ]; + } + '' + from systemd.daemon import notify + import argparse + import logging + import os + import subprocess + import socket + import sys + import time - logger = logging.getLogger(__name__) + logging.basicConfig(level=logging.INFO, format='%(message)s') - class RegexLogger: - def __init__(self): - self.verbose_patterns = None - - def load(self, loader): - loader.add_option( - name="verbose_pattern", - typespec=Sequence[str], - default=[], - help="Regex patterns for verbose logging", - ) - - def response(self, flow: http.HTTPFlow): - if self.verbose_patterns is None: - self.verbose_patterns = [re.compile(p) for p in ctx.options.verbose_pattern] - - matched = any(p.search(flow.request.path) for p in self.verbose_patterns) - if matched: - logger.info(format_flow(flow)) + def wait_for_port(host, port, timeout=10): + deadline = time.time() + timeout + while time.time() < deadline: + try: + with socket.create_connection((host, port), timeout=0.5): + return True + except Exception: + time.sleep(0.1) + return False - def format_flow(flow: http.HTTPFlow) -> str: - return ( - "\n" - "RequestHeaders:\n" - f" {format_headers(flow.request.headers.items())}\n" - f"RequestBody: {flow.request.get_text()}\n" - f"Status: {flow.response.data.status_code}\n" - "ResponseHeaders:\n" - f" {format_headers(flow.response.headers.items())}\n" - f"ResponseBody: {flow.response.get_text()}\n" + def flatten(xss): + return [x for xs in xss for x in xs] + + + parser = argparse.ArgumentParser() + parser.add_argument("--listen_host", default="127.0.0.1", help="Host mitmdump will listen on") + parser.add_argument("--listen_port", required=True, help="Port mitmdump will listen on") + parser.add_argument("--upstream_host", default="http://127.0.0.1", help="Host mitmdump will connect to for upstream. Example: http://127.0.0.1 or https://otherhost") + parser.add_argument("--upstream_port", required=True, help="Port mitmdump will connect to for upstream") + args, rest = parser.parse_known_args() + + MITMDUMP_BIN = os.environ.get("MITMDUMP_BIN") + if MITMDUMP_BIN is None: + raise Exception("MITMDUMP_BIN env var must be set to the path of the mitmdump binary") + + logging.info(f"Waiting for upstream address '{args.upstream_host}:{args.upstream_port}' to be up.") + wait_for_port(args.upstream_host, args.upstream_port, timeout=10) + logging.info(f"Upstream address '{args.upstream_host}:{args.upstream_port}' is up.") + + proc = subprocess.Popen( + [ + MITMDUMP_BIN, + "--listen-host", args.listen_host, + "-p", args.listen_port, + "--mode", f"reverse:{args.upstream_host}:{args.upstream_port}", + ] + rest, + stdout=sys.stdout, + stderr=sys.stderr, ) + logging.info(f"Waiting for mitmdump instance to start on port {args.listen_port}.") + if wait_for_port("127.0.0.1", args.listen_port, timeout=10): + logging.info(f"Mitmdump is started on port {args.listen_port}.") + notify("READY=1") + else: + proc.terminate() + exit(1) - def format_headers(headers) -> str: - return "\n ".join(k + ": " + v for k, v in headers) + proc.wait() + ''; + + logger = toString ( + pkgs.writers.writeText "loggerAddon.py" '' + import logging + from collections.abc import Sequence + from mitmproxy import ctx, http + import re - addons = [RegexLogger()] - ''); + logger = logging.getLogger(__name__) + + + class RegexLogger: + def __init__(self): + self.verbose_patterns = None + + def load(self, loader): + loader.add_option( + name="verbose_pattern", + typespec=Sequence[str], + default=[], + help="Regex patterns for verbose logging", + ) + + def response(self, flow: http.HTTPFlow): + if self.verbose_patterns is None: + self.verbose_patterns = [re.compile(p) for p in ctx.options.verbose_pattern] + + matched = any(p.search(flow.request.path) for p in self.verbose_patterns) + if matched: + logger.info(format_flow(flow)) + + + def format_flow(flow: http.HTTPFlow) -> str: + return ( + "\n" + "RequestHeaders:\n" + f" {format_headers(flow.request.headers.items())}\n" + f"RequestBody: {flow.request.get_text()}\n" + f"Status: {flow.response.data.status_code}\n" + "ResponseHeaders:\n" + f" {format_headers(flow.response.headers.items())}\n" + f"ResponseBody: {flow.response.get_text()}\n" + ) + + + def format_headers(headers) -> str: + return "\n ".join(k + ": " + v for k, v in headers) + + + addons = [RegexLogger()] + '' + ); in { options.shb.mitmdump = { addons = mkOption { type = attrsOf str; - default = []; + default = [ ]; description = '' Addons available to the be added to the mitmdump instance. @@ -146,120 +166,129 @@ in }; instances = mkOption { - default = {}; + default = { }; description = "Mitmdump instance."; - type = attrsOf (submodule ({ name, ... }: { - options = { - package = lib.mkPackageOption pkgs "mitmproxy" {}; + type = attrsOf ( + submodule ( + { name, ... }: + { + options = { + package = lib.mkPackageOption pkgs "mitmproxy" { }; - serviceName = mkOption { - type = str; - description = '' - Name of the mitmdump system service. - ''; - default = "mitmdump-${name}.service"; - readOnly = true; - }; + serviceName = mkOption { + type = str; + description = '' + Name of the mitmdump system service. + ''; + default = "mitmdump-${name}.service"; + readOnly = true; + }; - listenHost = mkOption { - type = str; - default = "127.0.0.1"; - description = '' - Host the mitmdump instance will connect on. - ''; - }; + listenHost = mkOption { + type = str; + default = "127.0.0.1"; + description = '' + Host the mitmdump instance will connect on. + ''; + }; - listenPort = mkOption { - type = port; - description = '' - Port the mitmdump instance will listen on. + listenPort = mkOption { + type = port; + description = '' + Port the mitmdump instance will listen on. - The upstream port from the client's perspective. - ''; - }; + The upstream port from the client's perspective. + ''; + }; - upstreamHost = mkOption { - type = str; - default = "http://127.0.0.1"; - description = '' - Host the mitmdump instance will connect to. + upstreamHost = mkOption { + type = str; + default = "http://127.0.0.1"; + description = '' + Host the mitmdump instance will connect to. - If only an IP or domain is provided, - mitmdump will default to connect using HTTPS. - If this is not wanted, prefix the IP or domain with the 'http://' protocol. - ''; - }; + If only an IP or domain is provided, + mitmdump will default to connect using HTTPS. + If this is not wanted, prefix the IP or domain with the 'http://' protocol. + ''; + }; - upstreamPort = mkOption { - type = port; - description = '' - Port the mitmdump instance will connect to. + upstreamPort = mkOption { + type = port; + description = '' + Port the mitmdump instance will connect to. - The port the server is listening on. - ''; - }; + The port the server is listening on. + ''; + }; - after = mkOption { - type = listOf str; - default = []; - description = '' - Systemd services that must be started before this mitmdump proxy instance. + after = mkOption { + type = listOf str; + default = [ ]; + description = '' + Systemd services that must be started before this mitmdump proxy instance. - You are guaranteed the mitmdump is listening on the `listenPort` - when its systemd service has started. - ''; - }; + You are guaranteed the mitmdump is listening on the `listenPort` + when its systemd service has started. + ''; + }; - enabledAddons = mkOption { - type = listOf str; - default = []; - description = '' - Addons to enable on this mitmdump instance. - ''; - example = lib.literalExpression ''[ config.shb.mitmdump.addons.logger ]''; - }; + enabledAddons = mkOption { + type = listOf str; + default = [ ]; + description = '' + Addons to enable on this mitmdump instance. + ''; + example = lib.literalExpression ''[ config.shb.mitmdump.addons.logger ]''; + }; - extraArgs = mkOption { - type = listOf str; - default = []; - description = '' - Extra arguments to pass to the mitmdump instance. + extraArgs = mkOption { + type = listOf str; + default = [ ]; + description = '' + Extra arguments to pass to the mitmdump instance. - See upstream [manual](https://docs.mitmproxy.org/stable/concepts/options/#flow_detail) for all possible options. - ''; - example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]''; - }; - }; - })); + See upstream [manual](https://docs.mitmproxy.org/stable/concepts/options/#flow_detail) for all possible options. + ''; + example = lib.literalExpression ''[ "--set" "verbose_pattern=/api" ]''; + }; + }; + } + ) + ); }; }; config = { - systemd.services = mapAttrs' (name: cfg': nameValuePair "mitmdump-${name}" { - environment = { - "HOME" = "/var/lib/private/mitmdump-${name}"; - "MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump"; - }; - serviceConfig = { - Type = "notify"; - Restart = "on-failure"; - StandardOutput = "journal"; - StandardError = "journal"; + systemd.services = mapAttrs' ( + name: cfg': + nameValuePair "mitmdump-${name}" { + environment = { + "HOME" = "/var/lib/private/mitmdump-${name}"; + "MITMDUMP_BIN" = "${cfg'.package}/bin/mitmdump"; + }; + serviceConfig = { + Type = "notify"; + Restart = "on-failure"; + StandardOutput = "journal"; + StandardError = "journal"; - DynamicUser = true; - WorkingDirectory = "/var/lib/mitmdump-${name}"; - StateDirectory = "mitmdump-${name}"; + DynamicUser = true; + WorkingDirectory = "/var/lib/mitmdump-${name}"; + StateDirectory = "mitmdump-${name}"; - ExecStart = let - addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons; - extraArgs = lib.concatStringsSep " " cfg'.extraArgs; - in - "${lib.getExe mitmdumpScript} --listen_host ${cfg'.listenHost} --listen_port ${toString cfg'.listenPort} --upstream_host ${cfg'.upstreamHost} --upstream_port ${toString cfg'.upstreamPort} ${addons} ${extraArgs}"; - }; - requires = cfg'.after; - after = cfg'.after; - wantedBy = [ "multi-user.target" ]; - }) cfg.instances; + ExecStart = + let + addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons; + extraArgs = lib.concatStringsSep " " cfg'.extraArgs; + in + "${lib.getExe mitmdumpScript} --listen_host ${cfg'.listenHost} --listen_port ${toString cfg'.listenPort} --upstream_host ${cfg'.upstreamHost} --upstream_port ${toString cfg'.upstreamPort} ${addons} ${extraArgs}"; + }; + requires = cfg'.after; + after = cfg'.after; + wantedBy = [ "multi-user.target" ]; + } + ) cfg.instances; shb.mitmdump.addons = { inherit logger; diff --git a/modules/blocks/monitoring.nix b/modules/blocks/monitoring.nix index ef34ffe..b4ad524 100644 --- a/modules/blocks/monitoring.nix +++ b/modules/blocks/monitoring.nix @@ -1,9 +1,14 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.monitoring; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; @@ -53,7 +58,10 @@ in }; lokiMajorVersion = lib.mkOption { - type = lib.types.enum [ 2 3 ]; + type = lib.types.enum [ + 2 + 3 + ]; description = '' Switching from version 2 to 3 requires manual intervention https://grafana.com/docs/loki/latest/setup/upgrade/#main--unreleased. So this let's the user @@ -84,7 +92,7 @@ in contactPoints = lib.mkOption { type = lib.types.listOf lib.types.str; description = "List of email addresses to send alerts to"; - default = []; + default = [ ]; }; adminPassword = lib.mkOption { @@ -114,37 +122,39 @@ in smtp = lib.mkOption { description = "SMTP options."; default = null; - type = lib.types.nullOr (lib.types.submodule { - options = { - from_address = lib.mkOption { - type = lib.types.str; - description = "SMTP address from which the emails originate."; - example = "vaultwarden@mydomain.com"; + type = lib.types.nullOr ( + lib.types.submodule { + options = { + from_address = lib.mkOption { + type = lib.types.str; + description = "SMTP address from which the emails originate."; + example = "vaultwarden@mydomain.com"; + }; + from_name = lib.mkOption { + type = lib.types.str; + description = "SMTP name from which the emails originate."; + default = "Grafana"; + }; + host = lib.mkOption { + type = lib.types.str; + description = "SMTP host to send the emails to."; + }; + port = lib.mkOption { + type = lib.types.port; + description = "SMTP port to send the emails to."; + default = 25; + }; + username = lib.mkOption { + type = lib.types.str; + description = "Username to connect to the SMTP host."; + }; + passwordFile = lib.mkOption { + type = lib.types.str; + description = "File containing the password to connect to the SMTP host."; + }; }; - from_name = lib.mkOption { - type = lib.types.str; - description = "SMTP name from which the emails originate."; - default = "Grafana"; - }; - host = lib.mkOption { - type = lib.types.str; - description = "SMTP host to send the emails to."; - }; - port = lib.mkOption { - type = lib.types.port; - description = "SMTP port to send the emails to."; - default = 25; - }; - username = lib.mkOption { - type = lib.types.str; - description = "Username to connect to the SMTP host."; - }; - passwordFile = lib.mkOption { - type = lib.types.str; - description = "File containing the password to connect to the SMTP host."; - }; - }; - }); + } + ); }; }; @@ -204,12 +214,14 @@ in services.grafana.provision = { dashboards.settings = lib.mkIf cfg.provisionDashboards { apiVersion = 1; - providers = [{ - folder = "Self Host Blocks"; - options.path = ./monitoring/dashboards; - allowUiUpdates = true; - disableDeletion = true; - }]; + providers = [ + { + folder = "Self Host Blocks"; + options.path = ./monitoring/dashboards; + allowUiUpdates = true; + disableDeletion = true; + } + ]; }; datasources.settings = { apiVersion = 1; @@ -245,26 +257,35 @@ in }; alerting.contactPoints.settings = { apiVersion = 1; - contactPoints = [{ - inherit (cfg) orgId; - name = "grafana-default-email"; - receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [{ - uid = "sysadmin"; - type = "email"; - settings.addresses = lib.concatStringsSep ";" cfg.contactPoints; - }]; - }]; + contactPoints = [ + { + inherit (cfg) orgId; + name = "grafana-default-email"; + receivers = lib.optionals ((builtins.length cfg.contactPoints) > 0) [ + { + uid = "sysadmin"; + type = "email"; + settings.addresses = lib.concatStringsSep ";" cfg.contactPoints; + } + ]; + } + ]; }; alerting.policies.settings = { apiVersion = 1; - policies = [{ - inherit (cfg) orgId; - receiver = "grafana-default-email"; - group_by = [ "grafana_folder" "alertname" ]; - group_wait = "30s"; - group_interval = "5m"; - repeat_interval = "4h"; - }]; + policies = [ + { + inherit (cfg) orgId; + receiver = "grafana-default-email"; + group_by = [ + "grafana_folder" + "alertname" + ]; + group_wait = "30s"; + group_interval = "5m"; + repeat_interval = "4h"; + } + ]; # resetPolicies seems to happen after setting the above policies, effectively rolling back # any updates. }; @@ -273,18 +294,20 @@ in rules = builtins.fromJSON (builtins.readFile ./monitoring/rules.json); ruleIds = map (r: r.uid) rules; in - { - apiVersion = 1; - groups = [{ + { + apiVersion = 1; + groups = [ + { inherit (cfg) orgId; name = "SysAdmin"; folder = "Self Host Blocks"; interval = "10m"; inherit rules; - }]; - # deleteRules seems to happen after creating the above rules, effectively rolling back - # any updates. - }; + } + ]; + # deleteRules seems to happen after creating the above rules, effectively rolling back + # any updates. + }; }; services.prometheus = { @@ -295,36 +318,45 @@ in services.loki = { enable = true; dataDir = "/var/lib/loki"; - package = if cfg.lokiMajorVersion == 3 then pkgs.grafana-loki else - # Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4 - (pkgs.grafana-loki.overrideAttrs (finalAttrs: previousAttrs: rec { - version = "2.9.6"; + package = + if cfg.lokiMajorVersion == 3 then + pkgs.grafana-loki + else + # Comes from https://github.com/NixOS/nixpkgs/commit/8f95320f39d7e4e4a29ee70b8718974295a619f4 + (pkgs.grafana-loki.overrideAttrs ( + finalAttrs: previousAttrs: rec { + version = "2.9.6"; - src = pkgs.fetchFromGitHub { - owner = "grafana"; - repo = "loki"; - rev = "v${version}"; - hash = "sha256-79hK7axHf6soku5DvdXkE/0K4WKc4pnS9VMbVc1FS2I="; - }; + src = pkgs.fetchFromGitHub { + owner = "grafana"; + repo = "loki"; + rev = "v${version}"; + hash = "sha256-79hK7axHf6soku5DvdXkE/0K4WKc4pnS9VMbVc1FS2I="; + }; - subPackages = [ - "cmd/loki" - "cmd/loki-canary" - "clients/cmd/promtail" - "cmd/logcli" - # Removes "cmd/lokitool" - ]; + subPackages = [ + "cmd/loki" + "cmd/loki-canary" + "clients/cmd/promtail" + "cmd/logcli" + # Removes "cmd/lokitool" + ]; - ldflags = let t = "github.com/grafana/loki/pkg/util/build"; in [ - "-s" - "-w" - "-X ${t}.Version=${version}" - "-X ${t}.BuildUser=nix@nixpkgs" - "-X ${t}.BuildDate=unknown" - "-X ${t}.Branch=unknown" - "-X ${t}.Revision=unknown" - ]; - })); + ldflags = + let + t = "github.com/grafana/loki/pkg/util/build"; + in + [ + "-s" + "-w" + "-X ${t}.Version=${version}" + "-X ${t}.BuildUser=nix@nixpkgs" + "-X ${t}.BuildDate=unknown" + "-X ${t}.Branch=unknown" + "-X ${t}.Revision=unknown" + ]; + } + )); configuration = { auth_enabled = false; @@ -439,7 +471,7 @@ in proxyPass = "http://${toString config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}"; proxyWebsockets = true; extraConfig = '' - proxy_set_header Host $host; + proxy_set_header Host $host; ''; }; }; @@ -448,61 +480,75 @@ in services.prometheus.scrapeConfigs = [ { job_name = "node"; - static_configs = [{ - targets = ["127.0.0.1:${toString config.services.prometheus.exporters.node.port}"]; - labels = commonLabels; - }]; + static_configs = [ + { + targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ]; + labels = commonLabels; + } + ]; } { job_name = "netdata"; metrics_path = "/api/v1/allmetrics"; params.format = [ "prometheus" ]; honor_labels = true; - static_configs = [{ - targets = [ "127.0.0.1:19999" ]; - labels = commonLabels; - }]; + static_configs = [ + { + targets = [ "127.0.0.1:19999" ]; + labels = commonLabels; + } + ]; } { job_name = "smartctl"; - static_configs = [{ - targets = ["127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}"]; - labels = commonLabels; - }]; + static_configs = [ + { + targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.smartctl.port}" ]; + labels = commonLabels; + } + ]; } { job_name = "prometheus_internal"; - static_configs = [{ - targets = ["127.0.0.1:${toString config.services.prometheus.port}"]; - labels = commonLabels; - }]; + static_configs = [ + { + targets = [ "127.0.0.1:${toString config.services.prometheus.port}" ]; + labels = commonLabels; + } + ]; } - ] ++ (lib.lists.optional config.services.nginx.enable { - job_name = "nginx"; - static_configs = [{ - targets = ["127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}"]; + ] + ++ (lib.lists.optional config.services.nginx.enable { + job_name = "nginx"; + static_configs = [ + { + targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.nginx.port}" ]; labels = commonLabels; - }]; - # }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) { - # job_name = "redis"; - # static_configs = [ - # { - # targets = ["127.0.0.1:${toString config.services.prometheus.exporters.redis.port}"]; - # } - # ]; - # }) ++ (lib.optional (builtins.length (lib.attrNames config.services.openvpn.servers) > 0) { - # job_name = "openvpn"; - # static_configs = [ - # { - # targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"]; - # } - # ]; - }) ++ (lib.optional config.services.dnsmasq.enable { - job_name = "dnsmasq"; - static_configs = [{ - targets = ["127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}"]; + } + ]; + # }) ++ (lib.optional (builtins.length (lib.attrNames config.services.redis.servers) > 0) { + # job_name = "redis"; + # static_configs = [ + # { + # targets = ["127.0.0.1:${toString config.services.prometheus.exporters.redis.port}"]; + # } + # ]; + # }) ++ (lib.optional (builtins.length (lib.attrNames config.services.openvpn.servers) > 0) { + # job_name = "openvpn"; + # static_configs = [ + # { + # targets = ["127.0.0.1:${toString config.services.prometheus.exporters.openvpn.port}"]; + # } + # ]; + }) + ++ (lib.optional config.services.dnsmasq.enable { + job_name = "dnsmasq"; + static_configs = [ + { + targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}" ]; labels = commonLabels; - }]; + } + ]; }); services.prometheus.exporters.nginx = lib.mkIf config.services.nginx.enable { enable = true; @@ -513,7 +559,7 @@ in services.prometheus.exporters.node = { enable = true; # https://github.com/prometheus/node_exporter#collectors - enabledCollectors = ["ethtool"]; + enabledCollectors = [ "ethtool" ]; port = 9112; listenAddress = "127.0.0.1"; }; diff --git a/modules/blocks/nginx.nix b/modules/blocks/nginx.nix index 80e1b7d..eadb57a 100644 --- a/modules/blocks/nginx.nix +++ b/modules/blocks/nginx.nix @@ -1,9 +1,14 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.nginx; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = c: "${c.subdomain}.${c.domain}"; @@ -42,12 +47,13 @@ let autheliaRules = lib.mkOption { type = lib.types.listOf (lib.types.attrsOf lib.types.anything); - default = []; + default = [ ]; description = "Authelia rule configuration"; - example = lib.literalExpression ''[{ - policy = "two_factor"; - subject = ["group:service_user"]; - }]''; + example = lib.literalExpression '' + [{ + policy = "two_factor"; + subject = ["group:service_user"]; + }]''; }; extraConfig = lib.mkOption { @@ -77,41 +83,44 @@ in vhosts = lib.mkOption { description = "Endpoints to be protected by authelia."; type = lib.types.listOf vhostConfig; - default = []; + default = [ ]; }; }; config = { - networking.firewall.allowedTCPPorts = [ 80 443 ]; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; services.nginx.enable = true; services.nginx.logError = lib.mkIf cfg.debugLog "stderr warn"; services.nginx.appendHttpConfig = lib.mkIf cfg.accessLog '' - log_format apm - '{' - '"remote_addr":"$remote_addr",' - '"remote_user":"$remote_user",' - '"time_local":"$time_local",' - '"request":"$request",' - '"request_length":"$request_length",' - '"server_name":"$server_name",' - '"status":"$status",' - '"bytes_sent":"$bytes_sent",' - '"body_bytes_sent":"$body_bytes_sent",' - '"referrer":"$http_referrer",' - '"user_agent":"$http_user_agent",' - '"gzip_ration":"$gzip_ratio",' - '"post":"$request_body",' - '"upstream_addr":"$upstream_addr",' - '"upstream_status":"$upstream_status",' - '"request_time":"$request_time",' - '"upstream_response_time":"$upstream_response_time",' - '"upstream_connect_time":"$upstream_connect_time",' - '"upstream_header_time":"$upstream_header_time"' - '}'; + log_format apm + '{' + '"remote_addr":"$remote_addr",' + '"remote_user":"$remote_user",' + '"time_local":"$time_local",' + '"request":"$request",' + '"request_length":"$request_length",' + '"server_name":"$server_name",' + '"status":"$status",' + '"bytes_sent":"$bytes_sent",' + '"body_bytes_sent":"$body_bytes_sent",' + '"referrer":"$http_referrer",' + '"user_agent":"$http_user_agent",' + '"gzip_ration":"$gzip_ratio",' + '"post":"$request_body",' + '"upstream_addr":"$upstream_addr",' + '"upstream_status":"$upstream_status",' + '"request_time":"$request_time",' + '"upstream_response_time":"$upstream_response_time",' + '"upstream_connect_time":"$upstream_connect_time",' + '"upstream_header_time":"$upstream_header_time"' + '}'; - access_log syslog:server=unix:/dev/log apm; - ''; + access_log syslog:server=unix:/dev/log apm; + ''; services.nginx.virtualHosts = let @@ -189,13 +198,13 @@ in }; }; in - lib.mkMerge (map vhostCfg cfg.vhosts); + lib.mkMerge (map vhostCfg cfg.vhosts); shb.authelia.rules = let authConfig = c: map (r: r // { domain = fqdn c; }) c.autheliaRules; in - lib.flatten (map authConfig cfg.vhosts); + lib.flatten (map authConfig cfg.vhosts); security.acme.defaults.reloadServices = [ "nginx.service" diff --git a/modules/blocks/postgresql.nix b/modules/blocks/postgresql.nix index 92818aa..9e5f47a 100644 --- a/modules/blocks/postgresql.nix +++ b/modules/blocks/postgresql.nix @@ -1,9 +1,15 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.shb.postgresql; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; - upgrade-script = old: new: + upgrade-script = + old: new: let oldStr = builtins.toString old; newStr = builtins.toString new; @@ -11,37 +17,37 @@ let oldPkg = pkgs.${"postgresql_${oldStr}"}; newPkg = pkgs.${"postgresql_${newStr}"}; in - pkgs.writeScriptBin "upgrade-pg-cluster-${oldStr}-${newStr}" '' - set -eux - # XXX it's perhaps advisable to stop all services that depend on postgresql - systemctl stop postgresql + pkgs.writeScriptBin "upgrade-pg-cluster-${oldStr}-${newStr}" '' + set -eux + # XXX it's perhaps advisable to stop all services that depend on postgresql + systemctl stop postgresql - export NEWDATA="/var/lib/postgresql/${newPkg.psqlSchema}" - export NEWBIN="${newPkg}/bin" + export NEWDATA="/var/lib/postgresql/${newPkg.psqlSchema}" + export NEWBIN="${newPkg}/bin" - export OLDDATA="/var/lib/postgresql/${oldPkg.psqlSchema}" - export OLDBIN="${oldPkg}/bin" + export OLDDATA="/var/lib/postgresql/${oldPkg.psqlSchema}" + export OLDBIN="${oldPkg}/bin" - install -d -m 0700 -o postgres -g postgres "$NEWDATA" - cd "$NEWDATA" - sudo -u postgres $NEWBIN/initdb -D "$NEWDATA" + install -d -m 0700 -o postgres -g postgres "$NEWDATA" + cd "$NEWDATA" + sudo -u postgres $NEWBIN/initdb -D "$NEWDATA" - sudo -u postgres $NEWBIN/pg_upgrade \ - --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \ - --old-bindir $OLDBIN --new-bindir $NEWBIN \ - "$@" - ''; + sudo -u postgres $NEWBIN/pg_upgrade \ + --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \ + --old-bindir $OLDBIN --new-bindir $NEWBIN \ + "$@" + ''; in { options.shb.postgresql = { debug = lib.mkOption { type = lib.types.bool; description = '' - Enable debugging options. + Enable debugging options. - Currently enables shared_preload_libraries = "auto_explain, pg_stat_statements" + Currently enables shared_preload_libraries = "auto_explain, pg_stat_statements" - See https://www.postgresql.org/docs/current/pgstatstatements.html''; + See https://www.postgresql.org/docs/current/pgstatstatements.html''; default = false; }; enableTCPIP = lib.mkOption { @@ -55,7 +61,7 @@ in Backup configuration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.databasebackup.mkRequester { user = "postgres"; @@ -75,27 +81,29 @@ in ensures = lib.mkOption { description = "List of username, database and/or passwords that should be created."; - type = lib.types.listOf (lib.types.submodule { - options = { - username = lib.mkOption { - type = lib.types.str; - description = "Postgres user name."; - }; + type = lib.types.listOf ( + lib.types.submodule { + options = { + username = lib.mkOption { + type = lib.types.str; + description = "Postgres user name."; + }; - database = lib.mkOption { - type = lib.types.str; - description = "Postgres database."; - }; + database = lib.mkOption { + type = lib.types.str; + description = "Postgres database."; + }; - passwordFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - description = "Optional password file for the postgres user. If not given, only peer auth is accepted for this user, otherwise password auth is allowed."; - default = null; - example = "/run/secrets/postgresql/password"; + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = "Optional password file for the postgres user. If not given, only peer auth is accepted for this user, otherwise password auth is allowed."; + default = null; + example = "/run/secrets/postgresql/password"; + }; }; - }; - }); - default = []; + } + ); + default = [ ]; }; }; @@ -123,51 +131,61 @@ in dbConfig = ensureCfgs: { services.postgresql.enable = lib.mkDefault ((builtins.length ensureCfgs) > 0); services.postgresql.ensureDatabases = map ({ database, ... }: database) ensureCfgs; - services.postgresql.ensureUsers = map ({ username, database, ... }: { - name = username; - ensureDBOwnership = true; - ensureClauses.login = true; - }) ensureCfgs; + services.postgresql.ensureUsers = map ( + { username, database, ... }: + { + name = username; + ensureDBOwnership = true; + ensureClauses.login = true; + } + ) ensureCfgs; }; pwdConfig = ensureCfgs: { - systemd.services.postgresql-setup.script = lib.mkAfter - (let + systemd.services.postgresql-setup.script = lib.mkAfter ( + let prefix = '' - psql -tA <<'EOF' - DO $$ - DECLARE password TEXT; - BEGIN + psql -tA <<'EOF' + DO $$ + DECLARE password TEXT; + BEGIN ''; suffix = '' - END $$; - EOF - ''; - exec = { username, passwordFile, ... }: '' - password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', ''')); - EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password); + END $$; + EOF ''; + exec = + { username, passwordFile, ... }: + '' + password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', ''')); + EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password); + ''; cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs; in - if (builtins.length cfgsWithPasswords) == 0 then "" else - prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix); + if (builtins.length cfgsWithPasswords) == 0 then + "" + else + prefix + (lib.concatStrings (map exec cfgsWithPasswords)) + suffix + ); }; - debugConfig = enableDebug: lib.mkIf enableDebug { - services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements"; - }; + debugConfig = + enableDebug: + lib.mkIf enableDebug { + services.postgresql.settings.shared_preload_libraries = "auto_explain, pg_stat_statements"; + }; in - lib.mkMerge ([ - commonConfig - (dbConfig cfg.ensures) - (pwdConfig cfg.ensures) - (lib.mkIf cfg.enableTCPIP tcpConfig) - (debugConfig cfg.debug) - { - environment.systemPackages = lib.mkIf config.services.postgresql.enable [ - (upgrade-script 15 16) - (upgrade-script 16 17) - ]; - } - ]); + lib.mkMerge ([ + commonConfig + (dbConfig cfg.ensures) + (pwdConfig cfg.ensures) + (lib.mkIf cfg.enableTCPIP tcpConfig) + (debugConfig cfg.debug) + { + environment.systemPackages = lib.mkIf config.services.postgresql.enable [ + (upgrade-script 15 16) + (upgrade-script 16 17) + ]; + } + ]); } diff --git a/modules/blocks/restic.nix b/modules/blocks/restic.nix index eec9cc3..4b78dce 100644 --- a/modules/blocks/restic.nix +++ b/modules/blocks/restic.nix @@ -1,167 +1,226 @@ -{ config, pkgs, lib, utils, ... }: +{ + config, + pkgs, + lib, + utils, + ... +}: let cfg = config.shb.restic; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; - inherit (lib) concatStringsSep filterAttrs flatten literalExpression optionals listToAttrs mapAttrsToList mkEnableOption mkOption mkMerge; - inherit (lib) hasPrefix mkIf nameValuePair optionalAttrs removePrefix; - inherit (lib.types) attrsOf enum int ints oneOf nonEmptyStr nullOr str submodule; + inherit (lib) + concatStringsSep + filterAttrs + flatten + literalExpression + optionals + listToAttrs + mapAttrsToList + mkEnableOption + mkOption + mkMerge + ; + inherit (lib) + hasPrefix + mkIf + nameValuePair + optionalAttrs + removePrefix + ; + inherit (lib.types) + attrsOf + enum + int + ints + oneOf + nonEmptyStr + nullOr + str + submodule + ; - commonOptions = { name, prefix, config, ... }: { - enable = mkEnableOption '' - this backup intance. + commonOptions = + { + name, + prefix, + config, + ... + }: + { + enable = mkEnableOption '' + this backup intance. - A disabled instance will not backup data anymore - but still provides the helper tool to restore snapshots - ''; + A disabled instance will not backup data anymore + but still provides the helper tool to restore snapshots + ''; - passphrase = lib.mkOption { - description = "Encryption key for the backup repository."; - type = lib.types.submodule { - options = contracts.secret.mkRequester { - mode = "0400"; - owner = config.request.user; - ownerText = "[shb.restic.${prefix}..request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)"; - restartUnits = [ (fullName name config.settings.repository) ]; - restartUnitsText = "[ [shb.restic.${prefix}..settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]"; + passphrase = lib.mkOption { + description = "Encryption key for the backup repository."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = config.request.user; + ownerText = "[shb.restic.${prefix}..request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)"; + restartUnits = [ (fullName name config.settings.repository) ]; + restartUnitsText = "[ [shb.restic.${prefix}..settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]"; + }; }; }; - }; - repository = mkOption { - description = "Repositories to back this instance to."; - type = submodule { - options = { - path = mkOption { - type = str; - description = "Repository location"; - }; - - secrets = mkOption { - type = attrsOf lib.shb.secretFileType; - default = {}; - description = '' - Secrets needed to access the repository where the backups will be stored. - - See [s3 config](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3) for an example - and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets. - - ''; - example = literalExpression '' - { - AWS_ACCESS_KEY_ID.source = ; - AWS_SECRET_ACCESS_KEY.source = ; - } - ''; - }; - - timerConfig = mkOption { - type = attrsOf utils.systemdUtils.unitOptions.unitOption; - default = { - OnCalendar = "daily"; - Persistent = true; + repository = mkOption { + description = "Repositories to back this instance to."; + type = submodule { + options = { + path = mkOption { + type = str; + description = "Repository location"; }; - description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.''; - example = { - OnCalendar = "00:05"; - RandomizedDelaySec = "5h"; - Persistent = true; + + secrets = mkOption { + type = attrsOf lib.shb.secretFileType; + default = { }; + description = '' + Secrets needed to access the repository where the backups will be stored. + + See [s3 config](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3) for an example + and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets. + + ''; + example = literalExpression '' + { + AWS_ACCESS_KEY_ID.source = ; + AWS_SECRET_ACCESS_KEY.source = ; + } + ''; + }; + + timerConfig = mkOption { + type = attrsOf utils.systemdUtils.unitOptions.unitOption; + default = { + OnCalendar = "daily"; + Persistent = true; + }; + description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.''; + example = { + OnCalendar = "00:05"; + RandomizedDelaySec = "5h"; + Persistent = true; + }; }; }; }; }; - }; - retention = mkOption { - description = "For how long to keep backup files."; - type = attrsOf (oneOf [ int nonEmptyStr ]); - default = { - keep_within = "1d"; - keep_hourly = 24; - keep_daily = 7; - keep_weekly = 4; - keep_monthly = 6; + retention = mkOption { + description = "For how long to keep backup files."; + type = attrsOf (oneOf [ + int + nonEmptyStr + ]); + default = { + keep_within = "1d"; + keep_hourly = 24; + keep_daily = 7; + keep_weekly = 4; + keep_monthly = 6; + }; + }; + + limitUploadKiBs = mkOption { + type = nullOr int; + description = "Limit upload bandwidth to the given KiB/s amount."; + default = null; + example = 8000; + }; + + limitDownloadKiBs = mkOption { + type = nullOr int; + description = "Limit download bandwidth to the given KiB/s amount."; + default = null; + example = 8000; }; }; - limitUploadKiBs = mkOption { - type = nullOr int; - description = "Limit upload bandwidth to the given KiB/s amount."; - default = null; - example = 8000; - }; - - limitDownloadKiBs = mkOption { - type = nullOr int; - description = "Limit download bandwidth to the given KiB/s amount."; - default = null; - example = 8000; - }; - }; - - repoSlugName = name: builtins.replaceStrings ["/" ":"] ["_" "_"] (removePrefix "/" name); + repoSlugName = name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (removePrefix "/" name); fullName = name: repository: "restic-backups-${name}_${repoSlugName repository.path}"; in { options.shb.restic = { instances = mkOption { description = "Files to backup following the [backup contract](./contracts-backup.html)."; - default = {}; - type = attrsOf (submodule ({ name, config, ... }: { - options = contracts.backup.mkProvider { - settings = mkOption { - description = '' - Settings specific to the Restic provider. - ''; + default = { }; + type = attrsOf ( + submodule ( + { name, config, ... }: + { + options = contracts.backup.mkProvider { + settings = mkOption { + description = '' + Settings specific to the Restic provider. + ''; - type = submodule { - options = commonOptions { inherit name config; prefix = "instances"; }; + type = submodule { + options = commonOptions { + inherit name config; + prefix = "instances"; + }; + }; + }; + + resultCfg = { + restoreScript = fullName name config.settings.repository; + restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; + + backupService = "${fullName name config.settings.repository}.service"; + backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; + }; }; - }; - - resultCfg = { - restoreScript = fullName name config.settings.repository; - restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; - - backupService = "${fullName name config.settings.repository}.service"; - backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; - }; - }; - })); + } + ) + ); }; databases = mkOption { description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html)."; - default = {}; - type = attrsOf (submodule ({ name, config, ... }: { - options = contracts.databasebackup.mkProvider { - settings = mkOption { - description = '' - Settings specific to the Restic provider. - ''; + default = { }; + type = attrsOf ( + submodule ( + { name, config, ... }: + { + options = contracts.databasebackup.mkProvider { + settings = mkOption { + description = '' + Settings specific to the Restic provider. + ''; - type = submodule { - options = commonOptions { inherit name config; prefix = "databases"; }; + type = submodule { + options = commonOptions { + inherit name config; + prefix = "databases"; + }; + }; + }; + + resultCfg = { + restoreScript = fullName name config.settings.repository; + restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; + + backupService = "${fullName name config.settings.repository}.service"; + backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; + }; }; - }; - - resultCfg = { - restoreScript = fullName name config.settings.repository; - restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; - - backupService = "${fullName name config.settings.repository}.service"; - backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; - }; - }; - })); + } + ) + ); }; # Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23 performance = mkOption { description = "Reduce performance impact of backup jobs."; - default = {}; + default = { }; type = submodule { options = { niceness = mkOption { @@ -170,7 +229,11 @@ in default = 15; }; ioSchedulingClass = mkOption { - type = enum [ "idle" "best-effort" "realtime" ]; + type = enum [ + "idle" + "best-effort" + "realtime" + ]; description = "ionice scheduling class, defaults to best-effort IO. Only used for `restic backup`, `restic forget` and `restic check` commands."; default = "best-effort"; }; @@ -184,23 +247,28 @@ in }; }; - config = mkIf (cfg.instances != {} || cfg.databases != {}) ( + config = mkIf (cfg.instances != { } || cfg.databases != { }) ( let enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances; enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases; - in mkMerge [ + in + mkMerge [ { - environment.systemPackages = optionals (enabledInstances != {} || enabledDatabases != {}) [ pkgs.restic ]; + environment.systemPackages = optionals (enabledInstances != { } || enabledDatabases != { }) [ + pkgs.restic + ]; } { # Create repository if it is a local path. systemd.tmpfiles.rules = let - mkSettings = name: instance: optionals (hasPrefix "/" instance.settings.repository.path) [ - "d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -" - ]; + mkSettings = + name: instance: + optionals (hasPrefix "/" instance.settings.repository.path) [ + "d '${instance.settings.repository.path}' 0750 ${instance.request.user} root - -" + ]; in - flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases)); + flatten (mapAttrsToList mkSettings (cfg.instances // cfg.databases)); } { services.restic.backups = @@ -219,8 +287,8 @@ in inherit (instance.settings.repository) timerConfig; - pruneOpts = mapAttrsToList (name: value: - "--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}" + pruneOpts = mapAttrsToList ( + name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}" ) instance.settings.retention; backupPrepareCommand = concatStringsSep "\n" instance.request.hooks.beforeBackup; @@ -234,12 +302,13 @@ in ++ (optionals (instance.settings.limitDownloadKiBs != null) [ "--limit-download=${toString instance.settings.limitDownloadKiBs}" ]); - } // optionalAttrs (builtins.length instance.request.excludePatterns > 0) { + } + // optionalAttrs (builtins.length instance.request.excludePatterns > 0) { exclude = instance.request.excludePatterns; }; }; in - mkMerge (flatten (mapAttrsToList mkSettings enabledInstances)); + mkMerge (flatten (mapAttrsToList mkSettings enabledInstances)); } { services.restic.backups = @@ -258,8 +327,8 @@ in inherit (instance.settings.repository) timerConfig; - pruneOpts = mapAttrsToList (name: value: - "--${builtins.replaceStrings ["_"] ["-"] name} ${builtins.toString value}" + pruneOpts = mapAttrsToList ( + name: value: "--${builtins.replaceStrings [ "_" ] [ "-" ] name} ${builtins.toString value}" ) instance.settings.retention; extraBackupArgs = @@ -269,120 +338,131 @@ in ++ (optionals (instance.settings.limitDownloadKiBs != null) [ "--limit-download=${toString instance.settings.limitDownloadKiBs}" ]) - ++ - (let - cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd; - in + ++ ( + let + cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd; + in [ "--stdin-filename ${instance.request.backupName} --stdin-from-command -- ${cmd}/bin/dump.sh" - ]); + ] + ); }; }; in - mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases)); + mkMerge (flatten (mapAttrsToList mkSettings enabledDatabases)); } { systemd.services = let - mkSettings = name: instance: + mkSettings = + name: instance: let serviceName = fullName name instance.settings.repository; in - { - ${serviceName} = mkMerge [ - { - serviceConfig = { - Nice = cfg.performance.niceness; - IOSchedulingClass = cfg.performance.ioSchedulingClass; - IOSchedulingPriority = cfg.performance.ioPriority; - # BindReadOnlyPaths = instance.sourceDirectories; - }; - } - (optionalAttrs (instance.settings.repository.secrets != {}) - { - serviceConfig.EnvironmentFile = [ - "/run/secrets_restic/${serviceName}" - ]; - after = [ "${serviceName}-pre.service" ]; - requires = [ "${serviceName}-pre.service" ]; - }) - ]; + { + ${serviceName} = mkMerge [ + { + serviceConfig = { + Nice = cfg.performance.niceness; + IOSchedulingClass = cfg.performance.ioSchedulingClass; + IOSchedulingPriority = cfg.performance.ioPriority; + # BindReadOnlyPaths = instance.sourceDirectories; + }; + } + (optionalAttrs (instance.settings.repository.secrets != { }) { + serviceConfig.EnvironmentFile = [ + "/run/secrets_restic/${serviceName}" + ]; + after = [ "${serviceName}-pre.service" ]; + requires = [ "${serviceName}-pre.service" ]; + }) + ]; - "${serviceName}-pre" = mkIf (instance.settings.repository.secrets != {}) - (let - script = lib.shb.genConfigOutOfBandSystemd { - config = instance.settings.repository.secrets; - configLocation = "/run/secrets_restic/${serviceName}"; - generator = lib.shb.toEnvVar; - user = instance.request.user; - }; - in - { - script = script.preStart; - serviceConfig.Type = "oneshot"; - serviceConfig.LoadCredential = script.loadCredentials; - }); - }; + "${serviceName}-pre" = mkIf (instance.settings.repository.secrets != { }) ( + let + script = lib.shb.genConfigOutOfBandSystemd { + config = instance.settings.repository.secrets; + configLocation = "/run/secrets_restic/${serviceName}"; + generator = lib.shb.toEnvVar; + user = instance.request.user; + }; + in + { + script = script.preStart; + serviceConfig.Type = "oneshot"; + serviceConfig.LoadCredential = script.loadCredentials; + } + ); + }; in - mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases))); + mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases))); } { - systemd.services = let - mkEnv = name: instance: - nameValuePair "${fullName name instance.settings.repository}_restore_gen" { - enable = true; - wantedBy = [ "multi-user.target" ]; - serviceConfig.Type = "oneshot"; - script = (lib.shb.replaceSecrets { - userConfig = instance.settings.repository.secrets // { - RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path; - RESTIC_REPOSITORY = instance.settings.repository.path; - }; - resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}"; - generator = lib.shb.toEnvVar; - user = instance.request.user; - }); - }; - in + systemd.services = + let + mkEnv = + name: instance: + nameValuePair "${fullName name instance.settings.repository}_restore_gen" { + enable = true; + wantedBy = [ "multi-user.target" ]; + serviceConfig.Type = "oneshot"; + script = ( + lib.shb.replaceSecrets { + userConfig = instance.settings.repository.secrets // { + RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path; + RESTIC_REPOSITORY = instance.settings.repository.path; + }; + resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}"; + generator = lib.shb.toEnvVar; + user = instance.request.user; + } + ); + }; + in listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases))); } { - environment.systemPackages = let - mkResticBinary = name: instance: - pkgs.writeShellScriptBin (fullName name instance.settings.repository) '' - set -euo pipefail + environment.systemPackages = + let + mkResticBinary = + name: instance: + pkgs.writeShellScriptBin (fullName name instance.settings.repository) '' + set -euo pipefail - export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \ - | xargs -d '\n') + export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \ + | xargs -d '\n') - if ! [ "$1" = "restore" ]; then - sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@ - else - shift - sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic restore $@ --target /" - fi + if ! [ "$1" = "restore" ]; then + sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@ + else + shift + sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic restore $@ --target /" + fi ''; - in + in flatten (mapAttrsToList mkResticBinary cfg.instances); } { - environment.systemPackages = let - mkResticBinary = name: instance: - pkgs.writeShellScriptBin (fullName name instance.settings.repository) '' - set -euo pipefail + environment.systemPackages = + let + mkResticBinary = + name: instance: + pkgs.writeShellScriptBin (fullName name instance.settings.repository) '' + set -euo pipefail - export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \ - | xargs -d '\n') + export $(grep -v '^#' "/run/secrets_restic_env/${fullName name instance.settings.repository}" \ + | xargs -d '\n') - if ! [ "$1" = "restore" ]; then - sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@ - else - shift - sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupName} | ${instance.request.restoreCmd}" - fi + if ! [ "$1" = "restore" ]; then + sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@ + else + shift + sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupName} | ${instance.request.restoreCmd}" + fi ''; - in + in flatten (mapAttrsToList mkResticBinary cfg.databases); } - ]); + ] + ); } diff --git a/modules/blocks/sops.nix b/modules/blocks/sops.nix index 34960d4..1d7d733 100644 --- a/modules/blocks/sops.nix +++ b/modules/blocks/sops.nix @@ -1,9 +1,14 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let inherit (lib) mapAttrs mkOption; inherit (lib.types) attrsOf anything submodule; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; cfg = config.shb.sops; in @@ -11,35 +16,42 @@ in options.shb.sops = { secret = mkOption { description = "Secret following the [secret contract](./contracts-secret.html)."; - default = {}; - type = attrsOf (submodule ({ name, options, ... }: { - options = contracts.secret.mkProvider { - settings = mkOption { - description = '' - Settings specific to the Sops provider. + default = { }; + type = attrsOf ( + submodule ( + { name, options, ... }: + { + options = contracts.secret.mkProvider { + settings = mkOption { + description = '' + Settings specific to the Sops provider. - This is a passthrough option to set [sops-nix options](https://github.com/Mic92/sops-nix/blob/master/modules/sops/default.nix). + This is a passthrough option to set [sops-nix options](https://github.com/Mic92/sops-nix/blob/master/modules/sops/default.nix). - Note though that the `mode`, `owner`, `group`, and `restartUnits` - are managed by the [shb.sops.secret..request](#blocks-sops-options-shb.sops.secret._name_.request) option. - ''; + Note though that the `mode`, `owner`, `group`, and `restartUnits` + are managed by the [shb.sops.secret..request](#blocks-sops-options-shb.sops.secret._name_.request) option. + ''; - type = attrsOf anything; - default = {}; - }; + type = attrsOf anything; + default = { }; + }; - resultCfg = { - path = "/run/secrets/${name}"; - pathText = "/run/secrets/"; - }; - }; - })); + resultCfg = { + path = "/run/secrets/${name}"; + pathText = "/run/secrets/"; + }; + }; + } + ) + ); }; }; config = { - sops.secrets = let - mkSecret = n: secretCfg: secretCfg.request // secretCfg.settings; - in mapAttrs mkSecret cfg.secret; + sops.secrets = + let + mkSecret = n: secretCfg: secretCfg.request // secretCfg.settings; + in + mapAttrs mkSecret cfg.secret; }; } diff --git a/modules/blocks/ssl.nix b/modules/blocks/ssl.nix index 6f4ebc5..7e09dca 100644 --- a/modules/blocks/ssl.nix +++ b/modules/blocks/ssl.nix @@ -1,12 +1,23 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.certs; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; inherit (builtins) dirOf; - inherit (lib) flatten mapAttrsToList optionalAttrs optionals unique; + inherit (lib) + flatten + mapAttrsToList + optionalAttrs + optionals + unique + ; in { options.shb.certs = { @@ -19,288 +30,303 @@ in }; cas.selfsigned = lib.mkOption { description = "Generate a self-signed Certificate Authority."; - default = {}; - type = lib.types.attrsOf (lib.types.submodule ({ config, ...}: { - options = { - name = lib.mkOption { - type = lib.types.str; - description = '' - Certificate Authority Name. You can put what you want here, it will be displayed by the - browser. - ''; - default = "Self Host Blocks Certificate"; - }; + default = { }; + type = lib.types.attrsOf ( + lib.types.submodule ( + { config, ... }: + { + options = { + name = lib.mkOption { + type = lib.types.str; + description = '' + Certificate Authority Name. You can put what you want here, it will be displayed by the + browser. + ''; + default = "Self Host Blocks Certificate"; + }; - paths = lib.mkOption { - description = '' - Paths where CA certs will be located. + paths = lib.mkOption { + description = '' + Paths where CA certs will be located. - This option implements the SSL Generator contract. - ''; - type = contracts.ssl.certs-paths; - default = { - key = "/var/lib/certs/cas/${config._module.args.name}.key"; - cert = "/var/lib/certs/cas/${config._module.args.name}.cert"; + This option implements the SSL Generator contract. + ''; + type = contracts.ssl.certs-paths; + default = { + key = "/var/lib/certs/cas/${config._module.args.name}.key"; + cert = "/var/lib/certs/cas/${config._module.args.name}.cert"; + }; + }; + + systemdService = lib.mkOption { + description = '' + Systemd oneshot service used to generate the certs. + + This option implements the SSL Generator contract. + ''; + type = lib.types.str; + default = "shb-certs-ca-${config._module.args.name}.service"; + }; }; - }; - - systemdService = lib.mkOption { - description = '' - Systemd oneshot service used to generate the certs. - - This option implements the SSL Generator contract. - ''; - type = lib.types.str; - default = "shb-certs-ca-${config._module.args.name}.service"; - }; - }; - })); + } + ) + ); }; certs.selfsigned = lib.mkOption { description = "Generate self-signed certificates signed by a Certificate Authority."; - default = {}; - type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: { - options = { - ca = lib.mkOption { - type = lib.types.nullOr contracts.ssl.cas; - description = '' - CA used to generate this certificate. Only used for self-signed. + default = { }; + type = lib.types.attrsOf ( + lib.types.submodule ( + { config, ... }: + { + options = { + ca = lib.mkOption { + type = lib.types.nullOr contracts.ssl.cas; + description = '' + CA used to generate this certificate. Only used for self-signed. - This contract input takes the contract output of the `shb.certs.cas` SSL block. - ''; - default = null; - }; + This contract input takes the contract output of the `shb.certs.cas` SSL block. + ''; + default = null; + }; - domain = lib.mkOption { - type = lib.types.str; - description = '' - Domain to generate a certificate for. This can be a wildcard domain like - `*.example.com`. - ''; - example = "example.com"; - }; + domain = lib.mkOption { + type = lib.types.str; + description = '' + Domain to generate a certificate for. This can be a wildcard domain like + `*.example.com`. + ''; + example = "example.com"; + }; - extraDomains = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = '' - Other domains to generate a certificate for. - ''; - default = []; - example = lib.literalExpression '' - [ - "sub1.example.com" - "sub2.example.com" - ] - ''; - }; + extraDomains = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + Other domains to generate a certificate for. + ''; + default = [ ]; + example = lib.literalExpression '' + [ + "sub1.example.com" + "sub2.example.com" + ] + ''; + }; - group = lib.mkOption { - type = lib.types.str; - description = '' - Unix group owning this certificate. - ''; - default = "root"; - example = "nginx"; - }; + group = lib.mkOption { + type = lib.types.str; + description = '' + Unix group owning this certificate. + ''; + default = "root"; + example = "nginx"; + }; - paths = lib.mkOption { - description = '' - Paths where certs will be located. + paths = lib.mkOption { + description = '' + Paths where certs will be located. - This option implements the SSL Generator contract. - ''; - type = contracts.ssl.certs-paths; - default = { - key = "/var/lib/certs/selfsigned/${config._module.args.name}.key"; - cert = "/var/lib/certs/selfsigned/${config._module.args.name}.cert"; + This option implements the SSL Generator contract. + ''; + type = contracts.ssl.certs-paths; + default = { + key = "/var/lib/certs/selfsigned/${config._module.args.name}.key"; + cert = "/var/lib/certs/selfsigned/${config._module.args.name}.cert"; + }; + }; + + systemdService = lib.mkOption { + description = '' + Systemd oneshot service used to generate the certs. + + This option implements the SSL Generator contract. + ''; + type = lib.types.str; + default = "shb-certs-cert-selfsigned-${config._module.args.name}.service"; + }; + + reloadServices = lib.mkOption { + description = '' + The list of systemd services to call `systemctl try-reload-or-restart` on. + ''; + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "nginx.service" ]; + }; }; - }; - - systemdService = lib.mkOption { - description = '' - Systemd oneshot service used to generate the certs. - - This option implements the SSL Generator contract. - ''; - type = lib.types.str; - default = "shb-certs-cert-selfsigned-${config._module.args.name}.service"; - }; - - reloadServices = lib.mkOption { - description = '' - The list of systemd services to call `systemctl try-reload-or-restart` on. - ''; - type = lib.types.listOf lib.types.str; - default = []; - example = [ "nginx.service" ]; - }; - }; - })); + } + ) + ); }; certs.letsencrypt = lib.mkOption { description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/)."; - default = {}; - type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: { - options = { - domain = lib.mkOption { - type = lib.types.str; - description = '' - Domain to generate a certificate for. This can be a wildcard domain like - `*.example.com`. - ''; - example = "example.com"; - }; + default = { }; + type = lib.types.attrsOf ( + lib.types.submodule ( + { config, ... }: + { + options = { + domain = lib.mkOption { + type = lib.types.str; + description = '' + Domain to generate a certificate for. This can be a wildcard domain like + `*.example.com`. + ''; + example = "example.com"; + }; - extraDomains = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = '' - Other domains to generate a certificate for. - ''; - default = []; - example = lib.literalExpression '' - [ - "sub1.example.com" - "sub2.example.com" - ] - ''; - }; + extraDomains = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + Other domains to generate a certificate for. + ''; + default = [ ]; + example = lib.literalExpression '' + [ + "sub1.example.com" + "sub2.example.com" + ] + ''; + }; - paths = lib.mkOption { - description = '' - Paths where certs will be located. + paths = lib.mkOption { + description = '' + Paths where certs will be located. - This option implements the SSL Generator contract. - ''; - type = contracts.ssl.certs-paths; - default = { - key = "/var/lib/acme/${config._module.args.name}/key.pem"; - cert = "/var/lib/acme/${config._module.args.name}/cert.pem"; + This option implements the SSL Generator contract. + ''; + type = contracts.ssl.certs-paths; + default = { + key = "/var/lib/acme/${config._module.args.name}/key.pem"; + cert = "/var/lib/acme/${config._module.args.name}/cert.pem"; + }; + }; + + group = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = '' + Unix group owning this certificate. + ''; + default = "acme"; + example = "nginx"; + }; + + systemdService = lib.mkOption { + description = '' + Systemd oneshot service used to generate the certs. + + This option implements the SSL Generator contract. + ''; + type = lib.types.str; + default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service"; + }; + + afterAndWants = lib.mkOption { + description = '' + Systemd service(s) that must start successfully before attempting to reach acme. + ''; + type = lib.types.listOf lib.types.str; + default = [ ]; + example = lib.literalExpression '' + [ "dnsmasq.service" ] + ''; + }; + + reloadServices = lib.mkOption { + description = '' + The list of systemd services to call `systemctl try-reload-or-restart` on. + ''; + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "nginx.service" ]; + }; + + dnsProvider = lib.mkOption { + description = '' + DNS provider to use. + + See https://go-acme.github.io/lego/dns/ for the list of supported providers. + + If null is given, use instead the reverse proxy to validate the domain. + ''; + type = lib.types.nullOr lib.types.str; + default = null; + example = "linode"; + }; + + dnsResolver = lib.mkOption { + description = "IP of a DNS server used to resolve hostnames."; + type = lib.types.str; + default = "8.8.8.8"; + }; + + credentialsFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + description = '' + Credentials file location for the chosen DNS provider. + + The content of this file must expose environment variables as written in the + [documentation](https://go-acme.github.io/lego/dns/) of each DNS provider. + + For example, if the documentation says the credential must be located in the environment + variable DNSPROVIDER_TOKEN, then the file content must be: + + DNSPROVIDER_TOKEN=xyz + + You can put non-secret environment variables here too or use shb.ssl.additionalcfg instead. + ''; + example = "/run/secrets/ssl"; + default = null; + }; + + additionalEnvironment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Additional environment variables used to configure the DNS provider. + + For secrets, use shb.ssl.credentialsFile instead. + + See the chosen provider's [documentation](https://go-acme.github.io/lego/dns/) for + available options. + ''; + example = lib.literalExpression '' + { + DNSPROVIDER_TIMEOUT = "10"; + DNSPROVIDER_PROPAGATION_TIMEOUT = "240"; + } + ''; + }; + + makeAvailableToUser = lib.mkOption { + type = lib.types.nullOr lib.types.str; + description = '' + Make all certificates available to given user. + ''; + default = null; + }; + + adminEmail = lib.mkOption { + description = "Admin email in case certificate retrieval goes wrong."; + type = lib.types.str; + }; + + stagingServer = lib.mkOption { + description = "User Let's Encrypt's staging server."; + type = lib.types.bool; + default = false; + }; + + debug = lib.mkOption { + description = "Enable debug logging"; + type = lib.types.bool; + default = false; + }; }; - }; - - group = lib.mkOption { - type = lib.types.nullOr lib.types.str; - description = '' - Unix group owning this certificate. - ''; - default = "acme"; - example = "nginx"; - }; - - systemdService = lib.mkOption { - description = '' - Systemd oneshot service used to generate the certs. - - This option implements the SSL Generator contract. - ''; - type = lib.types.str; - default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service"; - }; - - afterAndWants = lib.mkOption { - description = '' - Systemd service(s) that must start successfully before attempting to reach acme. - ''; - type = lib.types.listOf lib.types.str; - default = []; - example = lib.literalExpression '' - [ "dnsmasq.service" ] - ''; - }; - - reloadServices = lib.mkOption { - description = '' - The list of systemd services to call `systemctl try-reload-or-restart` on. - ''; - type = lib.types.listOf lib.types.str; - default = []; - example = [ "nginx.service" ]; - }; - - dnsProvider = lib.mkOption { - description = '' - DNS provider to use. - - See https://go-acme.github.io/lego/dns/ for the list of supported providers. - - If null is given, use instead the reverse proxy to validate the domain. - ''; - type = lib.types.nullOr lib.types.str; - default = null; - example = "linode"; - }; - - dnsResolver = lib.mkOption { - description = "IP of a DNS server used to resolve hostnames."; - type = lib.types.str; - default = "8.8.8.8"; - }; - - credentialsFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - description = '' - Credentials file location for the chosen DNS provider. - - The content of this file must expose environment variables as written in the - [documentation](https://go-acme.github.io/lego/dns/) of each DNS provider. - - For example, if the documentation says the credential must be located in the environment - variable DNSPROVIDER_TOKEN, then the file content must be: - - DNSPROVIDER_TOKEN=xyz - - You can put non-secret environment variables here too or use shb.ssl.additionalcfg instead. - ''; - example = "/run/secrets/ssl"; - default = null; - }; - - additionalEnvironment = lib.mkOption { - type = lib.types.attrsOf lib.types.str; - default = {}; - description = '' - Additional environment variables used to configure the DNS provider. - - For secrets, use shb.ssl.credentialsFile instead. - - See the chosen provider's [documentation](https://go-acme.github.io/lego/dns/) for - available options. - ''; - example = lib.literalExpression '' - { - DNSPROVIDER_TIMEOUT = "10"; - DNSPROVIDER_PROPAGATION_TIMEOUT = "240"; - } - ''; - }; - - makeAvailableToUser = lib.mkOption { - type = lib.types.nullOr lib.types.str; - description = '' - Make all certificates available to given user. - ''; - default = null; - }; - - adminEmail = lib.mkOption { - description = "Admin email in case certificate retrieval goes wrong."; - type = lib.types.str; - }; - - stagingServer = lib.mkOption { - description = "User Let's Encrypt's staging server."; - type = lib.types.bool; - default = false; - }; - - debug = lib.mkOption { - description = "Enable debug logging"; - type = lib.types.bool; - default = false; - }; - }; - })); + } + ) + ); }; }; @@ -308,87 +334,92 @@ in let serviceName = lib.strings.removeSuffix ".service"; in - lib.mkMerge [ - # Config for self-signed CA. - { - systemd.services = lib.mapAttrs' (_name: caCfg: - lib.nameValuePair (serviceName caCfg.systemdService) { - wantedBy = [ "multi-user.target" ]; - wants = [ config.shb.certs.systemdService ]; - before = [ config.shb.certs.systemdService ]; - serviceConfig.Type = "oneshot"; - serviceConfig.RuntimeDirectory = serviceName caCfg.systemdService; - # Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix - script = '' - cd $RUNTIME_DIRECTORY + lib.mkMerge [ + # Config for self-signed CA. + { + systemd.services = lib.mapAttrs' ( + _name: caCfg: + lib.nameValuePair (serviceName caCfg.systemdService) { + wantedBy = [ "multi-user.target" ]; + wants = [ config.shb.certs.systemdService ]; + before = [ config.shb.certs.systemdService ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RuntimeDirectory = serviceName caCfg.systemdService; + # Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix + script = '' + cd $RUNTIME_DIRECTORY - cat >ca.template <ca.template < /etc/ssl/certs/ca-bundle.crt - cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-certificates.crt - for file in ${lib.concatStringsSep " " (mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned)}; do - cat "$file" >> /etc/ssl/certs/ca-bundle.crt - cat "$file" >> /etc/ssl/certs/ca-certificates.crt - done + cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-bundle.crt + cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-certificates.crt + for file in ${ + lib.concatStringsSep " " (mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned) + }; do + cat "$file" >> /etc/ssl/certs/ca-bundle.crt + cat "$file" >> /etc/ssl/certs/ca-certificates.crt + done ''; - }); - } - # Config for self-signed cert. - { - systemd.services = lib.mapAttrs' (_name: certCfg: - lib.nameValuePair (serviceName certCfg.systemdService) { - after = [ certCfg.ca.systemdService ]; - requires = [ certCfg.ca.systemdService ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig.RuntimeDirectory = serviceName certCfg.systemdService; - # Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix - script = - let - extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains); - chmod = cert: - '' - chown root:${certCfg.group} ${cert} - chmod 640 ${cert} - ''; - in - '' + } + ); + } + # Config for self-signed cert. + { + systemd.services = lib.mapAttrs' ( + _name: certCfg: + lib.nameValuePair (serviceName certCfg.systemdService) { + after = [ certCfg.ca.systemdService ]; + requires = [ certCfg.ca.systemdService ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.RuntimeDirectory = serviceName certCfg.systemdService; + # Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix + script = + let + extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains); + chmod = cert: '' + chown root:${certCfg.group} ${cert} + chmod 640 ${cert} + ''; + in + '' cd $RUNTIME_DIRECTORY # server cert template @@ -421,122 +452,178 @@ in ${chmod certCfg.paths.cert} ''; - postStart = lib.optionalString (certCfg.reloadServices != []) '' - systemctl --no-block try-reload-or-restart ${lib.escapeShellArgs certCfg.reloadServices} - ''; + postStart = lib.optionalString (certCfg.reloadServices != [ ]) '' + systemctl --no-block try-reload-or-restart ${lib.escapeShellArgs certCfg.reloadServices} + ''; - serviceConfig.Type = "oneshot"; - # serviceConfig.User = "nextcloud"; - } - ) cfg.certs.selfsigned; - } - # Config for Let's Encrypt cert. - { - users.users = lib.mkMerge (mapAttrsToList (name: certCfg: { + serviceConfig.Type = "oneshot"; + # serviceConfig.User = "nextcloud"; + } + ) cfg.certs.selfsigned; + } + # Config for Let's Encrypt cert. + { + users.users = lib.mkMerge ( + mapAttrsToList (name: certCfg: { ${certCfg.makeAvailableToUser}.extraGroups = lib.mkIf (!(isNull certCfg.makeAvailableToUser)) [ config.security.acme.defaults.group ]; - }) cfg.certs.letsencrypt); + }) cfg.certs.letsencrypt + ); - security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != {}) true; + security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != { }) true; - security.acme.certs = let - extraDomainsCfg = certCfg: map (name: { - "${name}" = { - email = certCfg.adminEmail; - enableDebugLogs = certCfg.debug; - server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory"; - }; - }) certCfg.extraDomains; - in lib.mkMerge (flatten (mapAttrsToList (name: certCfg: - [{ - "${name}" = { - extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains; - email = certCfg.adminEmail; - enableDebugLogs = certCfg.debug; - server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory"; - } // lib.optionalAttrs (certCfg.dnsProvider != null) { - inherit (certCfg) dnsProvider dnsResolver; - inherit (certCfg) group reloadServices; - credentialsFile = certCfg.credentialsFile; - }; - }] - ++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg) - ) cfg.certs.letsencrypt)); + security.acme.certs = + let + extraDomainsCfg = + certCfg: + map (name: { + "${name}" = { + email = certCfg.adminEmail; + enableDebugLogs = certCfg.debug; + server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory"; + }; + }) certCfg.extraDomains; + in + lib.mkMerge ( + flatten ( + mapAttrsToList ( + name: certCfg: + [ + { + "${name}" = { + extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains; + email = certCfg.adminEmail; + enableDebugLogs = certCfg.debug; + server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory"; + } + // lib.optionalAttrs (certCfg.dnsProvider != null) { + inherit (certCfg) dnsProvider dnsResolver; + inherit (certCfg) group reloadServices; + credentialsFile = certCfg.credentialsFile; + }; + } + ] + ++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg) + ) cfg.certs.letsencrypt + ) + ); - services.nginx = let - extraDomainsCfg = extraDomains: map (name: { - virtualHosts."${name}" = { - # addSSL = true; - enableACME = true; - }; - }) extraDomains; - in lib.mkMerge (flatten (mapAttrsToList (name: certCfg: - lib.optionals (certCfg.dnsProvider == null) ( - [{ + services.nginx = + let + extraDomainsCfg = + extraDomains: + map (name: { virtualHosts."${name}" = { # addSSL = true; enableACME = true; }; - }] - ++ extraDomainsCfg certCfg.extraDomains - )) cfg.certs.letsencrypt)); + }) extraDomains; + in + lib.mkMerge ( + flatten ( + mapAttrsToList ( + name: certCfg: + lib.optionals (certCfg.dnsProvider == null) ( + [ + { + virtualHosts."${name}" = { + # addSSL = true; + enableACME = true; + }; + } + ] + ++ extraDomainsCfg certCfg.extraDomains + ) + ) cfg.certs.letsencrypt + ) + ); - systemd.services = let - extraDomainsCfg = certCfg: flatten (map (name: - lib.optionals (certCfg.additionalEnvironment != {} && certCfg.dnsProvider == null) [{ - "acme-${name}".environment = certCfg.additionalEnvironment; - }] - ++ lib.optionals (certCfg.afterAndWants != [] && certCfg.dnsProvider == null) [{ - "acme-${name}" = { - after = certCfg.afterAndWants; - wants = certCfg.afterAndWants; - }; - }] - ) certCfg.extraDomains); - in lib.mkMerge (flatten (mapAttrsToList (name: certCfg: - lib.optionals (certCfg.additionalEnvironment != {} && certCfg.dnsProvider == null) [{ - "acme-${certCfg.domain}".environment = certCfg.additionalEnvironment; - }] - ++ lib.optionals (certCfg.afterAndWants != [] && certCfg.dnsProvider == null) [{ - "acme-${certCfg.domain}" = { - after = certCfg.afterAndWants; - wants = certCfg.afterAndWants; - }; - }] - ++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg) - ) cfg.certs.letsencrypt)); + systemd.services = + let + extraDomainsCfg = + certCfg: + flatten ( + map ( + name: + lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [ + { + "acme-${name}".environment = certCfg.additionalEnvironment; + } + ] + ++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [ + { + "acme-${name}" = { + after = certCfg.afterAndWants; + wants = certCfg.afterAndWants; + }; + } + ] + ) certCfg.extraDomains + ); + in + lib.mkMerge ( + flatten ( + mapAttrsToList ( + name: certCfg: + lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [ + { + "acme-${certCfg.domain}".environment = certCfg.additionalEnvironment; + } + ] + ++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [ + { + "acme-${certCfg.domain}" = { + after = certCfg.afterAndWants; + wants = certCfg.afterAndWants; + }; + } + ] + ++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg) + ) cfg.certs.letsencrypt + ) + ); - services.prometheus.exporters.node-cert = optionalAttrs (cfg.certs.letsencrypt != {}) { - enable = true; - listenAddress = "127.0.0.1"; - user = "acme"; - paths = let - pathCfg = name: certCfg: + services.prometheus.exporters.node-cert = optionalAttrs (cfg.certs.letsencrypt != { }) { + enable = true; + listenAddress = "127.0.0.1"; + user = "acme"; + paths = + let + pathCfg = + name: certCfg: let - mainDomainPaths = map dirOf [ certCfg.paths.cert certCfg.paths.key ]; + mainDomainPaths = map dirOf [ + certCfg.paths.cert + certCfg.paths.key + ]; # Not sure this will work for all cases. mainPath = dirOf (dirOf certCfg.paths.cert); extraDomainsPath = map (x: "${mainPath}/${x}") certCfg.extraDomains; in - mainDomainPaths ++ extraDomainsPath; + mainDomainPaths ++ extraDomainsPath; in - unique (flatten (mapAttrsToList pathCfg cfg.certs.letsencrypt)); - }; + unique (flatten (mapAttrsToList pathCfg cfg.certs.letsencrypt)); + }; - services.prometheus.scrapeConfigs = let - scrapeCfg = name: certCfg: [{ - job_name = "node-cert-${name}"; - static_configs = [{ - targets = ["127.0.0.1:${toString config.services.prometheus.exporters.node-cert.port}"]; - labels = { - "hostname" = config.networking.hostName; - "domain" = certCfg.domain; - }; - }]; - }]; + services.prometheus.scrapeConfigs = + let + scrapeCfg = name: certCfg: [ + { + job_name = "node-cert-${name}"; + static_configs = [ + { + targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node-cert.port}" ]; + labels = { + "hostname" = config.networking.hostName; + "domain" = certCfg.domain; + }; + } + ]; + } + ]; in - optionals (cfg.certs.letsencrypt != {}) (flatten (mapAttrsToList scrapeCfg cfg.certs.letsencrypt)); - } - ]; + optionals (cfg.certs.letsencrypt != { }) (flatten (mapAttrsToList scrapeCfg cfg.certs.letsencrypt)); + } + ]; } diff --git a/modules/blocks/tinyproxy.nix b/modules/blocks/tinyproxy.nix index 69d8191..e8735d8 100644 --- a/modules/blocks/tinyproxy.nix +++ b/modules/blocks/tinyproxy.nix @@ -1,28 +1,41 @@ # Inspired from https://github.com/NixOS/nixpkgs/pull/231152 but made it so we can have multiple instances. -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: with lib; let cfg = config.shb.tinyproxy; - mkValueStringTinyproxy = with lib; v: - if true == v then "yes" - else if false == v then "no" - else generators.mkValueStringDefault {} v; + mkValueStringTinyproxy = + with lib; + v: + if true == v then + "yes" + else if false == v then + "no" + else + generators.mkValueStringDefault { } v; - mkKeyValueTinyproxy = { - mkValueString ? mkValueStringDefault {} - }: sep: k: v: - if null == v then "" - else "${lib.strings.escape [sep] k}${sep}${mkValueString v}"; + mkKeyValueTinyproxy = + { + mkValueString ? mkValueStringDefault { }, + }: + sep: k: v: + if null == v then "" else "${lib.strings.escape [ sep ] k}${sep}${mkValueString v}"; - settingsFormat = (pkgs.formats.keyValue { + settingsFormat = ( + pkgs.formats.keyValue { mkKeyValue = mkKeyValueTinyproxy { mkValueString = mkValueStringTinyproxy; } " "; - listsAsDuplicateKeys= true; - }); + listsAsDuplicateKeys = true; + } + ); configFile = name: cfg: settingsFormat.generate "tinyproxy-${name}.conf" cfg.settings; in @@ -33,115 +46,124 @@ in options = { enable = mkEnableOption "Tinyproxy daemon"; - package = mkPackageOption pkgs "tinyproxy" {}; + package = mkPackageOption pkgs "tinyproxy" { }; dynamicBindFile = mkOption { description = '' - File holding the IP to bind to. + File holding the IP to bind to. ''; default = ""; }; settings = mkOption { description = '' - Configuration for [tinyproxy](https://tinyproxy.github.io/). + Configuration for [tinyproxy](https://tinyproxy.github.io/). ''; default = { }; - example = literalExpression ''{ - Port 8888; - Listen 127.0.0.1; - Timeout 600; - Allow 127.0.0.1; - Anonymous = ['"Host"' '"Authorization"']; - ReversePath = '"/example/" "http://www.example.com/"'; - }''; - type = types.submodule ({name, ...}: { - freeformType = settingsFormat.type; - options = { - Listen = mkOption { - type = types.str; - default = "127.0.0.1"; - description = '' - Specify which address to listen to. - ''; + example = literalExpression '' + { + Port 8888; + Listen 127.0.0.1; + Timeout 600; + Allow 127.0.0.1; + Anonymous = ['"Host"' '"Authorization"']; + ReversePath = '"/example/" "http://www.example.com/"'; + }''; + type = types.submodule ( + { name, ... }: + { + freeformType = settingsFormat.type; + options = { + Listen = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Specify which address to listen to. + ''; + }; + Port = mkOption { + type = types.int; + default = 8888; + description = '' + Specify which port to listen to. + ''; + }; + Anonymous = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + If an `Anonymous` keyword is present, then anonymous proxying is enabled. The + headers listed with `Anonymous` are allowed through, while all others are denied. + If no Anonymous keyword is present, then all headers are allowed through. You must + include quotes around the headers. + ''; + }; + Filter = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Tinyproxy supports filtering of web sites based on URLs or domains. This option + specifies the location of the file containing the filter rules, one rule per line. + ''; + }; }; - Port = mkOption { - type = types.int; - default = 8888; - description = '' - Specify which port to listen to. - ''; - }; - Anonymous = mkOption { - type = types.listOf types.str; - default = []; - description = '' - If an `Anonymous` keyword is present, then anonymous proxying is enabled. The - headers listed with `Anonymous` are allowed through, while all others are denied. - If no Anonymous keyword is present, then all headers are allowed through. You must - include quotes around the headers. - ''; - }; - Filter = mkOption { - type = types.nullOr types.path; - default = null; - description = '' - Tinyproxy supports filtering of web sites based on URLs or domains. This option - specifies the location of the file containing the filter rules, one rule per line. - ''; - }; - }; - }); + } + ); }; }; }; in - { - shb.tinyproxy = mkOption { - description = "Tinyproxy instances."; - default = {}; - type = types.attrsOf instanceOption; - }; + { + shb.tinyproxy = mkOption { + description = "Tinyproxy instances."; + default = { }; + type = types.attrsOf instanceOption; }; + }; config = { systemd.services = let - instanceConfig = name: c: mkIf c.enable { - "tinyproxy-${name}" = { - description = "TinyProxy daemon - instance ${name}"; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - User = "tinyproxy"; - Group = "tinyproxy"; - Type = "simple"; - ExecStart = "${getExe c.package} -d -c /etc/tinyproxy/${name}.conf"; - ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID"; - KillSignal = "SIGINT"; - TimeoutStopSec = "30s"; - Restart = "on-failure"; - RestartSec = "1s"; - RestartSteps = "3"; - RestartMaxDelaySec = "10s"; - ConfigurationDirectory = "tinyproxy"; + instanceConfig = + name: c: + mkIf c.enable { + "tinyproxy-${name}" = { + description = "TinyProxy daemon - instance ${name}"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "tinyproxy"; + Group = "tinyproxy"; + Type = "simple"; + ExecStart = "${getExe c.package} -d -c /etc/tinyproxy/${name}.conf"; + ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID"; + KillSignal = "SIGINT"; + TimeoutStopSec = "30s"; + Restart = "on-failure"; + RestartSec = "1s"; + RestartSteps = "3"; + RestartMaxDelaySec = "10s"; + ConfigurationDirectory = "tinyproxy"; + }; + preStart = concatStringsSep "\n" ( + [ + "cat ${configFile name c} > /etc/tinyproxy/${name}.conf" + ] + ++ optionals (c.dynamicBindFile != "") [ + "echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf" + "cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf" + ] + ); }; - preStart = concatStringsSep "\n" ([ - "cat ${configFile name c} > /etc/tinyproxy/${name}.conf" - ] ++ optionals (c.dynamicBindFile != "") [ - "echo -n 'Bind ' >> /etc/tinyproxy/${name}.conf" - "cat ${c.dynamicBindFile} >> /etc/tinyproxy/${name}.conf" - ]); }; - }; in - mkMerge (mapAttrsToList instanceConfig cfg); + mkMerge (mapAttrsToList instanceConfig cfg); users.users.tinyproxy = { group = "tinyproxy"; isSystemUser = true; }; - users.groups.tinyproxy = {}; + users.groups.tinyproxy = { }; }; meta.maintainers = with maintainers; [ tcheronneau ]; diff --git a/modules/blocks/vpn.nix b/modules/blocks/vpn.nix index 97c5412..2e3d2ff 100644 --- a/modules/blocks/vpn.nix +++ b/modules/blocks/vpn.nix @@ -1,4 +1,9 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.vpn; @@ -6,197 +11,212 @@ let quoteEach = lib.concatMapStrings (x: ''"${x}"''); nordvpnConfig = - { name - , dev - , authFile - , remoteServerIP - , dependentServices ? [] - }: '' - client - dev ${dev} - proto tcp - remote ${remoteServerIP} 443 - resolv-retry infinite - remote-random - nobind - tun-mtu 1500 - tun-mtu-extra 32 - mssfix 1450 - persist-key - persist-tun - ping 15 - ping-restart 0 - ping-timer-rem - reneg-sec 0 - comp-lzo no + { + name, + dev, + authFile, + remoteServerIP, + dependentServices ? [ ], + }: + '' + client + dev ${dev} + proto tcp + remote ${remoteServerIP} 443 + resolv-retry infinite + remote-random + nobind + tun-mtu 1500 + tun-mtu-extra 32 + mssfix 1450 + persist-key + persist-tun + ping 15 + ping-restart 0 + ping-timer-rem + reneg-sec 0 + comp-lzo no - status /tmp/openvpn/${name}.status + status /tmp/openvpn/${name}.status - remote-cert-tls server + remote-cert-tls server - auth-user-pass ${authFile} - verb 3 - pull - fast-io - cipher AES-256-CBC - auth SHA512 + auth-user-pass ${authFile} + verb 3 + pull + fast-io + cipher AES-256-CBC + auth SHA512 - script-security 2 - route-noexec - route-up ${routeUp name dependentServices}/bin/routeUp.sh - down ${routeDown name dependentServices}/bin/routeDown.sh + script-security 2 + route-noexec + route-up ${routeUp name dependentServices}/bin/routeUp.sh + down ${routeDown name dependentServices}/bin/routeDown.sh - - -----BEGIN CERTIFICATE----- - MIIFCjCCAvKgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA5MQswCQYDVQQGEwJQQTEQ - MA4GA1UEChMHTm9yZFZQTjEYMBYGA1UEAxMPTm9yZFZQTiBSb290IENBMB4XDTE2 - MDEwMTAwMDAwMFoXDTM1MTIzMTIzNTk1OVowOTELMAkGA1UEBhMCUEExEDAOBgNV - BAoTB05vcmRWUE4xGDAWBgNVBAMTD05vcmRWUE4gUm9vdCBDQTCCAiIwDQYJKoZI - hvcNAQEBBQADggIPADCCAgoCggIBAMkr/BYhyo0F2upsIMXwC6QvkZps3NN2/eQF - kfQIS1gql0aejsKsEnmY0Kaon8uZCTXPsRH1gQNgg5D2gixdd1mJUvV3dE3y9FJr - XMoDkXdCGBodvKJyU6lcfEVF6/UxHcbBguZK9UtRHS9eJYm3rpL/5huQMCppX7kU - eQ8dpCwd3iKITqwd1ZudDqsWaU0vqzC2H55IyaZ/5/TnCk31Q1UP6BksbbuRcwOV - skEDsm6YoWDnn/IIzGOYnFJRzQH5jTz3j1QBvRIuQuBuvUkfhx1FEwhwZigrcxXu - MP+QgM54kezgziJUaZcOM2zF3lvrwMvXDMfNeIoJABv9ljw969xQ8czQCU5lMVmA - 37ltv5Ec9U5hZuwk/9QO1Z+d/r6Jx0mlurS8gnCAKJgwa3kyZw6e4FZ8mYL4vpRR - hPdvRTWCMJkeB4yBHyhxUmTRgJHm6YR3D6hcFAc9cQcTEl/I60tMdz33G6m0O42s - Qt/+AR3YCY/RusWVBJB/qNS94EtNtj8iaebCQW1jHAhvGmFILVR9lzD0EzWKHkvy - WEjmUVRgCDd6Ne3eFRNS73gdv/C3l5boYySeu4exkEYVxVRn8DhCxs0MnkMHWFK6 - MyzXCCn+JnWFDYPfDKHvpff/kLDobtPBf+Lbch5wQy9quY27xaj0XwLyjOltpiST - LWae/Q4vAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG - SIb3DQEBDQUAA4ICAQC9fUL2sZPxIN2mD32VeNySTgZlCEdVmlq471o/bDMP4B8g - nQesFRtXY2ZCjs50Jm73B2LViL9qlREmI6vE5IC8IsRBJSV4ce1WYxyXro5rmVg/ - k6a10rlsbK/eg//GHoJxDdXDOokLUSnxt7gk3QKpX6eCdh67p0PuWm/7WUJQxH2S - DxsT9vB/iZriTIEe/ILoOQF0Aqp7AgNCcLcLAmbxXQkXYCCSB35Vp06u+eTWjG0/ - pyS5V14stGtw+fA0DJp5ZJV4eqJ5LqxMlYvEZ/qKTEdoCeaXv2QEmN6dVqjDoTAo - k0t5u4YRXzEVCfXAC3ocplNdtCA72wjFJcSbfif4BSC8bDACTXtnPC7nD0VndZLp - +RiNLeiENhk0oTC+UVdSc+n2nJOzkCK0vYu0Ads4JGIB7g8IB3z2t9ICmsWrgnhd - NdcOe15BincrGA8avQ1cWXsfIKEjbrnEuEk9b5jel6NfHtPKoHc9mDpRdNPISeVa - wDBM1mJChneHt59Nh8Gah74+TM1jBsw4fhJPvoc7Atcg740JErb904mZfkIEmojC - VPhBHVQ9LHBAdM8qFI2kRK0IynOmAZhexlP/aT/kpEsEPyaZQlnBn3An1CRz8h0S - PApL8PytggYKeQmRhl499+6jLxcZ2IegLfqq41dzIjwHwTMplg+1pKIOVojpWA== - -----END CERTIFICATE----- - - key-direction 1 - - # - # 2048 bit OpenVPN static key - # - -----BEGIN OpenVPN Static key V1----- - e685bdaf659a25a200e2b9e39e51ff03 - 0fc72cf1ce07232bd8b2be5e6c670143 - f51e937e670eee09d4f2ea5a6e4e6996 - 5db852c275351b86fc4ca892d78ae002 - d6f70d029bd79c4d1c26cf14e9588033 - cf639f8a74809f29f72b9d58f9b8f5fe - fc7938eade40e9fed6cb92184abb2cc1 - 0eb1a296df243b251df0643d53724cdb - 5a92a1d6cb817804c4a9319b57d53be5 - 80815bcfcb2df55018cc83fc43bc7ff8 - 2d51f9b88364776ee9d12fc85cc7ea5b - 9741c4f598c485316db066d52db4540e - 212e1518a9bd4828219e24b20d88f598 - a196c9de96012090e333519ae18d3509 - 9427e7b372d348d352dc4c85e18cd4b9 - 3f8a56ddb2e64eb67adfc9b337157ff4 - -----END OpenVPN Static key V1----- - + + -----BEGIN CERTIFICATE----- + MIIFCjCCAvKgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA5MQswCQYDVQQGEwJQQTEQ + MA4GA1UEChMHTm9yZFZQTjEYMBYGA1UEAxMPTm9yZFZQTiBSb290IENBMB4XDTE2 + MDEwMTAwMDAwMFoXDTM1MTIzMTIzNTk1OVowOTELMAkGA1UEBhMCUEExEDAOBgNV + BAoTB05vcmRWUE4xGDAWBgNVBAMTD05vcmRWUE4gUm9vdCBDQTCCAiIwDQYJKoZI + hvcNAQEBBQADggIPADCCAgoCggIBAMkr/BYhyo0F2upsIMXwC6QvkZps3NN2/eQF + kfQIS1gql0aejsKsEnmY0Kaon8uZCTXPsRH1gQNgg5D2gixdd1mJUvV3dE3y9FJr + XMoDkXdCGBodvKJyU6lcfEVF6/UxHcbBguZK9UtRHS9eJYm3rpL/5huQMCppX7kU + eQ8dpCwd3iKITqwd1ZudDqsWaU0vqzC2H55IyaZ/5/TnCk31Q1UP6BksbbuRcwOV + skEDsm6YoWDnn/IIzGOYnFJRzQH5jTz3j1QBvRIuQuBuvUkfhx1FEwhwZigrcxXu + MP+QgM54kezgziJUaZcOM2zF3lvrwMvXDMfNeIoJABv9ljw969xQ8czQCU5lMVmA + 37ltv5Ec9U5hZuwk/9QO1Z+d/r6Jx0mlurS8gnCAKJgwa3kyZw6e4FZ8mYL4vpRR + hPdvRTWCMJkeB4yBHyhxUmTRgJHm6YR3D6hcFAc9cQcTEl/I60tMdz33G6m0O42s + Qt/+AR3YCY/RusWVBJB/qNS94EtNtj8iaebCQW1jHAhvGmFILVR9lzD0EzWKHkvy + WEjmUVRgCDd6Ne3eFRNS73gdv/C3l5boYySeu4exkEYVxVRn8DhCxs0MnkMHWFK6 + MyzXCCn+JnWFDYPfDKHvpff/kLDobtPBf+Lbch5wQy9quY27xaj0XwLyjOltpiST + LWae/Q4vAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqG + SIb3DQEBDQUAA4ICAQC9fUL2sZPxIN2mD32VeNySTgZlCEdVmlq471o/bDMP4B8g + nQesFRtXY2ZCjs50Jm73B2LViL9qlREmI6vE5IC8IsRBJSV4ce1WYxyXro5rmVg/ + k6a10rlsbK/eg//GHoJxDdXDOokLUSnxt7gk3QKpX6eCdh67p0PuWm/7WUJQxH2S + DxsT9vB/iZriTIEe/ILoOQF0Aqp7AgNCcLcLAmbxXQkXYCCSB35Vp06u+eTWjG0/ + pyS5V14stGtw+fA0DJp5ZJV4eqJ5LqxMlYvEZ/qKTEdoCeaXv2QEmN6dVqjDoTAo + k0t5u4YRXzEVCfXAC3ocplNdtCA72wjFJcSbfif4BSC8bDACTXtnPC7nD0VndZLp + +RiNLeiENhk0oTC+UVdSc+n2nJOzkCK0vYu0Ads4JGIB7g8IB3z2t9ICmsWrgnhd + NdcOe15BincrGA8avQ1cWXsfIKEjbrnEuEk9b5jel6NfHtPKoHc9mDpRdNPISeVa + wDBM1mJChneHt59Nh8Gah74+TM1jBsw4fhJPvoc7Atcg740JErb904mZfkIEmojC + VPhBHVQ9LHBAdM8qFI2kRK0IynOmAZhexlP/aT/kpEsEPyaZQlnBn3An1CRz8h0S + PApL8PytggYKeQmRhl499+6jLxcZ2IegLfqq41dzIjwHwTMplg+1pKIOVojpWA== + -----END CERTIFICATE----- + + key-direction 1 + + # + # 2048 bit OpenVPN static key + # + -----BEGIN OpenVPN Static key V1----- + e685bdaf659a25a200e2b9e39e51ff03 + 0fc72cf1ce07232bd8b2be5e6c670143 + f51e937e670eee09d4f2ea5a6e4e6996 + 5db852c275351b86fc4ca892d78ae002 + d6f70d029bd79c4d1c26cf14e9588033 + cf639f8a74809f29f72b9d58f9b8f5fe + fc7938eade40e9fed6cb92184abb2cc1 + 0eb1a296df243b251df0643d53724cdb + 5a92a1d6cb817804c4a9319b57d53be5 + 80815bcfcb2df55018cc83fc43bc7ff8 + 2d51f9b88364776ee9d12fc85cc7ea5b + 9741c4f598c485316db066d52db4540e + 212e1518a9bd4828219e24b20d88f598 + a196c9de96012090e333519ae18d3509 + 9427e7b372d348d352dc4c85e18cd4b9 + 3f8a56ddb2e64eb67adfc9b337157ff4 + -----END OpenVPN Static key V1----- + ''; - routeUp = name: dependentServices: pkgs.writeShellApplication { - name = "routeUp.sh"; + routeUp = + name: dependentServices: + pkgs.writeShellApplication { + name = "routeUp.sh"; - runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools ]; + runtimeInputs = [ + pkgs.iproute2 + pkgs.systemd + pkgs.nettools + ]; - text = '' - echo "Running route-up..." + text = '' + echo "Running route-up..." - echo "dev=''${dev:?}" - echo "ifconfig_local=''${ifconfig_local:?}" - echo "route_vpn_gateway=''${route_vpn_gateway:?}" + echo "dev=''${dev:?}" + echo "ifconfig_local=''${ifconfig_local:?}" + echo "route_vpn_gateway=''${route_vpn_gateway:?}" - set -x + set -x - ip rule - ip rule add from "''${ifconfig_local:?}/32" table ${name} - ip rule add to "''${route_vpn_gateway:?}/32" table ${name} - ip rule + ip rule + ip rule add from "''${ifconfig_local:?}/32" table ${name} + ip rule add to "''${route_vpn_gateway:?}/32" table ${name} + ip rule - ip route list table ${name} || : - retVal=$? - if [ $retVal -eq 2 ]; then - echo "table is empty" - elif [ $retVal -ne 0 ]; then - exit 1 - fi - ip route add default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} - ip route flush cache - ip route list table ${name} || : - retVal=$? - if [ $retVal -eq 2 ]; then - echo "table is empty" - elif [ $retVal -ne 0 ]; then - exit 1 - fi + ip route list table ${name} || : + retVal=$? + if [ $retVal -eq 2 ]; then + echo "table is empty" + elif [ $retVal -ne 0 ]; then + exit 1 + fi + ip route add default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} + ip route flush cache + ip route list table ${name} || : + retVal=$? + if [ $retVal -eq 2 ]; then + echo "table is empty" + elif [ $retVal -ne 0 ]; then + exit 1 + fi - echo "''${ifconfig_local:?}" > /run/openvpn/${name}/ifconfig_local + echo "''${ifconfig_local:?}" > /run/openvpn/${name}/ifconfig_local - dependencies=(${quoteEach dependentServices}) - for i in "''${dependencies[@]}"; do - systemctl restart "$i" || : - done + dependencies=(${quoteEach dependentServices}) + for i in "''${dependencies[@]}"; do + systemctl restart "$i" || : + done - echo "Running route-up DONE" - ''; - }; + echo "Running route-up DONE" + ''; + }; - routeDown = name: dependentServices: pkgs.writeShellApplication { - name = "routeDown.sh"; + routeDown = + name: dependentServices: + pkgs.writeShellApplication { + name = "routeDown.sh"; - runtimeInputs = [ pkgs.iproute2 pkgs.systemd pkgs.nettools pkgs.coreutils ]; + runtimeInputs = [ + pkgs.iproute2 + pkgs.systemd + pkgs.nettools + pkgs.coreutils + ]; - text = '' - echo "Running route-down..." + text = '' + echo "Running route-down..." - echo "dev=''${dev:?}" - echo "ifconfig_local=''${ifconfig_local:?}" - echo "route_vpn_gateway=''${route_vpn_gateway:?}" + echo "dev=''${dev:?}" + echo "ifconfig_local=''${ifconfig_local:?}" + echo "route_vpn_gateway=''${route_vpn_gateway:?}" - set -x + set -x - ip rule - ip rule del from "''${ifconfig_local:?}/32" table ${name} - ip rule del to "''${route_vpn_gateway:?}/32" table ${name} - ip rule + ip rule + ip rule del from "''${ifconfig_local:?}/32" table ${name} + ip rule del to "''${route_vpn_gateway:?}/32" table ${name} + ip rule - # This will probably fail because the dev is already gone. - ip route list table ${name} || : - retVal=$? - if [ $retVal -eq 2 ]; then - echo "table is empty" - elif [ $retVal -ne 0 ]; then - exit 1 - fi - ip route del default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} || : - ip route flush cache - ip route list table ${name} || : - retVal=$? - if [ $retVal -eq 2 ]; then - echo "table is empty" - elif [ $retVal -ne 0 ]; then - exit 1 - fi + # This will probably fail because the dev is already gone. + ip route list table ${name} || : + retVal=$? + if [ $retVal -eq 2 ]; then + echo "table is empty" + elif [ $retVal -ne 0 ]; then + exit 1 + fi + ip route del default via "''${route_vpn_gateway:?}" dev "''${dev:?}" table ${name} || : + ip route flush cache + ip route list table ${name} || : + retVal=$? + if [ $retVal -eq 2 ]; then + echo "table is empty" + elif [ $retVal -ne 0 ]; then + exit 1 + fi - rm /run/openvpn/${name}/ifconfig_local + rm /run/openvpn/${name}/ifconfig_local - dependencies=(${quoteEach dependentServices}) - for i in "''${dependencies[@]}"; do - systemctl stop "$i" || : - done + dependencies=(${quoteEach dependentServices}) + for i in "''${dependencies[@]}"; do + systemctl stop "$i" || : + done - echo "Running route-down DONE" - ''; - }; + echo "Running route-down DONE" + ''; + }; in { options = @@ -205,7 +225,7 @@ in options = { enable = lib.mkEnableOption "OpenVPN config"; - package = lib.mkPackageOption pkgs "openvpn" {}; + package = lib.mkPackageOption pkgs "openvpn" { }; provider = lib.mkOption { description = "VPN provider, if given uses ready-made configuration."; @@ -243,68 +263,76 @@ in }; }; in - { - shb.vpn = lib.mkOption { - description = "OpenVPN instances."; - default = {}; - type = lib.types.attrsOf instanceOption; - }; + { + shb.vpn = lib.mkOption { + description = "OpenVPN instances."; + default = { }; + type = lib.types.attrsOf instanceOption; }; + }; config = { services.openvpn.servers = let - instanceConfig = name: c: lib.mkIf c.enable { - ${name} = { - autoStart = true; + instanceConfig = + name: c: + lib.mkIf c.enable { + ${name} = { + autoStart = true; - up = "mkdir -p /run/openvpn/${name}"; + up = "mkdir -p /run/openvpn/${name}"; - config = nordvpnConfig { - inherit name; - inherit (c) dev remoteServerIP authFile; - dependentServices = lib.optional (c.proxyPort != null) "tinyproxy-${name}.service"; + config = nordvpnConfig { + inherit name; + inherit (c) dev remoteServerIP authFile; + dependentServices = lib.optional (c.proxyPort != null) "tinyproxy-${name}.service"; + }; }; }; - }; in - lib.mkMerge (lib.mapAttrsToList instanceConfig cfg); + lib.mkMerge (lib.mapAttrsToList instanceConfig cfg); - systemd.tmpfiles.rules = map (name: - "d /tmp/openvpn/${name}.status 0700 root root" - ) (lib.attrNames cfg); + systemd.tmpfiles.rules = map (name: "d /tmp/openvpn/${name}.status 0700 root root") ( + lib.attrNames cfg + ); networking.iproute2.enable = true; - networking.iproute2.rttablesExtraConfig = - lib.concatStringsSep "\n" (lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg); + networking.iproute2.rttablesExtraConfig = lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: c: "${toString c.routingNumber} ${name}") cfg + ); shb.tinyproxy = let - instanceConfig = name: c: lib.mkIf (c.enable && c.proxyPort != null) { - ${name} = { - enable = true; - # package = pkgs.tinyproxy.overrideAttrs (old: { - # withDebug = false; - # patches = old.patches ++ [ - # (pkgs.fetchpatch { - # name = ""; - # url = "https://github.com/tinyproxy/tinyproxy/pull/494/commits/2532ba09896352b31f3538d7819daa1fc3f829f1.patch"; - # sha256 = "sha256-Q0MkHnttW8tH3+hoCt9ACjHjmmZQgF6pC/menIrU0Co="; - # }) - # ]; - # }); - dynamicBindFile = "/run/openvpn/${name}/ifconfig_local"; - settings = { - Port = c.proxyPort; - Listen = "127.0.0.1"; - Syslog = "On"; - LogLevel = "Info"; - Allow = [ "127.0.0.1" "::1" ]; - ViaProxyName = ''"tinyproxy"''; + instanceConfig = + name: c: + lib.mkIf (c.enable && c.proxyPort != null) { + ${name} = { + enable = true; + # package = pkgs.tinyproxy.overrideAttrs (old: { + # withDebug = false; + # patches = old.patches ++ [ + # (pkgs.fetchpatch { + # name = ""; + # url = "https://github.com/tinyproxy/tinyproxy/pull/494/commits/2532ba09896352b31f3538d7819daa1fc3f829f1.patch"; + # sha256 = "sha256-Q0MkHnttW8tH3+hoCt9ACjHjmmZQgF6pC/menIrU0Co="; + # }) + # ]; + # }); + dynamicBindFile = "/run/openvpn/${name}/ifconfig_local"; + settings = { + Port = c.proxyPort; + Listen = "127.0.0.1"; + Syslog = "On"; + LogLevel = "Info"; + Allow = [ + "127.0.0.1" + "::1" + ]; + ViaProxyName = ''"tinyproxy"''; + }; }; }; - }; in - lib.mkMerge (lib.mapAttrsToList instanceConfig cfg); + lib.mkMerge (lib.mapAttrsToList instanceConfig cfg); }; } diff --git a/modules/blocks/zfs.nix b/modules/blocks/zfs.nix index f3e76d0..6f4126e 100644 --- a/modules/blocks/zfs.nix +++ b/modules/blocks/zfs.nix @@ -1,4 +1,9 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.zfs; @@ -21,38 +26,43 @@ in This block implements the following contracts: - mount ''; - default = {}; + default = { }; example = lib.literalExpression '' shb.zfs."safe/postgresql".path = "/var/lib/postgresql"; ''; - type = lib.types.attrsOf (lib.types.submodule { - options = { - enable = lib.mkEnableOption "shb.zfs.datasets"; + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + enable = lib.mkEnableOption "shb.zfs.datasets"; - poolName = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "ZFS pool name this dataset should be created on. Overrides the defaultPoolName."; - }; + poolName = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "ZFS pool name this dataset should be created on. Overrides the defaultPoolName."; + }; - path = lib.mkOption { - type = lib.types.str; - description = "Path this dataset should be mounted on."; + path = lib.mkOption { + type = lib.types.str; + description = "Path this dataset should be mounted on."; + }; }; - }; - }); + } + ); }; }; config = { assertions = [ { - assertion = lib.any (x: x.poolName == null) (lib.mapAttrsToList (n: v: v) cfg.datasets) -> cfg.defaultPoolName != null; + assertion = + lib.any (x: x.poolName == null) (lib.mapAttrsToList (n: v: v) cfg.datasets) + -> cfg.defaultPoolName != null; message = "Cannot have both datasets.poolName and defaultPoolName set to null"; } ]; - system.activationScripts = lib.mapAttrs' (name: cfg': + system.activationScripts = lib.mapAttrs' ( + name: cfg': let dataset = (if cfg'.poolName != null then cfg'.poolName else cfg.defaultPoolName) + "/" + name; in @@ -68,6 +78,7 @@ in mountpoint=${cfg'.path} \ ${dataset} ''; - }) cfg.datasets; + } + ) cfg.datasets; }; } diff --git a/modules/contracts/backup.nix b/modules/contracts/backup.nix index 7f55bf6..bd3a7ac 100644 --- a/modules/contracts/backup.nix +++ b/modules/contracts/backup.nix @@ -1,22 +1,35 @@ { lib, ... }: let - inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString; - inherit (lib.types) listOf nonEmptyListOf submodule str; + inherit (lib) + concatStringsSep + literalMD + mkOption + optionalAttrs + optionalString + ; + inherit (lib.types) + listOf + nonEmptyListOf + submodule + str + ; inherit (lib.shb) anyNotNull; in { mkRequest = - { user ? "", + { + user ? "", userText ? null, sourceDirectories ? [ "/var/lib/example" ], sourceDirectoriesText ? null, - excludePatterns ? [], + excludePatterns ? [ ], excludePatternsText ? null, - beforeBackup ? [], + beforeBackup ? [ ], beforeBackupText ? null, - afterBackup ? [], + afterBackup ? [ ], afterBackupText ? null, - }: mkOption { + }: + mkOption { description = '' Request part of the backup contract. @@ -31,72 +44,102 @@ in }; }; - defaultText = optionalString (anyNotNull [ - userText - sourceDirectoriesText - excludePatternsText - beforeBackupText - afterBackupText - ]) (literalMD '' - { - user = ${if userText != null then userText else user}; - sourceDirectories = ${if sourceDirectoriesText != null then sourceDirectoriesText else "[ " + concatStringsSep " " sourceDirectories + " ]"}; - excludePatterns = ${if excludePatternsText != null then excludePatternsText else "[ " + concatStringsSep " " excludePatterns + " ]"}; - hooks.beforeBackup = ${if beforeBackupText != null then beforeBackupText else "[ " + concatStringsSep " " beforeBackup + " ]"}; - hooks.afterBackup = ${if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]"}; - }; - ''); + defaultText = + optionalString + (anyNotNull [ + userText + sourceDirectoriesText + excludePatternsText + beforeBackupText + afterBackupText + ]) + (literalMD '' + { + user = ${if userText != null then userText else user}; + sourceDirectories = ${ + if sourceDirectoriesText != null then + sourceDirectoriesText + else + "[ " + concatStringsSep " " sourceDirectories + " ]" + }; + excludePatterns = ${ + if excludePatternsText != null then + excludePatternsText + else + "[ " + concatStringsSep " " excludePatterns + " ]" + }; + hooks.beforeBackup = ${ + if beforeBackupText != null then + beforeBackupText + else + "[ " + concatStringsSep " " beforeBackup + " ]" + }; + hooks.afterBackup = ${ + if afterBackupText != null then afterBackupText else "[ " + concatStringsSep " " afterBackup + " ]" + }; + }; + ''); type = submodule { options = { - user = mkOption { - description = '' - Unix user doing the backups. - ''; - type = str; - example = "vaultwarden"; - default = user; - } // optionalAttrs (userText != null) { - defaultText = literalMD userText; - }; + user = + mkOption { + description = '' + Unix user doing the backups. + ''; + type = str; + example = "vaultwarden"; + default = user; + } + // optionalAttrs (userText != null) { + defaultText = literalMD userText; + }; - sourceDirectories = mkOption { - description = "Directories to backup."; - type = nonEmptyListOf str; - example = "/var/lib/vaultwarden"; - default = sourceDirectories; - } // optionalAttrs (sourceDirectoriesText != null) { - defaultText = literalMD sourceDirectoriesText; - }; + sourceDirectories = + mkOption { + description = "Directories to backup."; + type = nonEmptyListOf str; + example = "/var/lib/vaultwarden"; + default = sourceDirectories; + } + // optionalAttrs (sourceDirectoriesText != null) { + defaultText = literalMD sourceDirectoriesText; + }; - excludePatterns = mkOption { - description = "File patterns to exclude."; - type = listOf str; - default = excludePatterns; - } // optionalAttrs (excludePatternsText != null) { - defaultText = literalMD excludePatternsText; - }; + excludePatterns = + mkOption { + description = "File patterns to exclude."; + type = listOf str; + default = excludePatterns; + } + // optionalAttrs (excludePatternsText != null) { + defaultText = literalMD excludePatternsText; + }; hooks = mkOption { description = "Hooks to run around the backup."; - default = {}; + default = { }; type = submodule { options = { - beforeBackup = mkOption { - description = "Hooks to run before backup."; - type = listOf str; - default = beforeBackup; - } // optionalAttrs (beforeBackupText != null) { - defaultText = literalMD beforeBackupText; - }; + beforeBackup = + mkOption { + description = "Hooks to run before backup."; + type = listOf str; + default = beforeBackup; + } + // optionalAttrs (beforeBackupText != null) { + defaultText = literalMD beforeBackupText; + }; - afterBackup = mkOption { - description = "Hooks to run after backup."; - type = listOf str; - default = afterBackup; - } // optionalAttrs (afterBackupText != null) { - defaultText = literalMD afterBackupText; - }; + afterBackup = + mkOption { + description = "Hooks to run after backup."; + type = listOf str; + default = afterBackup; + } + // optionalAttrs (afterBackupText != null) { + defaultText = literalMD afterBackupText; + }; }; }; }; @@ -104,70 +147,79 @@ in }; }; - mkResult = { - restoreScript ? "restore", - restoreScriptText ? null, - backupService ? "backup.service", - backupServiceText ? null, - }: mkOption { - description = '' - Result part of the backup contract. - - Options set by the provider module that indicates the name of the backup and restor scripts. - ''; - default = { - inherit restoreScript backupService; - }; - - defaultText = optionalString (anyNotNull [ - restoreScriptText - backupServiceText - ]) (literalMD '' + mkResult = { - restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript}; - backupService = ${if backupServiceText != null then backupServiceText else backupService}; - } - ''); + restoreScript ? "restore", + restoreScriptText ? null, + backupService ? "backup.service", + backupServiceText ? null, + }: + mkOption { + description = '' + Result part of the backup contract. - type = submodule { - options = { - restoreScript = mkOption { - description = '' - Name of script that can restore the database. - One can then list snapshots with: + Options set by the provider module that indicates the name of the backup and restor scripts. + ''; + default = { + inherit restoreScript backupService; + }; - ```bash - $ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots - ``` + defaultText = + optionalString + (anyNotNull [ + restoreScriptText + backupServiceText + ]) + (literalMD '' + { + restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript}; + backupService = ${if backupServiceText != null then backupServiceText else backupService}; + } + ''); - And restore the database with: + type = submodule { + options = { + restoreScript = + mkOption { + description = '' + Name of script that can restore the database. + One can then list snapshots with: - ```bash - $ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest - ``` - ''; - type = str; - default = restoreScript; - } // optionalAttrs (restoreScriptText != null) { - defaultText = literalMD restoreScriptText; - }; + ```bash + $ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots + ``` - backupService = mkOption { - description = '' - Name of service backing up the database. + And restore the database with: - This script can be ran manually to backup the database: + ```bash + $ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest + ``` + ''; + type = str; + default = restoreScript; + } + // optionalAttrs (restoreScriptText != null) { + defaultText = literalMD restoreScriptText; + }; - ```bash - $ systemctl start ${if backupServiceText != null then backupServiceText else backupService} - ``` - ''; - type = str; - default = backupService; - } // optionalAttrs (backupServiceText != null) { - defaultText = literalMD backupServiceText; + backupService = + mkOption { + description = '' + Name of service backing up the database. + + This script can be ran manually to backup the database: + + ```bash + $ systemctl start ${if backupServiceText != null then backupServiceText else backupService} + ``` + ''; + type = str; + default = backupService; + } + // optionalAttrs (backupServiceText != null) { + defaultText = literalMD backupServiceText; + }; }; }; }; - }; } diff --git a/modules/contracts/backup/dummyModule.nix b/modules/contracts/backup/dummyModule.nix index 004f44f..95ab466 100644 --- a/modules/contracts/backup/dummyModule.nix +++ b/modules/contracts/backup/dummyModule.nix @@ -1,6 +1,6 @@ { pkgs, lib, ... }: let - contracts = pkgs.callPackage ../. {}; + contracts = pkgs.callPackage ../. { }; inherit (lib) mkOption; inherit (lib.types) submodule; diff --git a/modules/contracts/backup/test.nix b/modules/contracts/backup/test.nix index b3748a9..28deaee 100644 --- a/modules/contracts/backup/test.nix +++ b/modules/contracts/backup/test.nix @@ -1,10 +1,17 @@ { pkgs, lib }: let - inherit (lib) concatMapStringsSep getAttrFromPath mkIf optionalAttrs setAttrByPath; + inherit (lib) + concatMapStringsSep + getAttrFromPath + mkIf + optionalAttrs + setAttrByPath + ; in -{ name, +{ + name, providerRoot, - modules ? [], + modules ? [ ], username ? "me", sourceDirectories ? [ "/opt/files/A" @@ -12,107 +19,115 @@ in ], settings, # { repository, config } -> attrset extraConfig ? null, # { username, config } -> attrset -}: lib.shb.runNixOSTest { +}: +lib.shb.runNixOSTest { inherit name; - nodes.machine = { config, ... }: { - imports = [ lib.shb.baseImports ] ++ modules; + nodes.machine = + { config, ... }: + { + imports = [ lib.shb.baseImports ] ++ modules; - config = lib.mkMerge [ - (setAttrByPath providerRoot { - request = { - inherit sourceDirectories; - user = username; - }; - settings = settings { - inherit config; - repository = "/opt/repos/${name}"; - }; - }) - (mkIf (username != "root") { - users.users.${username} = { - isSystemUser = true; - extraGroups = [ "sudoers" ]; - group = "root"; - }; - }) - (optionalAttrs (extraConfig != null) (extraConfig { inherit username config; })) - ]; - }; + config = lib.mkMerge [ + (setAttrByPath providerRoot { + request = { + inherit sourceDirectories; + user = username; + }; + settings = settings { + inherit config; + repository = "/opt/repos/${name}"; + }; + }) + (mkIf (username != "root") { + users.users.${username} = { + isSystemUser = true; + extraGroups = [ "sudoers" ]; + group = "root"; + }; + }) + (optionalAttrs (extraConfig != null) (extraConfig { + inherit username config; + })) + ]; + }; extraPythonPackages = p: [ p.dictdiffer ]; skipTypeCheck = true; - testScript = { nodes, ... }: let - provider = (getAttrFromPath providerRoot nodes.machine).result; - in '' - from dictdiffer import diff + testScript = + { nodes, ... }: + let + provider = (getAttrFromPath providerRoot nodes.machine).result; + in + '' + from dictdiffer import diff - username = "${username}" - sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ] + username = "${username}" + sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ] - def list_files(dir): - files_and_content = {} + def list_files(dir): + files_and_content = {} - files = machine.succeed(f"""find {dir} -type f""").split("\n")[:-1] + files = machine.succeed(f"""find {dir} -type f""").split("\n")[:-1] - for f in files: - content = machine.succeed(f"""cat {f}""").strip() - files_and_content[f] = content + for f in files: + content = machine.succeed(f"""cat {f}""").strip() + files_and_content[f] = content - return files_and_content + return files_and_content - def assert_files(dir, files): - result = list(diff(list_files(dir), files)) - if len(result) > 0: - raise Exception("Unexpected files:", result) + def assert_files(dir, files): + result = list(diff(list_files(dir), files)) + if len(result) > 0: + raise Exception("Unexpected files:", result) - with subtest("Create initial content"): - for path in sourceDirectories: - machine.succeed(f""" - mkdir -p {path} - echo repo_fileA_1 > {path}/fileA - echo repo_fileB_1 > {path}/fileB + with subtest("Create initial content"): + for path in sourceDirectories: + machine.succeed(f""" + mkdir -p {path} + echo repo_fileA_1 > {path}/fileA + echo repo_fileB_1 > {path}/fileB - chown {username}: -R {path} - chmod go-rwx -R {path} - """) - - for path in sourceDirectories: - assert_files(path, { - f'{path}/fileA': 'repo_fileA_1', - f'{path}/fileB': 'repo_fileB_1', - }) - - with subtest("First backup in repo"): - print(machine.succeed("systemctl cat ${provider.backupService}")) - machine.succeed("systemctl start ${provider.backupService}") - - with subtest("New content"): - for path in sourceDirectories: - machine.succeed(f""" - echo repo_fileA_2 > {path}/fileA - echo repo_fileB_2 > {path}/fileB + chown {username}: -R {path} + chmod go-rwx -R {path} """) - assert_files(path, { - f'{path}/fileA': 'repo_fileA_2', - f'{path}/fileB': 'repo_fileB_2', - }) + for path in sourceDirectories: + assert_files(path, { + f'{path}/fileA': 'repo_fileA_1', + f'{path}/fileB': 'repo_fileB_1', + }) - with subtest("Delete content"): - for path in sourceDirectories: - machine.succeed(f"""rm -r {path}/*""") + with subtest("First backup in repo"): + print(machine.succeed("systemctl cat ${provider.backupService}")) + machine.succeed("systemctl start ${provider.backupService}") - assert_files(path, {}) + with subtest("New content"): + for path in sourceDirectories: + machine.succeed(f""" + echo repo_fileA_2 > {path}/fileA + echo repo_fileB_2 > {path}/fileB + """) - with subtest("Restore initial content from repo"): - machine.succeed("""${provider.restoreScript} restore latest""") + assert_files(path, { + f'{path}/fileA': 'repo_fileA_2', + f'{path}/fileB': 'repo_fileB_2', + }) - for path in sourceDirectories: - assert_files(path, { - f'{path}/fileA': 'repo_fileA_1', - f'{path}/fileB': 'repo_fileB_1', - }) + with subtest("Delete content"): + for path in sourceDirectories: + machine.succeed(f"""rm -r {path}/*""") + + assert_files(path, {}) + + with subtest("Restore initial content from repo"): + machine.succeed("""${provider.restoreScript} restore latest""") + + for path in sourceDirectories: + assert_files(path, { + f'{path}/fileA': 'repo_fileA_1', + f'{path}/fileB': 'repo_fileB_1', + }) ''; } diff --git a/modules/contracts/databasebackup.nix b/modules/contracts/databasebackup.nix index fc7edca..298da2a 100644 --- a/modules/contracts/databasebackup.nix +++ b/modules/contracts/databasebackup.nix @@ -1,12 +1,19 @@ { lib, ... }: let - inherit (lib) mkOption literalExpression literalMD optionalAttrs optionalString; + inherit (lib) + mkOption + literalExpression + literalMD + optionalAttrs + optionalString + ; inherit (lib.types) submodule str; inherit (lib.shb) anyNotNull; in { mkRequest = - { user ? "root", + { + user ? "root", userText ? null, backupName ? "dump", backupNameText ? null, @@ -14,7 +21,8 @@ in backupCmdText ? null, restoreCmd ? "", restoreCmdText ? null, - }: mkOption { + }: + mkOption { description = '' Request part of the backup contract. @@ -23,137 +31,161 @@ in ''; default = { - inherit user backupName backupCmd restoreCmd; + inherit + user + backupName + backupCmd + restoreCmd + ; }; - defaultText = optionalString (anyNotNull [ - userText - backupNameText - backupCmdText - restoreCmdText - ]) (literalMD '' - { - user = ${if userText != null then userText else user}; - backupName = ${if backupNameText != null then backupNameText else backupName}; - backupCmd = ${if backupCmdText != null then backupCmdText else backupCmd}; - restoreCmd = ${if restoreCmdText != null then restoreCmdText else restoreCmd}; - } - ''); + defaultText = + optionalString + (anyNotNull [ + userText + backupNameText + backupCmdText + restoreCmdText + ]) + (literalMD '' + { + user = ${if userText != null then userText else user}; + backupName = ${if backupNameText != null then backupNameText else backupName}; + backupCmd = ${if backupCmdText != null then backupCmdText else backupCmd}; + restoreCmd = ${if restoreCmdText != null then restoreCmdText else restoreCmd}; + } + ''); type = submodule { options = { - user = mkOption { - description = '' - Unix user doing the backups. + user = + mkOption { + description = '' + Unix user doing the backups. - This should be an admin user having access to all databases. - ''; - type = str; - example = "postgres"; - default = user; - } // optionalAttrs (userText != null) { - defaultText = literalMD userText; - }; + This should be an admin user having access to all databases. + ''; + type = str; + example = "postgres"; + default = user; + } + // optionalAttrs (userText != null) { + defaultText = literalMD userText; + }; - backupName = mkOption { - description = "Name of the backup in the repository."; - type = str; - example = "postgresql.sql"; - default = backupName; - } // optionalAttrs (backupNameText != null) { - defaultText = literalMD backupNameText; - }; + backupName = + mkOption { + description = "Name of the backup in the repository."; + type = str; + example = "postgresql.sql"; + default = backupName; + } + // optionalAttrs (backupNameText != null) { + defaultText = literalMD backupNameText; + }; - backupCmd = mkOption { - description = "Command that produces the database dump on stdout."; - type = str; - example = literalExpression '' - ''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable - ''; - default = backupCmd; - } // optionalAttrs (backupCmdText != null) { - defaultText = literalMD backupCmdText; - }; + backupCmd = + mkOption { + description = "Command that produces the database dump on stdout."; + type = str; + example = literalExpression '' + ''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable + ''; + default = backupCmd; + } + // optionalAttrs (backupCmdText != null) { + defaultText = literalMD backupCmdText; + }; - restoreCmd = mkOption { - description = "Command that reads the database dump on stdin and restores the database."; - type = str; - example = literalExpression '' - ''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres - ''; - default = restoreCmd; - } // optionalAttrs (restoreCmdText != null) { - defaultText = literalMD restoreCmdText; - }; + restoreCmd = + mkOption { + description = "Command that reads the database dump on stdin and restores the database."; + type = str; + example = literalExpression '' + ''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres + ''; + default = restoreCmd; + } + // optionalAttrs (restoreCmdText != null) { + defaultText = literalMD restoreCmdText; + }; }; }; }; - - mkResult = { - restoreScript ? "restore", - restoreScriptText ? null, - backupService ? "backup.service", - backupServiceText ? null, - }: mkOption { - description = '' - Result part of the backup contract. - - Options set by the provider module that indicates the name of the backup and restor scripts. - ''; - default = { - inherit restoreScript backupService; - }; - - defaultText = optionalString (anyNotNull [ - restoreScriptText - backupServiceText - ]) (literalMD '' + mkResult = { - restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript}; - backupService = ${if backupServiceText != null then backupServiceText else backupService}; - } - ''); + restoreScript ? "restore", + restoreScriptText ? null, + backupService ? "backup.service", + backupServiceText ? null, + }: + mkOption { + description = '' + Result part of the backup contract. - type = submodule { - options = { - restoreScript = mkOption { - description = '' - Name of script that can restore the database. - One can then list snapshots with: + Options set by the provider module that indicates the name of the backup and restor scripts. + ''; + default = { + inherit restoreScript backupService; + }; - ```bash - $ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots - ``` + defaultText = + optionalString + (anyNotNull [ + restoreScriptText + backupServiceText + ]) + (literalMD '' + { + restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript}; + backupService = ${if backupServiceText != null then backupServiceText else backupService}; + } + ''); - And restore the database with: + type = submodule { + options = { + restoreScript = + mkOption { + description = '' + Name of script that can restore the database. + One can then list snapshots with: - ```bash - $ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest - ``` - ''; - type = str; - default = restoreScript; - } // optionalAttrs (restoreScriptText != null) { - defaultText = literalMD restoreScriptText; - }; + ```bash + $ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots + ``` - backupService = mkOption { - description = '' - Name of service backing up the database. + And restore the database with: - This script can be ran manually to backup the database: + ```bash + $ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest + ``` + ''; + type = str; + default = restoreScript; + } + // optionalAttrs (restoreScriptText != null) { + defaultText = literalMD restoreScriptText; + }; - ```bash - $ systemctl start ${if backupServiceText != null then backupServiceText else backupService} - ``` - ''; - type = str; - default = backupService; - } // optionalAttrs (backupServiceText != null) { - defaultText = literalMD backupServiceText; + backupService = + mkOption { + description = '' + Name of service backing up the database. + + This script can be ran manually to backup the database: + + ```bash + $ systemctl start ${if backupServiceText != null then backupServiceText else backupService} + ``` + ''; + type = str; + default = backupService; + } + // optionalAttrs (backupServiceText != null) { + defaultText = literalMD backupServiceText; + }; }; }; }; - }; } diff --git a/modules/contracts/databasebackup/dummyModule.nix b/modules/contracts/databasebackup/dummyModule.nix index 05f4bac..2ce18ae 100644 --- a/modules/contracts/databasebackup/dummyModule.nix +++ b/modules/contracts/databasebackup/dummyModule.nix @@ -1,6 +1,6 @@ { pkgs, lib, ... }: let - contracts = pkgs.callPackage ../. {}; + contracts = pkgs.callPackage ../. { }; inherit (lib) mkOption; inherit (lib.types) submodule; diff --git a/modules/contracts/databasebackup/test.nix b/modules/contracts/databasebackup/test.nix index ecb9e83..53f41a0 100644 --- a/modules/contracts/databasebackup/test.nix +++ b/modules/contracts/databasebackup/test.nix @@ -1,84 +1,98 @@ { pkgs, lib }: let - inherit (lib) getAttrFromPath mkIf optionalAttrs setAttrByPath; + inherit (lib) + getAttrFromPath + mkIf + optionalAttrs + setAttrByPath + ; in -{ name, +{ + name, requesterRoot, providerRoot, extraConfig ? null, # { config, database } -> attrset - modules ? [], + modules ? [ ], database ? "me", settings, # { repository, config } -> attrset -}: lib.shb.runNixOSTest { +}: +lib.shb.runNixOSTest { inherit name; - nodes.machine = { config, ... }: { - imports = [ lib.shb.baseImports ] ++ modules; - config = lib.mkMerge [ - (setAttrByPath providerRoot { - request = (getAttrFromPath requesterRoot config).request; - settings = settings { - inherit config; - repository = "/opt/repos/database"; - }; - }) - (mkIf (database != "root") { - users.users.${database} = { - isSystemUser = true; - extraGroups = [ "sudoers" ]; - group = "root"; - }; - }) - (optionalAttrs (extraConfig != null) (extraConfig { inherit config database; })) - ]; - }; + nodes.machine = + { config, ... }: + { + imports = [ lib.shb.baseImports ] ++ modules; + config = lib.mkMerge [ + (setAttrByPath providerRoot { + request = (getAttrFromPath requesterRoot config).request; + settings = settings { + inherit config; + repository = "/opt/repos/database"; + }; + }) + (mkIf (database != "root") { + users.users.${database} = { + isSystemUser = true; + extraGroups = [ "sudoers" ]; + group = "root"; + }; + }) + (optionalAttrs (extraConfig != null) (extraConfig { + inherit config database; + })) + ]; + }; - testScript = { nodes, ... }: let - provider = getAttrFromPath providerRoot nodes.machine; - in '' - import csv + testScript = + { nodes, ... }: + let + provider = getAttrFromPath providerRoot nodes.machine; + in + '' + import csv - start_all() - machine.wait_for_unit("postgresql.service") - machine.wait_for_open_port(5432) + start_all() + machine.wait_for_unit("postgresql.service") + machine.wait_for_open_port(5432) - def peer_cmd(cmd, db="me"): - return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db) + def peer_cmd(cmd, db="me"): + return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db) - def query(query): - res = machine.succeed(peer_cmd(query)) - return list(dict(l) for l in csv.DictReader(res.splitlines())) + def query(query): + res = machine.succeed(peer_cmd(query)) + return list(dict(l) for l in csv.DictReader(res.splitlines())) - def cmp_tables(a, b): - for i in range(max(len(a), len(b))): - diff = set(a[i]) ^ set(b[i]) - if len(diff) > 0: - raise Exception(i, diff) + def cmp_tables(a, b): + for i in range(max(len(a), len(b))): + diff = set(a[i]) ^ set(b[i]) + if len(diff) > 0: + raise Exception(i, diff) - table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}] + table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}] - with subtest("create fixture"): - machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)")) - machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)")) + with subtest("create fixture"): + machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)")) + machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)")) - res = query("SELECT * FROM test") - cmp_tables(res, table) + res = query("SELECT * FROM test") + cmp_tables(res, table) - with subtest("backup"): - print(machine.succeed("systemctl cat ${provider.result.backupService}")) - print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase")) - machine.succeed("systemctl start ${provider.result.backupService}") + with subtest("backup"): + print(machine.succeed("systemctl cat ${provider.result.backupService}")) + print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase")) + machine.succeed("systemctl start ${provider.result.backupService}") - with subtest("drop database"): - machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres")) - machine.fail(peer_cmd("SELECT * FROM test")) + with subtest("drop database"): + machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres")) + machine.fail(peer_cmd("SELECT * FROM test")) - with subtest("restore"): - print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})")) - machine.succeed("${provider.result.restoreScript} restore latest ") + with subtest("restore"): + print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})")) + machine.succeed("${provider.result.restoreScript} restore latest ") - with subtest("check restoration"): - res = query("SELECT * FROM test") - cmp_tables(res, table) - ''; + with subtest("check restoration"): + res = query("SELECT * FROM test") + cmp_tables(res, table) + ''; } diff --git a/modules/contracts/default.nix b/modules/contracts/default.nix index 2b22c10..278a81c 100644 --- a/modules/contracts/default.nix +++ b/modules/contracts/default.nix @@ -4,55 +4,61 @@ let inherit (lib.types) anything; mkContractFunctions = - { mkRequest, + { + mkRequest, mkResult, - }: { + }: + { mkRequester = requestCfg: { request = mkRequest requestCfg; - result = mkResult {}; + result = mkResult { }; }; mkProvider = - { resultCfg, - settings ? {}, - }: { - request = mkRequest {}; + { + resultCfg, + settings ? { }, + }: + { + request = mkRequest { }; result = mkResult resultCfg; - } // optionalAttrs (settings != {}) { inherit settings; }; + } + // optionalAttrs (settings != { }) { inherit settings; }; contract = { - request = mkRequest {}; + request = mkRequest { }; - result = mkResult {}; + result = mkResult { }; settings = mkOption { description = '' - Optional attribute set with options specific to the provider. + Optional attribute set with options specific to the provider. ''; type = anything; }; }; }; - importContract = module: + importContract = + module: let - importedModule = pkgs.callPackage module {}; + importedModule = pkgs.callPackage module { }; in - mkContractFunctions { - inherit (importedModule) mkRequest mkResult; - }; + mkContractFunctions { + inherit (importedModule) mkRequest mkResult; + }; in { databasebackup = importContract ./databasebackup.nix; backup = importContract ./backup.nix; - mount = pkgs.callPackage ./mount.nix {}; + mount = pkgs.callPackage ./mount.nix { }; secret = importContract ./secret.nix; - ssl = pkgs.callPackage ./ssl.nix {}; + ssl = pkgs.callPackage ./ssl.nix { }; test = { - secret = pkgs.callPackage ./secret/test.nix {}; - databasebackup = pkgs.callPackage ./databasebackup/test.nix {}; - backup = pkgs.callPackage ./backup/test.nix {}; + secret = pkgs.callPackage ./secret/test.nix { }; + databasebackup = pkgs.callPackage ./databasebackup/test.nix { }; + backup = pkgs.callPackage ./backup/test.nix { }; }; } diff --git a/modules/contracts/secret.nix b/modules/contracts/secret.nix index a22d760..28cfd79 100644 --- a/modules/contracts/secret.nix +++ b/modules/contracts/secret.nix @@ -1,20 +1,28 @@ { lib, ... }: let - inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString; + inherit (lib) + concatStringsSep + literalMD + mkOption + optionalAttrs + optionalString + ; inherit (lib.types) listOf submodule str; inherit (lib.shb) anyNotNull; in { mkRequest = - { mode ? "0400", + { + mode ? "0400", modeText ? null, owner ? "root", ownerText ? null, group ? "root", groupText ? null, - restartUnits ? [], + restartUnits ? [ ], restartUnitsText ? null, - }: mkOption { + }: + mkOption { description = '' Request part of the secret contract. @@ -23,64 +31,87 @@ in ''; default = { - inherit mode owner group restartUnits; + inherit + mode + owner + group + restartUnits + ; }; - defaultText = optionalString (anyNotNull [ - modeText - ownerText - groupText - restartUnitsText - ]) (literalMD '' - { - mode = ${if modeText != null then modeText else mode}; - owner = ${if ownerText != null then ownerText else owner}; - group = ${if groupText != null then groupText else group}; - restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + concatStringsSep " " restartUnits + " ]"}; - } - ''); + defaultText = + optionalString + (anyNotNull [ + modeText + ownerText + groupText + restartUnitsText + ]) + (literalMD '' + { + mode = ${if modeText != null then modeText else mode}; + owner = ${if ownerText != null then ownerText else owner}; + group = ${if groupText != null then groupText else group}; + restartUnits = ${ + if restartUnitsText != null then + restartUnitsText + else + "[ " + concatStringsSep " " restartUnits + " ]" + }; + } + ''); type = submodule { options = { - mode = mkOption { - description = '' - Mode of the secret file. - ''; - type = str; - default = mode; - } // optionalAttrs (modeText != null) { - defaultText = literalMD modeText; - }; + mode = + mkOption { + description = '' + Mode of the secret file. + ''; + type = str; + default = mode; + } + // optionalAttrs (modeText != null) { + defaultText = literalMD modeText; + }; - owner = mkOption ({ - description = '' - Linux user owning the secret file. - ''; - type = str; - default = owner; - } // optionalAttrs (ownerText != null) { - defaultText = literalMD ownerText; - }); + owner = mkOption ( + { + description = '' + Linux user owning the secret file. + ''; + type = str; + default = owner; + } + // optionalAttrs (ownerText != null) { + defaultText = literalMD ownerText; + } + ); - group = mkOption { - description = '' - Linux group owning the secret file. - ''; - type = str; - default = group; - } // optionalAttrs (groupText != null) { - defaultText = literalMD groupText; - }; + group = + mkOption { + description = '' + Linux group owning the secret file. + ''; + type = str; + default = group; + } + // optionalAttrs (groupText != null) { + defaultText = literalMD groupText; + }; - restartUnits = mkOption ({ - description = '' - Systemd units to restart after the secret is updated. - ''; - type = listOf str; - default = restartUnits; - } // optionalAttrs (restartUnitsText != null) { - defaultText = literalMD restartUnitsText; - }); + restartUnits = mkOption ( + { + description = '' + Systemd units to restart after the secret is updated. + ''; + type = listOf str; + default = restartUnits; + } + // optionalAttrs (restartUnitsText != null) { + defaultText = literalMD restartUnitsText; + } + ); }; }; }; @@ -90,34 +121,39 @@ in path ? "/run/secrets/secret", pathText ? null, }: - mkOption ({ - description = '' - Result part of the secret contract. + mkOption ( + { + description = '' + Result part of the secret contract. - Options set by the provider module that indicates where the secret can be found. - ''; - default = { - inherit path; - }; - type = submodule { - options = { - path = mkOption { - type = lib.types.path; - description = '' - Path to the file containing the secret generated out of band. + Options set by the provider module that indicates where the secret can be found. + ''; + default = { + inherit path; + }; + type = submodule { + options = { + path = + mkOption { + type = lib.types.path; + description = '' + Path to the file containing the secret generated out of band. - This path will exist after deploying to a target host, - it is not available through the nix store. - ''; - default = path; - } // optionalAttrs (pathText != null) { - defaultText = pathText; + This path will exist after deploying to a target host, + it is not available through the nix store. + ''; + default = path; + } + // optionalAttrs (pathText != null) { + defaultText = pathText; + }; }; }; - }; - } // optionalAttrs (pathText != null) { - defaultText = { - path = pathText; - }; - }); + } + // optionalAttrs (pathText != null) { + defaultText = { + path = pathText; + }; + } + ); } diff --git a/modules/contracts/secret/dummyModule.nix b/modules/contracts/secret/dummyModule.nix index ab41e0b..bc0162e 100644 --- a/modules/contracts/secret/dummyModule.nix +++ b/modules/contracts/secret/dummyModule.nix @@ -1,6 +1,6 @@ { pkgs, lib, ... }: let - contracts = pkgs.callPackage ../. {}; + contracts = pkgs.callPackage ../. { }; inherit (lib) mkOption; inherit (lib.types) submodule; diff --git a/modules/contracts/secret/test.nix b/modules/contracts/secret/test.nix index 7d69e5d..40ccc50 100644 --- a/modules/contracts/secret/test.nix +++ b/modules/contracts/secret/test.nix @@ -3,24 +3,33 @@ let inherit (lib) getAttrFromPath setAttrByPath; inherit (lib) mkIf; in - { name, - configRoot, - settingsCfg, # str -> attrset - modules ? [], - owner ? "root", - group ? "root", - mode ? "0400", - restartUnits ? [ "myunit.service" ], - }: lib.shb.runNixOSTest { - name = "secret_${name}_${owner}_${group}_${mode}"; +{ + name, + configRoot, + settingsCfg, # str -> attrset + modules ? [ ], + owner ? "root", + group ? "root", + mode ? "0400", + restartUnits ? [ "myunit.service" ], +}: +lib.shb.runNixOSTest { + name = "secret_${name}_${owner}_${group}_${mode}"; - nodes.machine = { config, ... }: { + nodes.machine = + { config, ... }: + { imports = [ lib.shb.baseImports ] ++ modules; config = lib.mkMerge [ (setAttrByPath configRoot { A = { request = { - inherit owner group mode restartUnits; + inherit + owner + group + mode + restartUnits + ; }; settings = settingsCfg "secretA"; }; @@ -29,35 +38,36 @@ in users.users.${owner}.isNormalUser = true; }) (mkIf (group != "root") { - users.groups.${group} = {}; + users.groups.${group} = { }; }) ]; }; - testScript = { nodes, ... }: - let - result = (getAttrFromPath configRoot nodes.machine)."A".result; - in - '' - owner = machine.succeed("stat -c '%U' ${result.path}").strip() - print(f"Got owner {owner}") - if owner != "${owner}": - raise Exception(f"Owner should be '${owner}' but got '{owner}'") + testScript = + { nodes, ... }: + let + result = (getAttrFromPath configRoot nodes.machine)."A".result; + in + '' + owner = machine.succeed("stat -c '%U' ${result.path}").strip() + print(f"Got owner {owner}") + if owner != "${owner}": + raise Exception(f"Owner should be '${owner}' but got '{owner}'") - group = machine.succeed("stat -c '%G' ${result.path}").strip() - print(f"Got group {group}") - if group != "${group}": - raise Exception(f"Group should be '${group}' but got '{group}'") + group = machine.succeed("stat -c '%G' ${result.path}").strip() + print(f"Got group {group}") + if group != "${group}": + raise Exception(f"Group should be '${group}' but got '{group}'") - mode = str(int(machine.succeed("stat -c '%a' ${result.path}").strip())) - print(f"Got mode {mode}") - wantedMode = str(int("${mode}")) - if mode != wantedMode: - raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'") + mode = str(int(machine.succeed("stat -c '%a' ${result.path}").strip())) + print(f"Got mode {mode}") + wantedMode = str(int("${mode}")) + if mode != wantedMode: + raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'") - content = machine.succeed("cat ${result.path}").strip() - print(f"Got content {content}") - if content != "secretA": - raise Exception(f"Content should be 'secretA' but got '{content}'") - ''; + content = machine.succeed("cat ${result.path}").strip() + print(f"Got content {content}") + if content != "secretA": + raise Exception(f"Content should be 'secretA' but got '{content}'") + ''; } diff --git a/modules/contracts/ssl/dummyModule.nix b/modules/contracts/ssl/dummyModule.nix index f78912e..d11651a 100644 --- a/modules/contracts/ssl/dummyModule.nix +++ b/modules/contracts/ssl/dummyModule.nix @@ -1,6 +1,6 @@ { pkgs, lib, ... }: let - contracts = pkgs.callPackage ../. {}; + contracts = pkgs.callPackage ../. { }; in { options.shb.contracts.ssl = lib.mkOption { diff --git a/modules/services/arr.nix b/modules/services/arr.nix index da82e27..ee60c97 100644 --- a/modules/services/arr.nix +++ b/modules/services/arr.nix @@ -1,9 +1,14 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.arr; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; apps = { radarr = { @@ -11,7 +16,7 @@ let moreOptions = { settings = lib.mkOption { description = "Specific options for radarr."; - default = {}; + default = { }; type = lib.types.submodule { freeformType = apps.radarr.settingsFormat.type; options = { @@ -20,7 +25,10 @@ let description = "Path to api key secret file."; }; LogLevel = lib.mkOption { - type = lib.types.enum ["debug" "info"]; + type = lib.types.enum [ + "debug" + "info" + ]; description = "Log level."; default = "info"; }; @@ -69,7 +77,7 @@ let moreOptions = { settings = lib.mkOption { description = "Specific options for sonarr."; - default = {}; + default = { }; type = lib.types.submodule { freeformType = apps.sonarr.settingsFormat.type; options = { @@ -78,7 +86,10 @@ let description = "Path to api key secret file."; }; LogLevel = lib.mkOption { - type = lib.types.enum ["debug" "info"]; + type = lib.types.enum [ + "debug" + "info" + ]; description = "Log level."; default = "info"; }; @@ -122,12 +133,15 @@ let moreOptions = { settings = lib.mkOption { description = "Specific options for bazarr."; - default = {}; + default = { }; type = lib.types.submodule { freeformType = apps.bazarr.settingsFormat.type; options = { LogLevel = lib.mkOption { - type = lib.types.enum ["debug" "info"]; + type = lib.types.enum [ + "debug" + "info" + ]; description = "Log level."; default = "info"; }; @@ -147,12 +161,15 @@ let moreOptions = { settings = lib.mkOption { description = "Specific options for readarr."; - default = {}; + default = { }; type = lib.types.submodule { freeformType = apps.readarr.settingsFormat.type; options = { LogLevel = lib.mkOption { - type = lib.types.enum ["debug" "info"]; + type = lib.types.enum [ + "debug" + "info" + ]; description = "Log level."; default = "info"; }; @@ -171,12 +188,15 @@ let moreOptions = { settings = lib.mkOption { description = "Specific options for lidarr."; - default = {}; + default = { }; type = lib.types.submodule { freeformType = apps.lidarr.settingsFormat.type; options = { LogLevel = lib.mkOption { - type = lib.types.enum ["debug" "info"]; + type = lib.types.enum [ + "debug" + "info" + ]; description = "Log level."; default = "info"; }; @@ -191,11 +211,11 @@ let }; }; jackett = { - settingsFormat = pkgs.formats.json {}; + settingsFormat = pkgs.formats.json { }; moreOptions = { settings = lib.mkOption { description = "Specific options for jackett."; - default = {}; + default = { }; type = lib.types.submodule { freeformType = apps.jackett.settingsFormat.type; options = { @@ -214,13 +234,18 @@ let default = null; }; ProxyType = lib.mkOption { - type = lib.types.enum [ "-1" "0" "1" "2" ]; + type = lib.types.enum [ + "-1" + "0" + "1" + "2" + ]; default = "-1"; description = '' - -1 = disabled - 0 = HTTP - 1 = SOCKS4 - 2 = SOCKS5 + -1 = disabled + 0 = HTTP + 1 = SOCKS4 + 2 = SOCKS5 ''; }; ProxyUrl = lib.mkOption { @@ -256,83 +281,101 @@ let }; }; - vhosts = { extraBypassResources ? [] }: c: { - inherit (c) subdomain domain authEndpoint ssl; + vhosts = + { + extraBypassResources ? [ ], + }: + c: { + inherit (c) + subdomain + domain + authEndpoint + ssl + ; - upstream = "http://127.0.0.1:${toString c.settings.Port}"; - autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [ - { - domain = "${c.subdomain}.${c.domain}"; - policy = "bypass"; - resources = extraBypassResources ++ [ - "^/api.*" - "^/feed.*" - ]; - } - { - domain = "${c.subdomain}.${c.domain}"; - policy = "two_factor"; - subject = ["group:arr_user"]; - } - ]; - }; - - appOption = name: c: lib.nameValuePair name (lib.mkOption { - description = "Configuration for ${name}"; - default = {}; - type = lib.types.submodule { - options = { - enable = lib.mkEnableOption name; - - subdomain = lib.mkOption { - type = lib.types.str; - description = "Subdomain under which ${name} will be served."; - example = name; - }; - - domain = lib.mkOption { - type = lib.types.str; - description = "Domain under which ${name} will be served."; - example = "example.com"; - }; - - dataDir = lib.mkOption { - type = lib.types.str; - description = "Directory where ${name} stores data."; - default = "/var/lib/${name}"; - }; - - ssl = lib.mkOption { - description = "Path to SSL files"; - type = lib.types.nullOr contracts.ssl.certs; - default = null; - }; - - authEndpoint = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "Endpoint to the SSO provider. Leave null to not have SSO configured."; - example = "https://authelia.example.com"; - }; - - backup = lib.mkOption { - description = '' - Backup configuration. - ''; - default = {}; - type = lib.types.submodule { - options = contracts.backup.mkRequester { - user = name; - sourceDirectories = [ - cfg.${name}.dataDir - ]; - excludePatterns = [".db-shm" ".db-wal" ".mono"]; - }; - }; - }; - } // (c.moreOptions or {}); + upstream = "http://127.0.0.1:${toString c.settings.Port}"; + autheliaRules = lib.optionals (!(isNull c.authEndpoint)) [ + { + domain = "${c.subdomain}.${c.domain}"; + policy = "bypass"; + resources = extraBypassResources ++ [ + "^/api.*" + "^/feed.*" + ]; + } + { + domain = "${c.subdomain}.${c.domain}"; + policy = "two_factor"; + subject = [ "group:arr_user" ]; + } + ]; }; - }); + + appOption = + name: c: + lib.nameValuePair name ( + lib.mkOption { + description = "Configuration for ${name}"; + default = { }; + type = lib.types.submodule { + options = { + enable = lib.mkEnableOption name; + + subdomain = lib.mkOption { + type = lib.types.str; + description = "Subdomain under which ${name} will be served."; + example = name; + }; + + domain = lib.mkOption { + type = lib.types.str; + description = "Domain under which ${name} will be served."; + example = "example.com"; + }; + + dataDir = lib.mkOption { + type = lib.types.str; + description = "Directory where ${name} stores data."; + default = "/var/lib/${name}"; + }; + + ssl = lib.mkOption { + description = "Path to SSL files"; + type = lib.types.nullOr contracts.ssl.certs; + default = null; + }; + + authEndpoint = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Endpoint to the SSO provider. Leave null to not have SSO configured."; + example = "https://authelia.example.com"; + }; + + backup = lib.mkOption { + description = '' + Backup configuration. + ''; + default = { }; + type = lib.types.submodule { + options = contracts.backup.mkRequester { + user = name; + sourceDirectories = [ + cfg.${name}.dataDir + ]; + excludePatterns = [ + ".db-shm" + ".db-wal" + ".mono" + ]; + }; + }; + }; + } + // (c.moreOptions or { }); + }; + } + ); in { imports = [ @@ -343,155 +386,167 @@ in config = lib.mkMerge [ (lib.mkIf cfg.radarr.enable ( - let - cfg' = cfg.radarr; - isSSOEnabled = !(isNull cfg'.authEndpoint); - in - { - services.nginx.enable = true; + let + cfg' = cfg.radarr; + isSSOEnabled = !(isNull cfg'.authEndpoint); + in + { + services.nginx.enable = true; - services.radarr = { - enable = true; - dataDir = "/var/lib/radarr"; - }; + services.radarr = { + enable = true; + dataDir = "/var/lib/radarr"; + }; - systemd.services.radarr.preStart = lib.shb.replaceSecrets { - userConfig = cfg'.settings - // (lib.optionalAttrs isSSOEnabled { - AuthenticationRequired = "DisabledForLocalAddresses"; - AuthenticationMethod = "External"; - }); - resultPath = "${config.services.radarr.dataDir}/config.xml"; - generator = lib.shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat; - }; + systemd.services.radarr.preStart = lib.shb.replaceSecrets { + userConfig = + cfg'.settings + // (lib.optionalAttrs isSSOEnabled { + AuthenticationRequired = "DisabledForLocalAddresses"; + AuthenticationMethod = "External"; + }); + resultPath = "${config.services.radarr.dataDir}/config.xml"; + generator = lib.shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat; + }; - shb.nginx.vhosts = [ (vhosts {} cfg') ]; - })) + shb.nginx.vhosts = [ (vhosts { } cfg') ]; + } + )) (lib.mkIf cfg.sonarr.enable ( - let - cfg' = cfg.sonarr; - isSSOEnabled = !(isNull cfg'.authEndpoint); - in - { - services.nginx.enable = true; + let + cfg' = cfg.sonarr; + isSSOEnabled = !(isNull cfg'.authEndpoint); + in + { + services.nginx.enable = true; - services.sonarr = { - enable = true; - dataDir = "/var/lib/sonarr"; - }; - users.users.sonarr = { - extraGroups = [ "media" ]; - }; + services.sonarr = { + enable = true; + dataDir = "/var/lib/sonarr"; + }; + users.users.sonarr = { + extraGroups = [ "media" ]; + }; - systemd.services.sonarr.preStart = lib.shb.replaceSecrets { - userConfig = cfg'.settings - // (lib.optionalAttrs isSSOEnabled { - AuthenticationRequired = "DisabledForLocalAddresses"; - AuthenticationMethod = "External"; - }); - resultPath = "${config.services.sonarr.dataDir}/config.xml"; - generator = apps.sonarr.settingsFormat.generate; - }; + systemd.services.sonarr.preStart = lib.shb.replaceSecrets { + userConfig = + cfg'.settings + // (lib.optionalAttrs isSSOEnabled { + AuthenticationRequired = "DisabledForLocalAddresses"; + AuthenticationMethod = "External"; + }); + resultPath = "${config.services.sonarr.dataDir}/config.xml"; + generator = apps.sonarr.settingsFormat.generate; + }; - shb.nginx.vhosts = [ (vhosts {} cfg') ]; - })) + shb.nginx.vhosts = [ (vhosts { } cfg') ]; + } + )) (lib.mkIf cfg.bazarr.enable ( - let - cfg' = cfg.bazarr; - isSSOEnabled = !(isNull cfg'.authEndpoint); - in - { - services.bazarr = { - enable = true; - listenPort = cfg'.settings.Port; - }; - users.users.bazarr = { - extraGroups = [ "media" ]; - }; - systemd.services.bazarr.preStart = lib.shb.replaceSecrets { - userConfig = cfg'.settings - // (lib.optionalAttrs isSSOEnabled { - AuthenticationRequired = "DisabledForLocalAddresses"; - AuthenticationMethod = "External"; - }); - resultPath = "/var/lib/bazarr/config.xml"; - generator = apps.bazarr.settingsFormat.generate; - }; + let + cfg' = cfg.bazarr; + isSSOEnabled = !(isNull cfg'.authEndpoint); + in + { + services.bazarr = { + enable = true; + listenPort = cfg'.settings.Port; + }; + users.users.bazarr = { + extraGroups = [ "media" ]; + }; + systemd.services.bazarr.preStart = lib.shb.replaceSecrets { + userConfig = + cfg'.settings + // (lib.optionalAttrs isSSOEnabled { + AuthenticationRequired = "DisabledForLocalAddresses"; + AuthenticationMethod = "External"; + }); + resultPath = "/var/lib/bazarr/config.xml"; + generator = apps.bazarr.settingsFormat.generate; + }; - shb.nginx.vhosts = [ (vhosts {} cfg') ]; - })) + shb.nginx.vhosts = [ (vhosts { } cfg') ]; + } + )) (lib.mkIf cfg.readarr.enable ( - let - cfg' = cfg.readarr; - in - { - services.readarr = { - enable = true; - dataDir = "/var/lib/readarr"; - }; - users.users.readarr = { - extraGroups = [ "media" ]; - }; - systemd.services.readarr.preStart = lib.shb.replaceSecrets { - userConfig = cfg'.settings; - resultPath = "${config.services.readarr.dataDir}/config.xml"; - generator = apps.readarr.settingsFormat.generate; - }; + let + cfg' = cfg.readarr; + in + { + services.readarr = { + enable = true; + dataDir = "/var/lib/readarr"; + }; + users.users.readarr = { + extraGroups = [ "media" ]; + }; + systemd.services.readarr.preStart = lib.shb.replaceSecrets { + userConfig = cfg'.settings; + resultPath = "${config.services.readarr.dataDir}/config.xml"; + generator = apps.readarr.settingsFormat.generate; + }; - shb.nginx.vhosts = [ (vhosts {} cfg') ]; - })) + shb.nginx.vhosts = [ (vhosts { } cfg') ]; + } + )) (lib.mkIf cfg.lidarr.enable ( - let - cfg' = cfg.lidarr; - isSSOEnabled = !(isNull cfg'.authEndpoint); - in - { - services.lidarr = { - enable = true; - dataDir = "/var/lib/lidarr"; - }; - users.users.lidarr = { - extraGroups = [ "media" ]; - }; - systemd.services.lidarr.preStart = lib.shb.replaceSecrets { - userConfig = cfg'.settings - // (lib.optionalAttrs isSSOEnabled { - AuthenticationRequired = "DisabledForLocalAddresses"; - AuthenticationMethod = "External"; - }); - resultPath = "${config.services.lidarr.dataDir}/config.xml"; - generator = apps.lidarr.settingsFormat.generate; - }; + let + cfg' = cfg.lidarr; + isSSOEnabled = !(isNull cfg'.authEndpoint); + in + { + services.lidarr = { + enable = true; + dataDir = "/var/lib/lidarr"; + }; + users.users.lidarr = { + extraGroups = [ "media" ]; + }; + systemd.services.lidarr.preStart = lib.shb.replaceSecrets { + userConfig = + cfg'.settings + // (lib.optionalAttrs isSSOEnabled { + AuthenticationRequired = "DisabledForLocalAddresses"; + AuthenticationMethod = "External"; + }); + resultPath = "${config.services.lidarr.dataDir}/config.xml"; + generator = apps.lidarr.settingsFormat.generate; + }; - shb.nginx.vhosts = [ (vhosts {} cfg') ]; - })) + shb.nginx.vhosts = [ (vhosts { } cfg') ]; + } + )) (lib.mkIf cfg.jackett.enable ( - let - cfg' = cfg.jackett; - in - { - services.jackett = { - enable = true; - dataDir = "/var/lib/jackett"; - }; - # TODO: avoid implicitly relying on the media group - users.users.jackett = { - extraGroups = [ "media" ]; - }; - systemd.services.jackett.preStart = lib.shb.replaceSecrets { - userConfig = lib.shb.renameAttrName cfg'.settings "ApiKey" "APIKey"; - resultPath = "${config.services.jackett.dataDir}/ServerConfig.json"; - generator = apps.jackett.settingsFormat.generate; - }; + let + cfg' = cfg.jackett; + in + { + services.jackett = { + enable = true; + dataDir = "/var/lib/jackett"; + }; + # TODO: avoid implicitly relying on the media group + users.users.jackett = { + extraGroups = [ "media" ]; + }; + systemd.services.jackett.preStart = lib.shb.replaceSecrets { + userConfig = lib.shb.renameAttrName cfg'.settings "ApiKey" "APIKey"; + resultPath = "${config.services.jackett.dataDir}/ServerConfig.json"; + generator = apps.jackett.settingsFormat.generate; + }; - shb.nginx.vhosts = [ (vhosts { - extraBypassResources = [ "^/dl.*" ]; - } cfg') ]; - })) + shb.nginx.vhosts = [ + (vhosts { + extraBypassResources = [ "^/dl.*" ]; + } cfg') + ]; + } + )) ]; } diff --git a/modules/services/audiobookshelf.nix b/modules/services/audiobookshelf.nix index 3e66e6e..9293317 100644 --- a/modules/services/audiobookshelf.nix +++ b/modules/services/audiobookshelf.nix @@ -1,9 +1,14 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.audiobookshelf; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; @@ -40,18 +45,18 @@ in extraServiceConfig = lib.mkOption { type = lib.types.attrsOf lib.types.str; description = "Extra configuration given to the systemd service file."; - default = {}; + default = { }; example = lib.literalExpression '' - { - MemoryHigh = "512M"; - MemoryMax = "900M"; - } + { + MemoryHigh = "512M"; + MemoryMax = "900M"; + } ''; }; sso = lib.mkOption { description = "SSO configuration."; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "SSO"; @@ -87,7 +92,10 @@ in }; authorization_policy = lib.mkOption { - type = lib.types.enum [ "one_factor" "two_factor" ]; + type = lib.types.enum [ + "one_factor" + "two_factor" + ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; @@ -133,84 +141,102 @@ in }; logLevel = lib.mkOption { - type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]); + type = lib.types.nullOr ( + lib.types.enum [ + "critical" + "error" + "warning" + "info" + "debug" + ] + ); description = "Enable logging."; default = false; example = true; }; }; - config = lib.mkIf cfg.enable (lib.mkMerge [{ - - services.audiobookshelf = { - enable = true; - openFirewall = true; - dataDir = "audiobookshelf"; - host = "127.0.0.1"; - port = cfg.webPort; - }; - - services.nginx.enable = true; - services.nginx.virtualHosts."${fqdn}" = { - http2 = true; - forceSSL = !(isNull cfg.ssl); - sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert; - sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key; - - # https://github.com/advplyr/audiobookshelf#nginx-reverse-proxy - extraConfig = '' - set $audiobookshelf 127.0.0.1; - location / { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $host; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - proxy_http_version 1.1; - - proxy_pass http://$audiobookshelf:${builtins.toString cfg.webPort}; - proxy_redirect http:// https://; - } - ''; - }; - - - shb.authelia.extraDefinitions = { - user_attributes.${roleClaim}.expression = - ''"${cfg.sso.adminUserGroup}" in groups ? ["admin"] : ("${cfg.sso.userGroup}" in groups ? ["user"] : [""])''; - }; - - shb.authelia.extraOidcClaimsPolicies.${roleClaim} = { - custom_claims = { - "${roleClaim}" = {}; - }; - }; - - shb.authelia.extraOidcScopes."${roleClaim}" = { - claims = [ "${roleClaim}" ]; - }; - - shb.authelia.oidcClients = lib.lists.optionals cfg.sso.enable [ + config = lib.mkIf cfg.enable ( + lib.mkMerge [ { - client_id = cfg.sso.clientID; - client_name = "Audiobookshelf"; - client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; - claims_policy = "${roleClaim}"; - public = false; - authorization_policy = cfg.sso.authorization_policy; - redirect_uris = [ - "https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback" - "https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect" + + services.audiobookshelf = { + enable = true; + openFirewall = true; + dataDir = "audiobookshelf"; + host = "127.0.0.1"; + port = cfg.webPort; + }; + + services.nginx.enable = true; + services.nginx.virtualHosts."${fqdn}" = { + http2 = true; + forceSSL = !(isNull cfg.ssl); + sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert; + sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key; + + # https://github.com/advplyr/audiobookshelf#nginx-reverse-proxy + extraConfig = '' + set $audiobookshelf 127.0.0.1; + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_http_version 1.1; + + proxy_pass http://$audiobookshelf:${builtins.toString cfg.webPort}; + proxy_redirect http:// https://; + } + ''; + }; + + shb.authelia.extraDefinitions = { + user_attributes.${roleClaim}.expression = + ''"${cfg.sso.adminUserGroup}" in groups ? ["admin"] : ("${cfg.sso.userGroup}" in groups ? ["user"] : [""])''; + }; + + shb.authelia.extraOidcClaimsPolicies.${roleClaim} = { + custom_claims = { + "${roleClaim}" = { }; + }; + }; + + shb.authelia.extraOidcScopes."${roleClaim}" = { + claims = [ "${roleClaim}" ]; + }; + + shb.authelia.oidcClients = lib.lists.optionals cfg.sso.enable [ + { + client_id = cfg.sso.clientID; + client_name = "Audiobookshelf"; + client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; + claims_policy = "${roleClaim}"; + public = false; + authorization_policy = cfg.sso.authorization_policy; + redirect_uris = [ + "https://${cfg.subdomain}.${cfg.domain}/auth/openid/callback" + "https://${cfg.subdomain}.${cfg.domain}/auth/openid/mobile-redirect" + ]; + scopes = [ + "openid" + "profile" + "email" + "groups" + "${roleClaim}" + ]; + require_pkce = true; + pkce_challenge_method = "S256"; + userinfo_signed_response_alg = "none"; + token_endpoint_auth_method = "client_secret_basic"; + } ]; - scopes = [ "openid" "profile" "email" "groups" "${roleClaim}" ]; - require_pkce = true; - pkce_challenge_method = "S256"; - userinfo_signed_response_alg = "none"; - token_endpoint_auth_method = "client_secret_basic"; } - ]; - } { - systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig; - }]); + { + systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig; + } + ] + ); } diff --git a/modules/services/deluge.nix b/modules/services/deluge.nix index 1736bb3..cabb57d 100644 --- a/modules/services/deluge.nix +++ b/modules/services/deluge.nix @@ -1,20 +1,31 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.deluge; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; - authGenerator = users: + authGenerator = + users: let - genLine = name: { password, priority ? 10 }: + genLine = + name: + { + password, + priority ? 10, + }: "${name}:${password}:${toString priority}"; lines = lib.mapAttrsToList genLine users; in - lib.concatStringsSep "\n" lines; + lib.concatStringsSep "\n" lines; in { imports = [ @@ -57,7 +68,10 @@ in daemonListenPorts = lib.mkOption { type = lib.types.listOf lib.types.int; description = "Deluge daemon listen ports"; - default = [ 6881 6889 ]; + default = [ + 6881 + 6889 + ]; }; webPort = lib.mkOption { @@ -158,12 +172,12 @@ in extraServiceConfig = lib.mkOption { type = lib.types.attrsOf lib.types.str; description = "Extra configuration given to the systemd service file."; - default = {}; + default = { }; example = lib.literalExpression '' - { - MemoryHigh = "512M"; - MemoryMax = "900M"; - } + { + MemoryHigh = "512M"; + MemoryMax = "900M"; + } ''; }; @@ -176,14 +190,16 @@ in extraUsers = lib.mkOption { description = "Users having access to this deluge instance. Attrset of username to user options."; - type = lib.types.attrsOf (lib.types.submodule { - options = { - password = lib.mkOption { - type = lib.shb.secretFileType; - description = "File containing the user password."; + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + password = lib.mkOption { + type = lib.shb.secretFileType; + description = "File containing the user password."; + }; }; - }; - }); + } + ); }; localclientPassword = lib.mkOption { @@ -198,12 +214,17 @@ in prometheusScraperPassword = lib.mkOption { description = "Password for prometheus scraper. Setting this option will activate the prometheus deluge exporter."; - type = lib.types.nullOr (lib.types.submodule { - options = contracts.secret.mkRequester { - owner = "deluge"; - restartUnits = [ "deluged.service" "prometheus.service" ]; - }; - }); + type = lib.types.nullOr ( + lib.types.submodule { + options = contracts.secret.mkRequester { + owner = "deluge"; + restartUnits = [ + "deluged.service" + "prometheus.service" + ]; + }; + } + ); default = null; }; @@ -214,35 +235,35 @@ in Label is automatically enabled if any of the `shb.arr.*` service is enabled. ''; - example = ["Label"]; - default = []; + example = [ "Label" ]; + default = [ ]; }; additionalPlugins = lib.mkOption { type = lib.types.listOf lib.types.path; description = "Location of additional plugins. Each item in the list must be the path to the directory containing the plugin .egg file."; - default = []; + default = [ ]; example = lib.literalExpression '' - additionalPlugins = [ - (pkgs.callPackage ({ python3, fetchFromGitHub }: python3.pkgs.buildPythonPackage { - name = "deluge-autotracker"; - version = "1.0.0"; - src = fetchFromGitHub { - owner = "ibizaman"; - repo = "deluge-autotracker"; - rev = "cc40d816a497bbf1c2ebeb3d8b1176210548a3e6"; - sha256 = "sha256-0LpVdv1fak2a5eX4unjhUcN7nMAl9fgpr3X+7XnQE6c="; - } + "/autotracker"; - doCheck = false; - format = "other"; - nativeBuildInputs = [ python3.pkgs.setuptools ]; - buildPhase = ''' - mkdir "$out" - python3 setup.py install --install-lib "$out" - '''; - doInstallPhase = false; - }) {}) - ]; + additionalPlugins = [ + (pkgs.callPackage ({ python3, fetchFromGitHub }: python3.pkgs.buildPythonPackage { + name = "deluge-autotracker"; + version = "1.0.0"; + src = fetchFromGitHub { + owner = "ibizaman"; + repo = "deluge-autotracker"; + rev = "cc40d816a497bbf1c2ebeb3d8b1176210548a3e6"; + sha256 = "sha256-0LpVdv1fak2a5eX4unjhUcN7nMAl9fgpr3X+7XnQE6c="; + } + "/autotracker"; + doCheck = false; + format = "other"; + nativeBuildInputs = [ python3.pkgs.setuptools ]; + buildPhase = ''' + mkdir "$out" + python3 setup.py install --install-lib "$out" + '''; + doInstallPhase = false; + }) {}) + ]; ''; }; @@ -250,7 +271,7 @@ in description = '' Backup configuration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = "deluge"; @@ -262,146 +283,175 @@ in }; logLevel = lib.mkOption { - type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]); + type = lib.types.nullOr ( + lib.types.enum [ + "critical" + "error" + "warning" + "info" + "debug" + ] + ); description = "Enable logging."; default = null; example = "info"; }; }; - config = lib.mkIf cfg.enable (lib.mkMerge [{ - services.deluge = { - enable = true; - declarative = true; - openFirewall = true; - inherit (cfg) dataDir; + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + services.deluge = { + enable = true; + declarative = true; + openFirewall = true; + inherit (cfg) dataDir; - config = { - download_location = cfg.settings.downloadLocation; - allow_remote = true; - daemon_port = cfg.daemonPort; - listen_ports = cfg.daemonListenPorts; - proxy = lib.optionalAttrs (cfg.proxyPort != null) { - force_proxy = true; - hostname = "127.0.0.1"; - port = cfg.proxyPort; - proxy_hostnames = true; - proxy_peer_connections = true; - proxy_tracker_connections = true; - type = 4; # HTTP + config = { + download_location = cfg.settings.downloadLocation; + allow_remote = true; + daemon_port = cfg.daemonPort; + listen_ports = cfg.daemonListenPorts; + proxy = lib.optionalAttrs (cfg.proxyPort != null) { + force_proxy = true; + hostname = "127.0.0.1"; + port = cfg.proxyPort; + proxy_hostnames = true; + proxy_peer_connections = true; + proxy_tracker_connections = true; + type = 4; # HTTP + }; + outgoing_interface = cfg.outgoingInterface; + + enabled_plugins = + cfg.enabledPlugins + ++ lib.optional (lib.any (x: x.enable) [ + config.services.radarr + config.services.sonarr + config.services.bazarr + config.services.readarr + config.services.lidarr + ]) "Label"; + + inherit (cfg.settings) + max_active_limit + max_active_downloading + max_active_seeding + max_connections_global + max_connections_per_torrent + + max_download_speed + max_download_speed_per_torrent + + max_upload_slots_global + max_upload_slots_per_torrent + max_upload_speed + max_upload_speed_per_torrent + + dont_count_slow_torrents + ; + + new_release_check = false; + }; + + authFile = "${cfg.dataDir}/.config/deluge/authTemplate"; + + web.enable = true; + web.port = cfg.webPort; }; - outgoing_interface = cfg.outgoingInterface; - enabled_plugins = cfg.enabledPlugins - ++ lib.optional (lib.any (x: x.enable) [ - config.services.radarr - config.services.sonarr - config.services.bazarr - config.services.readarr - config.services.lidarr - ]) "Label"; + systemd.services.deluged.preStart = lib.mkBefore ( + lib.shb.replaceSecrets { + userConfig = + cfg.extraUsers + // { + localclient.password.source = config.shb.deluge.localclientPassword.result.path; + } + // (lib.optionalAttrs (config.shb.deluge.prometheusScraperPassword != null) { + prometheus_scraper.password.source = config.shb.deluge.prometheusScraperPassword.result.path; + }); + resultPath = "${cfg.dataDir}/.config/deluge/authTemplate"; + generator = name: value: pkgs.writeText "delugeAuth" (authGenerator value); + } + ); - inherit (cfg.settings) - max_active_limit - max_active_downloading - max_active_seeding - max_connections_global - max_connections_per_torrent + systemd.services.deluged.serviceConfig.ExecStart = lib.mkForce ( + lib.concatStringsSep " \\\n " ( + [ + "${config.services.deluge.package}/bin/deluged" + "--do-not-daemonize" + "--config ${cfg.dataDir}/.config/deluge" + ] + ++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}") + ) + ); - max_download_speed - max_download_speed_per_torrent + systemd.tmpfiles.rules = + let + plugins = pkgs.symlinkJoin { + name = "deluge-plugins"; + paths = cfg.additionalPlugins; + }; + in + [ + "L+ ${cfg.dataDir}/.config/deluge/plugins - - - - ${plugins}" + ]; - max_upload_slots_global - max_upload_slots_per_torrent - max_upload_speed - max_upload_speed_per_torrent - - dont_count_slow_torrents; - - new_release_check = false; - }; - - authFile = "${cfg.dataDir}/.config/deluge/authTemplate"; - - web.enable = true; - web.port = cfg.webPort; - }; - - systemd.services.deluged.preStart = lib.mkBefore (lib.shb.replaceSecrets { - userConfig = cfg.extraUsers // { - localclient.password.source = config.shb.deluge.localclientPassword.result.path; - } // (lib.optionalAttrs (config.shb.deluge.prometheusScraperPassword != null) { - prometheus_scraper.password.source = config.shb.deluge.prometheusScraperPassword.result.path; - }); - resultPath = "${cfg.dataDir}/.config/deluge/authTemplate"; - generator = name: value: pkgs.writeText "delugeAuth" (authGenerator value); - }); - - systemd.services.deluged.serviceConfig.ExecStart = lib.mkForce (lib.concatStringsSep " \\\n " ([ - "${config.services.deluge.package}/bin/deluged" - "--do-not-daemonize" - "--config ${cfg.dataDir}/.config/deluge" - ] ++ (lib.optional (!(isNull cfg.logLevel)) "-L ${cfg.logLevel}") - )); - - systemd.tmpfiles.rules = - let - plugins = pkgs.symlinkJoin { - name = "deluge-plugins"; - paths = cfg.additionalPlugins; - }; - in - [ - "L+ ${cfg.dataDir}/.config/deluge/plugins - - - - ${plugins}" + shb.nginx.vhosts = [ + ( + { + inherit (cfg) subdomain domain ssl; + upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}"; + autheliaRules = lib.mkIf (cfg.authEndpoint != null) [ + { + domain = fqdn; + policy = "bypass"; + resources = [ + "^/json" + ]; + } + { + domain = fqdn; + policy = "two_factor"; + subject = [ "group:deluge_user" ]; + } + ]; + } + // (lib.optionalAttrs (cfg.authEndpoint != null) { + inherit (cfg) authEndpoint; + }) + ) ]; + } + { + systemd.services.deluged.serviceConfig = cfg.extraServiceConfig; + } + (lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) { + services.prometheus.exporters.deluge = { + enable = true; - shb.nginx.vhosts = [ - ({ - inherit (cfg) subdomain domain ssl; - upstream = "http://127.0.0.1:${toString config.services.deluge.web.port}"; - autheliaRules = lib.mkIf (cfg.authEndpoint != null) [ + delugeHost = "127.0.0.1"; + delugePort = config.services.deluge.config.daemon_port; + delugeUser = "prometheus_scraper"; + delugePasswordFile = config.shb.deluge.prometheusScraperPassword.result.path; + exportPerTorrentMetrics = true; + }; + + services.prometheus.scrapeConfigs = [ { - domain = fqdn; - policy = "bypass"; - resources = [ - "^/json" + job_name = "deluge"; + static_configs = [ + { + targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.deluge.port}" ]; + labels = { + "hostname" = config.networking.hostName; + "domain" = cfg.domain; + }; + } ]; } - { - domain = fqdn; - policy = "two_factor"; - subject = ["group:deluge_user"]; - } ]; - } // (lib.optionalAttrs (cfg.authEndpoint != null) { - inherit (cfg) authEndpoint; - })) - ]; - } { - systemd.services.deluged.serviceConfig = cfg.extraServiceConfig; - } (lib.mkIf (config.shb.deluge.prometheusScraperPassword != null) { - services.prometheus.exporters.deluge = { - enable = true; - - delugeHost = "127.0.0.1"; - delugePort = config.services.deluge.config.daemon_port; - delugeUser = "prometheus_scraper"; - delugePasswordFile = config.shb.deluge.prometheusScraperPassword.result.path; - exportPerTorrentMetrics = true; - }; - - services.prometheus.scrapeConfigs = [ - { - job_name = "deluge"; - static_configs = [{ - targets = ["127.0.0.1:${toString config.services.prometheus.exporters.deluge.port}"]; - labels = { - "hostname" = config.networking.hostName; - "domain" = cfg.domain; - }; - }]; - } - ]; - }) - ]); + }) + ] + ); } diff --git a/modules/services/forgejo.nix b/modules/services/forgejo.nix index 9171d1c..40fa0d5 100644 --- a/modules/services/forgejo.nix +++ b/modules/services/forgejo.nix @@ -1,25 +1,60 @@ -{ config, options, pkgs, lib, ... }: +{ + config, + options, + pkgs, + lib, + ... +}: let cfg = config.shb.forgejo; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; - inherit (lib) all attrNames concatMapStringsSep getExe lists literalExpression mapAttrsToList mkBefore mkEnableOption mkForce mkIf mkMerge mkOption mkOverride nameValuePair optionalString optionals; - inherit (lib.types) attrsOf bool enum listOf nullOr package port submodule str; + inherit (lib) + all + attrNames + concatMapStringsSep + getExe + lists + literalExpression + mapAttrsToList + mkBefore + mkEnableOption + mkForce + mkIf + mkMerge + mkOption + mkOverride + nameValuePair + optionalString + optionals + ; + inherit (lib.types) + attrsOf + bool + enum + listOf + nullOr + package + port + submodule + str + ; in { imports = [ ../blocks/nginx.nix - (lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''Instead, define an admin user in shb.forgejo.users and give it the same password, like so: - shb.forgejo.users = { - "forgejoadmin" = { - isAdmin = true; - email = "forgejoadmin@example.com"; - password.result = ; - }; - }; + (lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] '' + Instead, define an admin user in shb.forgejo.users and give it the same password, like so: + shb.forgejo.users = { + "forgejoadmin" = { + isAdmin = true; + email = "forgejoadmin@example.com"; + password.result = ; + }; + }; '') ]; @@ -60,7 +95,7 @@ in description = '' LDAP Integration. ''; - default = {}; + default = { }; type = nullOr (submodule { options = { enable = mkEnableOption "LDAP integration."; @@ -125,7 +160,7 @@ in waitForSystemdServices = mkOption { type = listOf str; - default = []; + default = [ ]; description = '' List of systemd services to wait on before starting. This is needed because forgejo will try a lookup on the LDAP instance @@ -140,7 +175,7 @@ in description = '' Setup SSO integration. ''; - default = {}; + default = { }; type = submodule { options = { enable = mkEnableOption "SSO integration."; @@ -164,7 +199,10 @@ in }; authorization_policy = mkOption { - type = enum [ "one_factor" "two_factor" ]; + type = enum [ + "one_factor" + "two_factor" + ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; @@ -205,9 +243,10 @@ in }; email = mkOption { - description = ''Email of user. + description = '' + Email of user. - This is only set when the user is created, changing this later on will have no effect. + This is only set when the user is created, changing this later on will have no effect. ''; type = str; }; @@ -254,7 +293,6 @@ in ''; }; - hostPackages = mkOption { type = listOf package; default = with pkgs; [ @@ -289,13 +327,14 @@ in description = '' Backup configuration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = options.services.forgejo.user.value; sourceDirectories = [ options.services.forgejo.dump.backupDir.value - ] ++ optionals (cfg.repositoryRoot != null) [ + ] + ++ optionals (cfg.repositoryRoot != null) [ cfg.repositoryRoot ]; }; @@ -317,7 +356,9 @@ in ``` ''; readOnly = true; - default = { path = config.services.forgejo.stateDir; }; + default = { + path = config.services.forgejo.stateDir; + }; }; smtp = mkOption { @@ -390,10 +431,12 @@ in # https://github.com/NixOS/nixpkgs/issues/258371#issuecomment-2271967113 systemd.services.forgejo.serviceConfig.Type = mkForce "exec"; - shb.nginx.vhosts = [{ - inherit (cfg) domain subdomain ssl; - upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}"; - }]; + shb.nginx.vhosts = [ + { + inherit (cfg) domain subdomain ssl; + upstream = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}"; + } + ]; }) (mkIf cfg.enable { @@ -420,58 +463,60 @@ in systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices; systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices; # The delimiter in the `cut` command is a TAB! - systemd.services.forgejo.preStart = let - provider = "SHB-${cfg.ldap.provider}"; - in '' - auth="${getExe config.services.forgejo.package} admin auth" + systemd.services.forgejo.preStart = + let + provider = "SHB-${cfg.ldap.provider}"; + in + '' + auth="${getExe config.services.forgejo.package} admin auth" - echo "Trying to find existing ldap configuration for ${provider}"... - set +e -o pipefail - id="$($auth list | grep "${provider}.*LDAP" | cut -d' ' -f1)" - found=$? - set -e +o pipefail + echo "Trying to find existing ldap configuration for ${provider}"... + set +e -o pipefail + id="$($auth list | grep "${provider}.*LDAP" | cut -d' ' -f1)" + found=$? + set -e +o pipefail - if [[ $found = 0 ]]; then - echo Found ldap configuration at id=$id, updating it if needed. - $auth update-ldap \ - --id $id \ - --name ${provider} \ - --host ${cfg.ldap.host} \ - --port ${toString cfg.ldap.port} \ - --bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \ - --bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \ - --security-protocol Unencrypted \ - --user-search-base ou=people,${cfg.ldap.dcdomain} \ - --user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \ - --admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \ - --username-attribute uid \ - --firstname-attribute givenName \ - --surname-attribute sn \ - --email-attribute mail \ - --avatar-attribute jpegPhoto \ - --synchronize-users - echo "Done updating LDAP configuration." - else - echo Did not find any ldap configuration, creating one with name ${provider}. - $auth add-ldap \ - --name ${provider} \ - --host ${cfg.ldap.host} \ - --port ${toString cfg.ldap.port} \ - --bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \ - --bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \ - --security-protocol Unencrypted \ - --user-search-base ou=people,${cfg.ldap.dcdomain} \ - --user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \ - --admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \ - --username-attribute uid \ - --firstname-attribute givenName \ - --surname-attribute sn \ - --email-attribute mail \ - --avatar-attribute jpegPhoto \ - --synchronize-users - echo "Done adding LDAP configuration." - fi - ''; + if [[ $found = 0 ]]; then + echo Found ldap configuration at id=$id, updating it if needed. + $auth update-ldap \ + --id $id \ + --name ${provider} \ + --host ${cfg.ldap.host} \ + --port ${toString cfg.ldap.port} \ + --bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \ + --bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \ + --security-protocol Unencrypted \ + --user-search-base ou=people,${cfg.ldap.dcdomain} \ + --user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \ + --admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \ + --username-attribute uid \ + --firstname-attribute givenName \ + --surname-attribute sn \ + --email-attribute mail \ + --avatar-attribute jpegPhoto \ + --synchronize-users + echo "Done updating LDAP configuration." + else + echo Did not find any ldap configuration, creating one with name ${provider}. + $auth add-ldap \ + --name ${provider} \ + --host ${cfg.ldap.host} \ + --port ${toString cfg.ldap.port} \ + --bind-dn uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain} \ + --bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \ + --security-protocol Unencrypted \ + --user-search-base ou=people,${cfg.ldap.dcdomain} \ + --user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \ + --admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \ + --username-attribute uid \ + --firstname-attribute givenName \ + --surname-attribute sn \ + --email-attribute mail \ + --avatar-attribute jpegPhoto \ + --synchronize-users + echo "Done adding LDAP configuration." + fi + ''; }) # For Authelia to Forgejo integration: https://www.authelia.com/integration/openid-connect/gitea/ @@ -488,7 +533,7 @@ in ENABLE_OPENID_SIGNUP = true; WHITELISTED_URIS = cfg.sso.endpoint; }; - + service = { # DISABLE_REGISTRATION = mkForce false; # ALLOW_ONLY_EXTERNAL_REGISTRATION = false; @@ -497,48 +542,53 @@ in }; # The delimiter in the `cut` command is a TAB! - systemd.services.forgejo.preStart = let - provider = "SHB-${cfg.sso.provider}"; - in '' - auth="${getExe config.services.forgejo.package} admin auth" + systemd.services.forgejo.preStart = + let + provider = "SHB-${cfg.sso.provider}"; + in + '' + auth="${getExe config.services.forgejo.package} admin auth" - echo "Trying to find existing sso configuration for ${provider}"... - set +e -o pipefail - id="$($auth list | grep "${provider}.*OAuth2" | cut -d' ' -f1)" - found=$? - set -e +o pipefail + echo "Trying to find existing sso configuration for ${provider}"... + set +e -o pipefail + id="$($auth list | grep "${provider}.*OAuth2" | cut -d' ' -f1)" + found=$? + set -e +o pipefail - if [[ $found = 0 ]]; then - echo Found sso configuration at id=$id, updating it if needed. - $auth update-oauth \ - --id $id \ - --name ${provider} \ - --provider openidConnect \ - --key forgejo \ - --secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \ - --auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration - else - echo Did not find any sso configuration, creating one with name ${provider}. - $auth add-oauth \ - --name ${provider} \ - --provider openidConnect \ - --key forgejo \ - --secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \ - --auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration - fi - ''; + if [[ $found = 0 ]]; then + echo Found sso configuration at id=$id, updating it if needed. + $auth update-oauth \ + --id $id \ + --name ${provider} \ + --provider openidConnect \ + --key forgejo \ + --secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \ + --auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration + else + echo Did not find any sso configuration, creating one with name ${provider}. + $auth add-oauth \ + --name ${provider} \ + --provider openidConnect \ + --key forgejo \ + --secret $(tr -d '\n' < ${cfg.sso.sharedSecret.result.path}) \ + --auto-discover-url ${cfg.sso.endpoint}/.well-known/openid-configuration + fi + ''; shb.authelia.oidcClients = lists.optionals (!(isNull cfg.sso)) [ - (let - provider = "SHB-${cfg.sso.provider}"; - in { - client_id = cfg.sso.clientID; - client_name = "Forgejo"; - client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; - public = false; - authorization_policy = cfg.sso.authorization_policy; - redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ]; - }) + ( + let + provider = "SHB-${cfg.sso.provider}"; + in + { + client_id = cfg.sso.clientID; + client_name = "Forgejo"; + client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; + public = false; + authorization_policy = cfg.sso.authorization_policy; + redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/user/oauth2/${provider}/callback" ]; + } + ) ]; }) @@ -552,13 +602,14 @@ in systemd.services.forgejo.preStart = '' admin="${getExe config.services.forgejo.package} admin user" - '' + concatMapStringsSep "\n" (u: '' + '' + + concatMapStringsSep "\n" (u: '' if ! $admin list | grep "${u.name}"; then $admin create ${optionalString u.value.isAdmin "--admin"} --email "${u.value.email}" --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})" else $admin change-password --must-change-password=false --username "${u.name}" --password "$(tr -d '\n' < ${u.value.password.result.path})" fi -'') (mapAttrsToList nameValuePair cfg.users); + '') (mapAttrsToList nameValuePair cfg.users); }) (mkIf (cfg.enable && cfg.smtp != null) { @@ -584,15 +635,17 @@ in instances.local = { enable = true; name = "local"; - url = let - protocol = if cfg.ssl != null then "https" else "http"; - in "${protocol}://${cfg.subdomain}.${cfg.domain}"; + url = + let + protocol = if cfg.ssl != null then "https" else "http"; + in + "${protocol}://${cfg.subdomain}.${cfg.domain}"; tokenFile = ""; # Empty variable to satisfy an assertion. labels = [ # "ubuntu-latest:docker://node:16-bullseye" # "ubuntu-22.04:docker://node:16-bullseye" # "ubuntu-20.04:docker://node:16-bullseye" - # "ubuntu-18.04:docker://node:16-buster" + # "ubuntu-18.04:docker://node:16-buster" "native:host" ]; inherit (cfg) hostPackages; diff --git a/modules/services/grocy.nix b/modules/services/grocy.nix index 708a138..244e08d 100644 --- a/modules/services/grocy.nix +++ b/modules/services/grocy.nix @@ -1,9 +1,14 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.grocy; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; in @@ -37,7 +42,24 @@ in }; culture = lib.mkOption { - type = lib.types.enum [ "de" "en" "da" "en_GB" "es" "fr" "hu" "it" "nl" "no" "pl" "pt_BR" "ru" "sk_SK" "sv_SE" "tr" ]; + type = lib.types.enum [ + "de" + "en" + "da" + "en_GB" + "es" + "fr" + "hu" + "it" + "nl" + "no" + "pl" + "pt_BR" + "ru" + "sk_SK" + "sv_SE" + "tr" + ]; default = "en"; description = '' Display language of the frontend. @@ -53,12 +75,12 @@ in extraServiceConfig = lib.mkOption { type = lib.types.attrsOf lib.types.str; description = "Extra configuration given to the systemd service file."; - default = {}; + default = { }; example = lib.literalExpression '' - { - MemoryHigh = "512M"; - MemoryMax = "900M"; - } + { + MemoryHigh = "512M"; + MemoryMax = "900M"; + } ''; }; @@ -78,34 +100,47 @@ in }; logLevel = lib.mkOption { - type = lib.types.nullOr (lib.types.enum ["critical" "error" "warning" "info" "debug"]); + type = lib.types.nullOr ( + lib.types.enum [ + "critical" + "error" + "warning" + "info" + "debug" + ] + ); description = "Enable logging."; default = false; example = true; }; }; - config = lib.mkIf cfg.enable (lib.mkMerge [{ - services.grocy = { - enable = true; - hostName = fqdn; - nginx.enableSSL = !(isNull cfg.ssl); - dataDir = cfg.dataDir; - settings.currency = cfg.currency; - settings.culture = cfg.culture; - }; + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + services.grocy = { + enable = true; + hostName = fqdn; + nginx.enableSSL = !(isNull cfg.ssl); + dataDir = cfg.dataDir; + settings.currency = cfg.currency; + settings.culture = cfg.culture; + }; - services.phpfpm.pools.grocy.group = lib.mkForce "grocy"; + services.phpfpm.pools.grocy.group = lib.mkForce "grocy"; - users.groups.grocy = {}; - users.users.grocy.group = lib.mkForce "grocy"; + users.groups.grocy = { }; + users.users.grocy.group = lib.mkForce "grocy"; - services.nginx.virtualHosts."${fqdn}" = { - enableACME = lib.mkForce false; - sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert; - sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key; - }; - } { - systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig; - }]); + services.nginx.virtualHosts."${fqdn}" = { + enableACME = lib.mkForce false; + sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert; + sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key; + }; + } + { + systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig; + } + ] + ); } diff --git a/modules/services/hledger.nix b/modules/services/hledger.nix index f956280..853fd9f 100644 --- a/modules/services/hledger.nix +++ b/modules/services/hledger.nix @@ -1,9 +1,14 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.hledger; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; in @@ -63,7 +68,7 @@ in description = '' Backup configuration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = "hledger"; @@ -76,7 +81,7 @@ in extraArguments = lib.mkOption { description = "Extra arguments append to the hledger command."; - default = ["--forecast"]; + default = [ "--forecast" ]; type = lib.types.listOf lib.types.str; }; }; @@ -88,7 +93,7 @@ in baseUrl = ""; stateDir = cfg.dataDir; - journalFiles = ["hledger.journal"]; + journalFiles = [ "hledger.journal" ]; host = "127.0.0.1"; port = cfg.port; @@ -101,20 +106,27 @@ in # If the hledger.journal file does not exist, hledger-web refuses to start, so we create an # empty one if it does not exist yet.. preStart = '' - test -f /var/lib/hledger/hledger.journal || touch /var/lib/hledger/hledger.journal + test -f /var/lib/hledger/hledger.journal || touch /var/lib/hledger/hledger.journal ''; serviceConfig.StateDirectory = "hledger"; }; shb.nginx.vhosts = [ { - inherit (cfg) subdomain domain authEndpoint ssl; + inherit (cfg) + subdomain + domain + authEndpoint + ssl + ; upstream = "http://${toString config.services.hledger-web.host}:${toString config.services.hledger-web.port}"; - autheliaRules = [{ - domain = fqdn; - policy = "two_factor"; - subject = ["group:hledger_user"]; - }]; + autheliaRules = [ + { + domain = fqdn; + policy = "two_factor"; + subject = [ "group:hledger_user" ]; + } + ]; } ]; }; diff --git a/modules/services/home-assistant.nix b/modules/services/home-assistant.nix index ccec792..d7a7301 100644 --- a/modules/services/home-assistant.nix +++ b/modules/services/home-assistant.nix @@ -1,9 +1,14 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.home-assistant; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; @@ -24,9 +29,7 @@ let nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config); - configWithSecretsIncludes = - nonSecrets - // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets); + configWithSecretsIncludes = nonSecrets // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets); in { options.shb.home-assistant = { @@ -56,28 +59,49 @@ in freeformType = lib.types.attrsOf lib.types.str; options = { name = lib.mkOption { - type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ]; + type = lib.types.oneOf [ + lib.types.str + lib.shb.secretFileType + ]; description = "Name of the Home Assistant instance."; }; country = lib.mkOption { - type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ]; + type = lib.types.oneOf [ + lib.types.str + lib.shb.secretFileType + ]; description = "Two letter country code where this instance is located."; }; latitude = lib.mkOption { - type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ]; + type = lib.types.oneOf [ + lib.types.str + lib.shb.secretFileType + ]; description = "Latitude where this instance is located."; }; longitude = lib.mkOption { - type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ]; + type = lib.types.oneOf [ + lib.types.str + lib.shb.secretFileType + ]; description = "Longitude where this instance is located."; }; time_zone = lib.mkOption { - type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ]; + type = lib.types.oneOf [ + lib.types.str + lib.shb.secretFileType + ]; description = "Timezone of this instance."; example = "America/Los_Angeles"; }; unit_system = lib.mkOption { - type = lib.types.oneOf [ lib.types.str (lib.types.enum [ "metric" "us_customary" ]) ]; + type = lib.types.oneOf [ + lib.types.str + (lib.types.enum [ + "metric" + "us_customary" + ]) + ]; description = "Unit system of this instance."; example = "metric"; }; @@ -95,7 +119,7 @@ in Also, enabling LDAP will skip onboarding otherwise Home Assistant gets into a cyclic lock. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "LDAP app."; @@ -140,7 +164,7 @@ in voice = lib.mkOption { description = "Options related to voice service."; - default = {}; + default = { }; type = lib.types.submodule { options = { speech-to-text = lib.mkOption { @@ -150,7 +174,7 @@ in https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.piper.servers ''; type = lib.types.attrsOf lib.types.anything; - default = {}; + default = { }; }; text-to-speech = lib.mkOption { description = '' @@ -159,7 +183,7 @@ in https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.faster-whisper.servers ''; type = lib.types.attrsOf lib.types.anything; - default = {}; + default = { }; }; wakeword = lib.mkOption { description = '' @@ -168,7 +192,9 @@ in https://search.nixos.org/options?channel=23.11&from=0&size=50&sort=relevance&type=packages&query=services.wyoming.openwakeword ''; type = lib.types.anything; - default = { enable = false; }; + default = { + enable = false; + }; }; }; }; @@ -178,7 +204,7 @@ in description = '' Backup configuration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = "hass"; @@ -219,7 +245,7 @@ in config = { # Includes dependencies for a basic setup # https://www.home-assistant.io/integrations/default_config/ - default_config = {}; + default_config = { }; http = { use_x_forwarded_for = true; server_host = "127.0.0.1"; @@ -240,7 +266,10 @@ in { type = "command_line"; command = ldap_auth_script + "/bin/ldap_auth.sh"; - args = [ "http://${cfg.ldap.host}:${toString cfg.ldap.port}" cfg.ldap.userGroup ]; + args = [ + "http://${cfg.ldap.host}:${toString cfg.ldap.port}" + cfg.ldap.userGroup + ]; meta = true; } ]); @@ -261,11 +290,11 @@ in action = [ { service = "shell_command.delete_backups"; - data = {}; + data = { }; } { service = "backup.create"; - data = {}; + data = { }; } ]; mode = "single"; @@ -286,7 +315,11 @@ in { name = "random_joke"; platform = "rest"; - json_attributes = ["joke" "id" "status"]; + json_attributes = [ + "joke" + "id" + "status" + ]; value_template = "{{ value_json.joke }}"; resource = "https://icanhazdadjoke.com/"; scan_interval = "3600"; @@ -324,33 +357,36 @@ in }; systemd.services.home-assistant.preStart = - (let - # TODO: this probably does not work anymore - onboarding = pkgs.writeText "onboarding" '' - { - "version": 4, - "minor_version": 1, - "key": "onboarding", - "data": { - "done": [ - ${lib.optionalString cfg.ldap.enable ''"user",''} - "core_config", - "analytics" - ] + ( + let + # TODO: this probably does not work anymore + onboarding = pkgs.writeText "onboarding" '' + { + "version": 4, + "minor_version": 1, + "key": "onboarding", + "data": { + "done": [ + ${lib.optionalString cfg.ldap.enable ''"user",''} + "core_config", + "analytics" + ] + } } - } - ''; - storage = "${config.services.home-assistant.configDir}"; - file = "${storage}/.storage/onboarding"; - in '' - if [ ! -f ${file} ]; then - mkdir -p ''$(dirname ${file}) && cp ${onboarding} ${file} - fi - '') + ''; + storage = "${config.services.home-assistant.configDir}"; + file = "${storage}/.storage/onboarding"; + in + '' + if [ ! -f ${file} ]; then + mkdir -p ''$(dirname ${file}) && cp ${onboarding} ${file} + fi + '' + ) + (lib.shb.replaceSecrets { userConfig = cfg.config; resultPath = "${config.services.home-assistant.configDir}/secrets.yaml"; - generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML {}); + generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toYAML { }); }); systemd.tmpfiles.rules = [ diff --git a/modules/services/immich.nix b/modules/services/immich.nix index 3ad8a44..55ebe7f 100644 --- a/modules/services/immich.nix +++ b/modules/services/immich.nix @@ -1,32 +1,45 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.immich; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; protocol = if !(isNull cfg.ssl) then "https" else "http"; roleClaim = "immich_user"; - # TODO: Quota management, see https://github.com/ibizaman/selfhostblocks/pull/523#discussion_r2309421694 + # TODO: Quota management, see https://github.com/ibizaman/selfhostblocks/pull/523#discussion_r2309421694 #quotaClaim = "immich_quota"; - scopes = [ "openid" "email" "profile" "groups" "immich_scope"]; + scopes = [ + "openid" + "email" + "profile" + "groups" + "immich_scope" + ]; dataFolder = cfg.mediaLocation; - ssoFqdnWithPort = if isNull cfg.sso.port - then cfg.sso.endpoint - else "${cfg.sso.endpoint}:${toString cfg.sso.port}"; + ssoFqdnWithPort = + if isNull cfg.sso.port then cfg.sso.endpoint else "${cfg.sso.endpoint}:${toString cfg.sso.port}"; # Generate Immich configuration file only for SHB-managed settings - shbManagedSettings = lib.optionalAttrs (cfg.settings != {}) cfg.settings + shbManagedSettings = + lib.optionalAttrs (cfg.settings != { }) cfg.settings // lib.optionalAttrs (cfg.sso.enable) { oauth = { enabled = true; issuerUrl = "${ssoFqdnWithPort}"; clientId = cfg.sso.clientID; roleClaim = roleClaim; - clientSecret = { source = cfg.sso.sharedSecret.result.path; }; + clientSecret = { + source = cfg.sso.sharedSecret.result.path; + }; scope = builtins.concatStringsSep " " scopes; storageLabelClaim = cfg.sso.storageLabelClaim; #storageQuotaClaim = quotaClaim; # TODO (commented out, otherwise defaults to 0 bytes!) @@ -49,7 +62,9 @@ let host = cfg.smtp.host; port = cfg.smtp.port; username = cfg.smtp.username; - password = { source = cfg.smtp.password.result.path; }; + password = { + source = cfg.smtp.password.result.path; + }; ignoreTLS = cfg.smtp.ignoreTLS; secure = cfg.smtp.secure; }; @@ -58,19 +73,36 @@ let }; configFile = "/var/lib/immich/config.json"; - + # Use SHB's replaceSecrets function for loading secrets at runtime configSetupScript = lib.optionalString (cfg.sso.enable || cfg.smtp != null) ( lib.shb.replaceSecrets { userConfig = shbManagedSettings; resultPath = configFile; - generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json {}); + generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json { }); user = "immich"; permissions = "u=r,g=,o="; } ); - inherit (lib) mkEnableOption mkIf lists mkOption optionals; - inherit (lib.types) attrs attrsOf bool enum listOf nullOr port submodule str path; + inherit (lib) + mkEnableOption + mkIf + lists + mkOption + optionals + ; + inherit (lib.types) + attrs + attrsOf + bool + enum + listOf + nullOr + port + submodule + str + path + ; in { imports = [ @@ -154,14 +186,16 @@ in ``` ''; readOnly = true; - default = { path = dataFolder; }; + default = { + path = dataFolder; + }; }; backup = mkOption { description = '' Backup configuration for Immich media files and database. ''; - default = {}; + default = { }; type = submodule { options = contracts.backup.mkRequester { user = "immich"; @@ -190,7 +224,7 @@ in machineLearning = mkOption { description = "Machine learning configuration."; - default = {}; + default = { }; type = submodule { options = { enable = mkOption { @@ -202,7 +236,7 @@ in environment = mkOption { description = "Extra environment variables for machine learning service."; type = attrsOf str; - default = {}; + default = { }; example = { MACHINE_LEARNING_WORKERS = "2"; MACHINE_LEARNING_WORKER_TIMEOUT = "180"; @@ -216,13 +250,17 @@ in description = '' Setup SSO integration. ''; - default = {}; + default = { }; type = submodule { options = { enable = mkEnableOption "SSO integration."; provider = mkOption { - type = enum [ "Authelia" "Keycloak" "Generic" ]; + type = enum [ + "Authelia" + "Keycloak" + "Generic" + ]; description = "OIDC provider name, used for display."; default = "Authelia"; }; @@ -311,7 +349,10 @@ in }; authorization_policy = mkOption { - type = enum [ "one_factor" "two_factor" ]; + type = enum [ + "one_factor" + "two_factor" + ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; @@ -325,10 +366,10 @@ in Immich configuration settings. Only specify settings that you want SHB to manage declaratively. Other settings can be configured through Immich's admin UI. - + See https://immich.app/docs/install/config-file/ for available options. ''; - default = {}; + default = { }; example = { ffmpeg.crf = 23; job.backgroundTask.concurrency = 5; @@ -351,31 +392,31 @@ in description = "SMTP address from which the emails originate."; example = "noreply@example.com"; }; - + replyTo = mkOption { type = str; description = "Reply-to address for emails."; example = "support@example.com"; }; - + host = mkOption { type = str; description = "SMTP host to send the emails to."; example = "smtp.example.com"; }; - + port = mkOption { type = port; description = "SMTP port to send the emails to."; default = 587; }; - + username = mkOption { type = str; description = "Username to connect to the SMTP host."; example = "smtp-user"; }; - + password = mkOption { description = "File containing the password to connect to the SMTP host."; type = submodule { @@ -386,13 +427,13 @@ in }; }; }; - + ignoreTLS = mkOption { type = bool; description = "Ignore TLS certificate errors."; default = false; }; - + secure = mkOption { type = bool; description = "Use secure connection (SSL/TLS)."; @@ -428,7 +469,7 @@ in host = "127.0.0.1"; port = cfg.port; mediaLocation = cfg.mediaLocation; - + # Hardware acceleration configuration accelerationDevices = cfg.accelerationDevices; @@ -436,7 +477,7 @@ in # Database configuration database = { - # Disable pgvecto.rs, as it was deprecated before SHB integration + # Disable pgvecto.rs, as it was deprecated before SHB integration enableVectors = false; }; @@ -452,9 +493,11 @@ in REDIS_HOSTNAME = "127.0.0.1"; REDIS_PORT = "6379"; REDIS_DBINDEX = "0"; - } // lib.optionalAttrs (cfg.jwtSecretFile != null) { + } + // lib.optionalAttrs (cfg.jwtSecretFile != null) { JWT_SECRET_FILE = cfg.jwtSecretFile.result.path; - } // lib.optionalAttrs (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) { + } + // lib.optionalAttrs (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) { IMMICH_CONFIG_FILE = configFile; }; }; @@ -464,27 +507,32 @@ in "d /var/lib/immich 0700 immich immich" ]; - # Configuration setup service - generates config only for SHB-managed settings - systemd.services.immich-setup-config = mkIf (cfg.enable && (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null)) { - description = "Setup Immich configuration for SHB-managed settings"; - wantedBy = [ "multi-user.target" ]; - before = [ "immich-server.service" ]; - after = [ "network.target" ]; - serviceConfig = { - Type = "oneshot"; - User = "immich"; - Group = "immich"; - }; - script = '' - mkdir -p ${dataFolder} - - # Generate config file with only SHB-managed settings - ${configSetupScript} - ''; - }; + # Configuration setup service - generates config only for SHB-managed settings + systemd.services.immich-setup-config = + mkIf (cfg.enable && (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null)) + { + description = "Setup Immich configuration for SHB-managed settings"; + wantedBy = [ "multi-user.target" ]; + before = [ "immich-server.service" ]; + after = [ "network.target" ]; + serviceConfig = { + Type = "oneshot"; + User = "immich"; + Group = "immich"; + }; + script = '' + mkdir -p ${dataFolder} + + # Generate config file with only SHB-managed settings + ${configSetupScript} + ''; + }; # Add immich user to video and render groups for hardware acceleration - users.users.immich.extraGroups = optionals (cfg.accelerationDevices != []) [ "video" "render" ]; + users.users.immich.extraGroups = optionals (cfg.accelerationDevices != [ ]) [ + "video" + "render" + ]; # PostgreSQL extensions are automatically handled by the Immich service @@ -499,7 +547,10 @@ in { domain = fqdn; policy = cfg.sso.authorization_policy; - subject = ["group:immich_user" "group:immich_admin"]; + subject = [ + "group:immich_user" + "group:immich_admin" + ]; } ]; authEndpoint = lib.mkIf (cfg.sso.enable) cfg.sso.endpoint; @@ -515,10 +566,20 @@ in # Ensure services start in correct order systemd.services.immich-server = { - after = [ "postgresql.service" "redis-immich.service" ] - ++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ]; - requires = [ "postgresql.service" "redis-immich.service" ] - ++ optionals (cfg.settings != {} || cfg.sso.enable || cfg.smtp != null) [ "immich-setup-config.service" ]; + after = [ + "postgresql.service" + "redis-immich.service" + ] + ++ optionals (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) [ + "immich-setup-config.service" + ]; + requires = [ + "postgresql.service" + "redis-immich.service" + ] + ++ optionals (cfg.settings != { } || cfg.sso.enable || cfg.smtp != null) [ + "immich-setup-config.service" + ]; }; systemd.services.immich-machine-learning = mkIf cfg.machineLearning.enable { @@ -527,21 +588,21 @@ in # Authelia integration for SSO shb.authelia.extraDefinitions = { - # Immich expects all users that get a token to be granted access. So users can either be part of the - # "admin" group or the "user" group. Users that are not part of either should be blocked by + # Immich expects all users that get a token to be granted access. So users can either be part of the + # "admin" group or the "user" group. Users that are not part of either should be blocked by # the ID provider (Authelia). - user_attributes.${roleClaim}.expression = ''"${cfg.sso.adminUserGroup}" in groups ? "admin" : "user"''; + user_attributes.${roleClaim}.expression = + ''"${cfg.sso.adminUserGroup}" in groups ? "admin" : "user"''; }; shb.authelia.extraOidcClaimsPolicies.immich_policy = { custom_claims = { - ${roleClaim} = {}; + ${roleClaim} = { }; }; }; shb.authelia.extraOidcScopes.immich_scope = { claims = [ roleClaim ]; }; - shb.authelia.oidcClients = lists.optionals (cfg.sso.enable && cfg.sso.provider == "Authelia") [ { client_id = cfg.sso.clientID; diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index 025bca1..fcdfe3d 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -1,11 +1,16 @@ -{ config, lib, pkgs, ...}: +{ + config, + lib, + pkgs, + ... +}: let inherit (lib) types; cfg = config.shb.jellyfin; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; @@ -91,31 +96,33 @@ in admin = lib.mkOption { description = "Default admin user info. Only needed if LDAP or SSO is not configured."; default = null; - type = types.nullOr (types.submodule { - options = { - username = lib.mkOption { - description = "Username of the default admin user."; - type = types.str; - default = "jellyfin"; - }; - password = lib.mkOption { - description = "Password of the default admin user."; - type = types.submodule { - options = contracts.secret.mkRequester { - mode = "0440"; - owner = "jellyfin"; - group = "jellyfin"; - restartUnits = [ "jellyfin.service" ]; + type = types.nullOr ( + types.submodule { + options = { + username = lib.mkOption { + description = "Username of the default admin user."; + type = types.str; + default = "jellyfin"; + }; + password = lib.mkOption { + description = "Password of the default admin user."; + type = types.submodule { + options = contracts.secret.mkRequester { + mode = "0440"; + owner = "jellyfin"; + group = "jellyfin"; + restartUnits = [ "jellyfin.service" ]; + }; }; }; }; - }; - }); + } + ); }; ldap = lib.mkOption { description = "LDAP configuration."; - default = {}; + default = { }; type = types.submodule { options = { enable = lib.mkEnableOption "LDAP"; @@ -167,7 +174,7 @@ in sso = lib.mkOption { description = "SSO configuration."; - default = {}; + default = { }; type = types.submodule { options = { enable = lib.mkEnableOption "SSO"; @@ -203,7 +210,10 @@ in }; authorization_policy = lib.mkOption { - type = types.enum [ "one_factor" "two_factor" ]; + type = types.enum [ + "one_factor" + "two_factor" + ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; @@ -238,23 +248,27 @@ in description = '' Backup configuration. ''; - default = {}; + default = { }; type = types.submodule { options = contracts.backup.mkRequester { user = "jellyfin"; sourceDirectories = [ config.services.jellyfin.dataDir ]; - sourceDirectoriesText = ''[ - "services.jellyfin.dataDir" - ]''; + sourceDirectoriesText = '' + [ + "services.jellyfin.dataDir" + ]''; }; }; }; }; imports = [ - (lib.mkRenamedOptionModule [ "shb" "jellyfin" "adminPassword" ] [ "shb" "jellyfin" "admin" "password" ]) + (lib.mkRenamedOptionModule + [ "shb" "jellyfin" "adminPassword" ] + [ "shb" "jellyfin" "admin" "password" ] + ) ]; config = lib.mkIf cfg.enable { @@ -269,7 +283,10 @@ in networking.firewall = { # from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery - allowedUDPPorts = [ 1900 7359 ]; + allowedUDPPorts = [ + 1900 + 7359 + ]; }; services.nginx.enable = true; @@ -380,21 +397,23 @@ in proxy_set_header X-Forwarded-Protocol $scheme; proxy_set_header X-Forwarded-Host $http_host; } - ''; + ''; }; - services.prometheus.scrapeConfigs = [{ - job_name = "jellyfin"; - static_configs = [ - { - targets = ["127.0.0.1:${toString cfg.port}"]; - labels = { - "hostname" = config.networking.hostName; - "domain" = cfg.domain; - }; - } - ]; - }]; + services.prometheus.scrapeConfigs = [ + { + job_name = "jellyfin"; + static_configs = [ + { + targets = [ "127.0.0.1:${toString cfg.port}" ]; + labels = { + "hostname" = config.networking.hostName; + "domain" = cfg.domain; + }; + } + ]; + } + ]; # LDAP config but you need to install the plugin by hand @@ -427,7 +446,7 @@ in - ''; + ''; # SchemeOverride is needed because of # https://github.com/9p4/jellyfin-plugin-sso/issues/264 @@ -560,23 +579,23 @@ in ''; in - lib.strings.optionalString cfg.debug - '' - if [ -f "${config.services.jellyfin.configDir}/logging.json" ] && [ ! -L "${config.services.jellyfin.configDir}/logging.json" ]; then - echo "A ${config.services.jellyfin.configDir}/logging.json file exists already, this indicates probably an existing installation. Please remove it before continuing." - exit 1 - fi - ln -fs "${debugLogging}" "${config.services.jellyfin.configDir}/logging.json" - '' - + (lib.shb.replaceSecretsScript { - file = networkConfig; - # Write permissions are needed otherwise the jellyfin-cli tool will not work correctly. - permissions = "u=rw,g=rw,o="; - resultPath = "${config.services.jellyfin.dataDir}/config/network.xml"; - replacements = [ - ]; - }) - + lib.strings.optionalString cfg.ldap.enable (lib.shb.replaceSecretsScript { + lib.strings.optionalString cfg.debug '' + if [ -f "${config.services.jellyfin.configDir}/logging.json" ] && [ ! -L "${config.services.jellyfin.configDir}/logging.json" ]; then + echo "A ${config.services.jellyfin.configDir}/logging.json file exists already, this indicates probably an existing installation. Please remove it before continuing." + exit 1 + fi + ln -fs "${debugLogging}" "${config.services.jellyfin.configDir}/logging.json" + '' + + (lib.shb.replaceSecretsScript { + file = networkConfig; + # Write permissions are needed otherwise the jellyfin-cli tool will not work correctly. + permissions = "u=rw,g=rw,o="; + resultPath = "${config.services.jellyfin.dataDir}/config/network.xml"; + replacements = [ + ]; + }) + + lib.strings.optionalString cfg.ldap.enable ( + lib.shb.replaceSecretsScript { file = ldapConfig; resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml"; replacements = [ @@ -585,8 +604,10 @@ in source = cfg.ldap.adminPassword.result.path; } ]; - }) - + lib.strings.optionalString cfg.sso.enable (lib.shb.replaceSecretsScript { + } + ) + + lib.strings.optionalString cfg.sso.enable ( + lib.shb.replaceSecretsScript { file = ssoConfig; resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/SSO-Auth.xml"; replacements = [ @@ -595,92 +616,96 @@ in source = cfg.sso.sharedSecret.result.path; } ]; - }) - + lib.strings.optionalString cfg.sso.enable (lib.shb.replaceSecretsScript { + } + ) + + lib.strings.optionalString cfg.sso.enable ( + lib.shb.replaceSecretsScript { file = brandingConfig; resultPath = "${config.services.jellyfin.dataDir}/config/branding.xml"; replacements = [ ]; - }); + } + ); - systemd.services.jellyfin.serviceConfig.ExecStartPost = let - # We must always wait for the service to be fully initialized, - # even if we're planning on changing the config and restarting. - waitForCurl = pkgs.writeShellApplication { - name = "waitForCurl"; - runtimeInputs = [ pkgs.curl ]; - text = '' - URL="http://127.0.0.1:${toString cfg.port}/System/Info/Public" - SLEEP_INTERVAL_SEC=2 - TIMEOUT=60 + systemd.services.jellyfin.serviceConfig.ExecStartPost = + let + # We must always wait for the service to be fully initialized, + # even if we're planning on changing the config and restarting. + waitForCurl = pkgs.writeShellApplication { + name = "waitForCurl"; + runtimeInputs = [ pkgs.curl ]; + text = '' + URL="http://127.0.0.1:${toString cfg.port}/System/Info/Public" + SLEEP_INTERVAL_SEC=2 + TIMEOUT=60 - start_time=$(date +%s) + start_time=$(date +%s) - echo "Waiting for $URL to return HTTP 200..." + echo "Waiting for $URL to return HTTP 200..." - while true; do - status_code=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || true) - if [ "$status_code" = "200" ]; then - echo "Service is up (HTTP 200 received)." - exit 0 - fi + while true; do + status_code=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || true) + if [ "$status_code" = "200" ]; then + echo "Service is up (HTTP 200 received)." + exit 0 + fi - now=$(date +%s) - elapsed=$(( now - start_time )) + now=$(date +%s) + elapsed=$(( now - start_time )) - if [ $elapsed -ge $TIMEOUT ]; then - echo "Timeout reached ($TIMEOUT seconds). Exiting with failure." - exit 1 - fi + if [ $elapsed -ge $TIMEOUT ]; then + echo "Timeout reached ($TIMEOUT seconds). Exiting with failure." + exit 1 + fi - echo "Waiting for service... (status: $status_code), elapsed: ''${elapsed}s" - sleep "$SLEEP_INTERVAL_SEC" - done + echo "Waiting for service... (status: $status_code), elapsed: ''${elapsed}s" + sleep "$SLEEP_INTERVAL_SEC" + done - echo "Finished waiting, curl returned a 200." - ''; - }; + echo "Finished waiting, curl returned a 200." + ''; + }; - # This file is used to know if the jellyfin service has been restarted - # because a new config just got written to. - # - # If the file does not exist, write the config, create the file then restart. - # If the file exists, do nothing and remove the file, resetting the state for the next time. - restartedFile="${config.services.jellyfin.dataDir}/.jellyfin-restarted"; + # This file is used to know if the jellyfin service has been restarted + # because a new config just got written to. + # + # If the file does not exist, write the config, create the file then restart. + # If the file exists, do nothing and remove the file, resetting the state for the next time. + restartedFile = "${config.services.jellyfin.dataDir}/.jellyfin-restarted"; - writeConfig = pkgs.writeShellApplication { - name = "writeConfig"; - runtimeInputs = [ pkgs.systemd ]; - text = '' - if ! [ -f "${restartedFile}" ]; then - ${lib.getExe jellyfin-cli} wizard \ - --datadir='${config.services.jellyfin.dataDir}' \ - --configdir='${config.services.jellyfin.configDir}' \ - --cachedir='${config.services.jellyfin.cacheDir}' \ - --logdir='${config.services.jellyfin.logDir}' \ - --username=${cfg.admin.username} \ - --password-file=${cfg.admin.password.result.path} \ - --enable-remote-access=true \ - --write - fi - ''; - }; + writeConfig = pkgs.writeShellApplication { + name = "writeConfig"; + runtimeInputs = [ pkgs.systemd ]; + text = '' + if ! [ -f "${restartedFile}" ]; then + ${lib.getExe jellyfin-cli} wizard \ + --datadir='${config.services.jellyfin.dataDir}' \ + --configdir='${config.services.jellyfin.configDir}' \ + --cachedir='${config.services.jellyfin.cacheDir}' \ + --logdir='${config.services.jellyfin.logDir}' \ + --username=${cfg.admin.username} \ + --password-file=${cfg.admin.password.result.path} \ + --enable-remote-access=true \ + --write + fi + ''; + }; - restartJellyfinOnce = pkgs.writeShellApplication { - name = "restartJellyfin"; - runtimeInputs = [ pkgs.systemd ]; - text = '' - if [ -f "${restartedFile}" ]; then - echo "jellyfin.service has been restarted" - rm "${restartedFile}" - else - echo "Restarting jellyfin.service" - touch "${restartedFile}" - systemctl reload-or-restart jellyfin.service - fi - ''; - }; - in + restartJellyfinOnce = pkgs.writeShellApplication { + name = "restartJellyfin"; + runtimeInputs = [ pkgs.systemd ]; + text = '' + if [ -f "${restartedFile}" ]; then + echo "jellyfin.service has been restarted" + rm "${restartedFile}" + else + echo "Restarting jellyfin.service" + touch "${restartedFile}" + systemctl reload-or-restart jellyfin.service + fi + ''; + }; + in lib.optionals (cfg.admin != null) [ (lib.getExe waitForCurl) diff --git a/modules/services/karakeep.nix b/modules/services/karakeep.nix index 639139a..02169a0 100644 --- a/modules/services/karakeep.nix +++ b/modules/services/karakeep.nix @@ -1,8 +1,13 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.shb.karakeep; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; in { imports = [ @@ -37,18 +42,18 @@ in }; environment = lib.mkOption { - default = {}; + default = { }; type = lib.types.attrsOf lib.types.str; description = "Extra environment variables. See https://docs.karakeep.app/configuration/"; example = '' - { - OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}"; - INFERENCE_TEXT_MODEL = "deepseek-r1:1.5b"; - INFERENCE_IMAGE_MODEL = "llava"; - EMBEDDING_TEXT_MODEL = "nomic-embed-text:v1.5"; - INFERENCE_ENABLE_AUTO_SUMMARIZATION = "true"; - INFERENCE_JOB_TIMEOUT_SEC = "200"; - } + { + OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}"; + INFERENCE_TEXT_MODEL = "deepseek-r1:1.5b"; + INFERENCE_IMAGE_MODEL = "llava"; + EMBEDDING_TEXT_MODEL = "nomic-embed-text:v1.5"; + INFERENCE_ENABLE_AUTO_SUMMARIZATION = "true"; + INFERENCE_JOB_TIMEOUT_SEC = "200"; + } ''; }; @@ -56,7 +61,7 @@ in description = '' Setup LDAP integration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { userGroup = lib.mkOption { @@ -72,7 +77,7 @@ in description = '' Setup SSO integration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "SSO integration."; @@ -91,7 +96,10 @@ in }; authorization_policy = lib.mkOption { - type = lib.types.enum [ "one_factor" "two_factor" ]; + type = lib.types.enum [ + "one_factor" + "two_factor" + ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; @@ -102,7 +110,11 @@ in options = contracts.secret.mkRequester { owner = "karakeep"; # These services are the ones relying on the environment file containing the secrets. - restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ]; + restartUnits = [ + "karakeep-init.service" + "karakeep-workers.service" + "karakeep-workers.service" + ]; }; }; }; @@ -125,7 +137,7 @@ in description = '' Backup state directory. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = "karakeep"; @@ -142,7 +154,11 @@ in options = contracts.secret.mkRequester { owner = "karakeep"; # These services are the ones relying on the environment file containing the secrets. - restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ]; + restartUnits = [ + "karakeep-init.service" + "karakeep-workers.service" + "karakeep-workers.service" + ]; }; }; }; @@ -153,93 +169,108 @@ in options = contracts.secret.mkRequester { owner = "karakeep"; # These services are the ones relying on the environment file containing the secrets. - restartUnits = [ "karakeep-init.service" "karakeep-workers.service" "karakeep-workers.service" ]; + restartUnits = [ + "karakeep-init.service" + "karakeep-workers.service" + "karakeep-workers.service" + ]; }; }; }; }; - config = (lib.mkMerge [ - (lib.mkIf cfg.enable { - services.karakeep = { - enable = true; - meilisearch.enable = true; + config = ( + lib.mkMerge [ + (lib.mkIf cfg.enable { + services.karakeep = { + enable = true; + meilisearch.enable = true; - extraEnvironment = { - PORT = toString cfg.port; - DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS - } // cfg.environment; - }; + extraEnvironment = { + PORT = toString cfg.port; + DISABLE_NEW_RELEASE_CHECK = "true"; # These are handled by NixOS + } + // cfg.environment; + }; - shb.nginx.vhosts = [ - { - inherit (cfg) subdomain domain ssl; - upstream = "http://127.0.0.1:${toString cfg.port}/"; - } - ]; - - # Piggybacking onto the upstream karakeep-init and replacing its script by ours. - # This is needed otherwise the MEILI_MASTER_KEY is generated randomly on first start - # instead of using the value from the cfg.meilisearchMasterKey option. - systemd.services.karakeep-init = { - script = lib.mkForce ((lib.shb.replaceSecrets { - userConfig = { - MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path; - NEXTAUTH_SECRET.source = cfg.nextauthSecret.result.path; - } // lib.optionalAttrs cfg.sso.enable { - OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path; - }; - resultPath = "/var/lib/karakeep/settings.env"; - generator = lib.shb.toEnvVar; - }) + '' - export DATA_DIR="$STATE_DIRECTORY" - exec ${config.services.karakeep.package}/lib/karakeep/migrate - ''); - }; - }) - (lib.mkIf cfg.enable { - services.meilisearch = { - dumplessUpgrade = true; - environment = "production"; - masterKeyFile = cfg.meilisearchMasterKey.result.path; - }; - }) - (lib.mkIf (cfg.enable && cfg.sso.enable) { - shb.lldap.ensureGroups = { - ${cfg.ldap.userGroup} = {}; - }; - - shb.authelia.extraOidcAuthorizationPolicies.karakeep = { - default_policy = "deny"; - rules = [ + shb.nginx.vhosts = [ { - subject = [ "group:${cfg.ldap.userGroup}" ]; - policy = cfg.sso.authorization_policy; + inherit (cfg) subdomain domain ssl; + upstream = "http://127.0.0.1:${toString cfg.port}/"; } ]; - }; - shb.authelia.oidcClients = [ - { - client_id = cfg.sso.clientID; - client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; - scopes = [ "openid" "email" "profile" ]; - authorization_policy = "karakeep"; - redirect_uris = [ - "https://${cfg.subdomain}.${cfg.domain}/api/auth/callback/custom" - ]; - } - ]; - services.karakeep = { - extraEnvironment = { - DISABLE_SIGNUPS = "false"; - DISABLE_PASSWORD_AUTH = "true"; - NEXTAUTH_URL = "https://${cfg.subdomain}.${cfg.domain}"; - OAUTH_WELLKNOWN_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration"; - OAUTH_PROVIDER_NAME = "Single Sign-On"; - OAUTH_CLIENT_ID = cfg.sso.clientID; - OAUTH_SCOPE = "openid email profile"; + + # Piggybacking onto the upstream karakeep-init and replacing its script by ours. + # This is needed otherwise the MEILI_MASTER_KEY is generated randomly on first start + # instead of using the value from the cfg.meilisearchMasterKey option. + systemd.services.karakeep-init = { + script = lib.mkForce ( + (lib.shb.replaceSecrets { + userConfig = { + MEILI_MASTER_KEY.source = cfg.meilisearchMasterKey.result.path; + NEXTAUTH_SECRET.source = cfg.nextauthSecret.result.path; + } + // lib.optionalAttrs cfg.sso.enable { + OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path; + }; + resultPath = "/var/lib/karakeep/settings.env"; + generator = lib.shb.toEnvVar; + }) + + '' + export DATA_DIR="$STATE_DIRECTORY" + exec ${config.services.karakeep.package}/lib/karakeep/migrate + '' + ); }; - }; - }) - ]); + }) + (lib.mkIf cfg.enable { + services.meilisearch = { + dumplessUpgrade = true; + environment = "production"; + masterKeyFile = cfg.meilisearchMasterKey.result.path; + }; + }) + (lib.mkIf (cfg.enable && cfg.sso.enable) { + shb.lldap.ensureGroups = { + ${cfg.ldap.userGroup} = { }; + }; + + shb.authelia.extraOidcAuthorizationPolicies.karakeep = { + default_policy = "deny"; + rules = [ + { + subject = [ "group:${cfg.ldap.userGroup}" ]; + policy = cfg.sso.authorization_policy; + } + ]; + }; + shb.authelia.oidcClients = [ + { + client_id = cfg.sso.clientID; + client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; + scopes = [ + "openid" + "email" + "profile" + ]; + authorization_policy = "karakeep"; + redirect_uris = [ + "https://${cfg.subdomain}.${cfg.domain}/api/auth/callback/custom" + ]; + } + ]; + services.karakeep = { + extraEnvironment = { + DISABLE_SIGNUPS = "false"; + DISABLE_PASSWORD_AUTH = "true"; + NEXTAUTH_URL = "https://${cfg.subdomain}.${cfg.domain}"; + OAUTH_WELLKNOWN_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration"; + OAUTH_PROVIDER_NAME = "Single Sign-On"; + OAUTH_CLIENT_ID = cfg.sso.clientID; + OAUTH_SCOPE = "openid email profile"; + }; + }; + }) + ] + ); } diff --git a/modules/services/nextcloud-server.nix b/modules/services/nextcloud-server.nix index 13110d5..aaffb4b 100644 --- a/modules/services/nextcloud-server.nix +++ b/modules/services/nextcloud-server.nix @@ -1,4 +1,9 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.nextcloud; @@ -7,12 +12,17 @@ let fqdnWithPort = if isNull cfg.port then fqdn else "${fqdn}:${toString cfg.port}"; protocol = if !(isNull cfg.ssl) then "https" else "http"; - ssoFqdnWithPort = if isNull cfg.apps.sso.port then cfg.apps.sso.endpoint else "${cfg.apps.sso.endpoint}:${toString cfg.apps.sso.port}"; + ssoFqdnWithPort = + if isNull cfg.apps.sso.port then + cfg.apps.sso.endpoint + else + "${cfg.apps.sso.endpoint}:${toString cfg.apps.sso.port}"; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; nextcloudPkg = builtins.getAttr ("nextcloud" + builtins.toString cfg.version) pkgs; - nextcloudApps = (builtins.getAttr ("nextcloud" + builtins.toString cfg.version + "Packages") pkgs).apps; + nextcloudApps = + (builtins.getAttr ("nextcloud" + builtins.toString cfg.version + "Packages") pkgs).apps; occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ"; in @@ -71,7 +81,10 @@ in version = lib.mkOption { description = "Nextcloud version to choose from."; - type = lib.types.enum [ 31 32 ]; + type = lib.types.enum [ + 31 + 32 + ]; default = 31; }; @@ -84,7 +97,7 @@ in mountPointServices = lib.mkOption { description = "If given, all the systemd services and timers will depend on the specified mount point systemd services."; type = lib.types.listOf lib.types.str; - default = []; + default = [ ]; example = lib.literalExpression ''["var.mount"]''; }; @@ -105,7 +118,6 @@ in }; }; - maxUploadSize = lib.mkOption { default = "4G"; type = lib.types.str; @@ -132,35 +144,35 @@ in Go to https://pgtune.leopard.in.ua/ and copy the generated configuration here. ''; example = lib.literalExpression '' - { - # From https://pgtune.leopard.in.ua/ with: + { + # From https://pgtune.leopard.in.ua/ with: - # DB Version: 14 - # OS Type: linux - # DB Type: dw - # Total Memory (RAM): 7 GB - # CPUs num: 4 - # Connections num: 100 - # Data Storage: ssd + # DB Version: 14 + # OS Type: linux + # DB Type: dw + # Total Memory (RAM): 7 GB + # CPUs num: 4 + # Connections num: 100 + # Data Storage: ssd - max_connections = "100"; - shared_buffers = "1792MB"; - effective_cache_size = "5376MB"; - maintenance_work_mem = "896MB"; - checkpoint_completion_target = "0.9"; - wal_buffers = "16MB"; - default_statistics_target = "500"; - random_page_cost = "1.1"; - effective_io_concurrency = "200"; - work_mem = "4587kB"; - huge_pages = "off"; - min_wal_size = "4GB"; - max_wal_size = "16GB"; - max_worker_processes = "4"; - max_parallel_workers_per_gather = "2"; - max_parallel_workers = "4"; - max_parallel_maintenance_workers = "2"; - } + max_connections = "100"; + shared_buffers = "1792MB"; + effective_cache_size = "5376MB"; + maintenance_work_mem = "896MB"; + checkpoint_completion_target = "0.9"; + wal_buffers = "16MB"; + default_statistics_target = "500"; + random_page_cost = "1.1"; + effective_io_concurrency = "200"; + work_mem = "4587kB"; + huge_pages = "off"; + min_wal_size = "4GB"; + max_wal_size = "16GB"; + max_worker_processes = "4"; + max_parallel_workers_per_gather = "2"; + max_parallel_workers = "4"; + max_parallel_maintenance_workers = "2"; + } ''; }; @@ -173,22 +185,22 @@ in "pm.start_servers" = 5; }; example = lib.literalExpression '' - { - "pm" = "dynamic"; - "pm.max_children" = 50; - "pm.start_servers" = 25; - "pm.min_spare_servers" = 10; - "pm.max_spare_servers" = 20; - "pm.max_spawn_rate" = 50; - "pm.max_requests" = 50; - "pm.process_idle_timeout" = "20s"; - } + { + "pm" = "dynamic"; + "pm.max_children" = 50; + "pm.start_servers" = 25; + "pm.min_spare_servers" = 10; + "pm.max_spare_servers" = 20; + "pm.max_spawn_rate" = 50; + "pm.max_requests" = 50; + "pm.process_idle_timeout" = "20s"; + } ''; }; phpFpmPrometheusExporter = lib.mkOption { description = "Settings for exporting"; - default = {}; + default = { }; type = lib.types.submodule { options = { @@ -216,7 +228,7 @@ in through the UI. You can still make changes but they will be overridden on next deploy. You can still install and configure other apps through the UI. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { onlyoffice = lib.mkOption { @@ -226,7 +238,7 @@ in Enabling this app will also start an OnlyOffice instance accessible at the given subdomain from the given network range. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "Nextcloud OnlyOffice App"; @@ -275,7 +287,7 @@ in nextcloud-occ -vvv preview:generate-all ``` ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "Nextcloud Preview Generator App"; @@ -330,34 +342,39 @@ in other side, a spinning hard drive can store more data which is well suited for storing user data. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "Nextcloud External Storage App"; userLocalMount = lib.mkOption { default = null; description = "If set, adds a local mount as external storage."; - type = lib.types.nullOr (lib.types.submodule { - options = { - directory = lib.mkOption { - type = lib.types.str; - description = '' - Local directory on the filesystem to mount. Use `$user` and/or `$home` - which will be replaced by the user's name and home directory. - ''; - example = "/srv/nextcloud/$user"; - }; + type = lib.types.nullOr ( + lib.types.submodule { + options = { + directory = lib.mkOption { + type = lib.types.str; + description = '' + Local directory on the filesystem to mount. Use `$user` and/or `$home` + which will be replaced by the user's name and home directory. + ''; + example = "/srv/nextcloud/$user"; + }; - mountName = lib.mkOption { - type = lib.types.str; - description = '' - Path of the mount in Nextcloud. Use `/` to mount as the root. - ''; - default = ""; - example = [ "home" "/" ]; + mountName = lib.mkOption { + type = lib.types.str; + description = '' + Path of the mount in Nextcloud. Use `/` to mount as the root. + ''; + default = ""; + example = [ + "home" + "/" + ]; + }; }; - }; - }); + } + ); }; }; }; @@ -370,66 +387,68 @@ in Enabling this app will create a new LDAP configuration or update one that exists with the given host. ''; - default = {}; - type = lib.types.nullOr (lib.types.submodule { - options = { - enable = lib.mkEnableOption "LDAP app."; + default = { }; + type = lib.types.nullOr ( + lib.types.submodule { + options = { + enable = lib.mkEnableOption "LDAP app."; - host = lib.mkOption { - type = lib.types.str; - description = '' - Host serving the LDAP server. - ''; - default = "127.0.0.1"; - }; + host = lib.mkOption { + type = lib.types.str; + description = '' + Host serving the LDAP server. + ''; + default = "127.0.0.1"; + }; - port = lib.mkOption { - type = lib.types.port; - description = '' - Port of the service serving the LDAP server. - ''; - default = 389; - }; + port = lib.mkOption { + type = lib.types.port; + description = '' + Port of the service serving the LDAP server. + ''; + default = 389; + }; - dcdomain = lib.mkOption { - type = lib.types.str; - description = "dc domain for ldap."; - example = "dc=mydomain,dc=com"; - }; + dcdomain = lib.mkOption { + type = lib.types.str; + description = "dc domain for ldap."; + example = "dc=mydomain,dc=com"; + }; - adminName = lib.mkOption { - type = lib.types.str; - description = "Admin user of the LDAP server."; - default = "admin"; - }; + adminName = lib.mkOption { + type = lib.types.str; + description = "Admin user of the LDAP server."; + default = "admin"; + }; - adminPassword = lib.mkOption { - description = "LDAP server admin password."; - type = lib.types.submodule { - options = contracts.secret.mkRequester { - mode = "0400"; - owner = "nextcloud"; - restartUnits = [ "phpfpm-nextcloud.service" ]; + adminPassword = lib.mkOption { + description = "LDAP server admin password."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "nextcloud"; + restartUnits = [ "phpfpm-nextcloud.service" ]; + }; }; }; - }; - userGroup = lib.mkOption { - type = lib.types.str; - description = "Group users must belong to to be able to login to Nextcloud."; - default = "nextcloud_user"; - }; + userGroup = lib.mkOption { + type = lib.types.str; + description = "Group users must belong to to be able to login to Nextcloud."; + default = "nextcloud_user"; + }; - configID = lib.mkOption { - type = lib.types.int; - description = '' - Multiple LDAP configs can co-exist with only one active at a time. - This option sets the config ID used by Self Host Blocks. - ''; - default = 50; + configID = lib.mkOption { + type = lib.types.int; + description = '' + Multiple LDAP configs can co-exist with only one active at a time. + This option sets the config ID used by Self Host Blocks. + ''; + default = 50; + }; }; - }; - }); + } + ); }; sso = lib.mkOption { @@ -439,7 +458,7 @@ in Enabling this app will create a new LDAP configuration or update one that exists with the given host. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "SSO app."; @@ -469,7 +488,10 @@ in }; authorization_policy = lib.mkOption { - type = lib.types.enum [ "one_factor" "two_factor" ]; + type = lib.types.enum [ + "one_factor" + "two_factor" + ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; @@ -491,7 +513,6 @@ in }; }; - secretForAuthelia = lib.mkOption { description = "OIDC shared secret. Content must be the same as `secretFile` option."; type = lib.types.submodule { @@ -502,7 +523,6 @@ in }; }; - fallbackDefaultAuth = lib.mkOption { type = lib.types.bool; description = '' @@ -528,7 +548,7 @@ in nextcloud-occ memories:index ``` ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "Memories app."; @@ -561,7 +581,7 @@ in Enabling this app will set up the Recognize app and configure all its dependencies. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "Recognize app."; @@ -599,19 +619,18 @@ in ''; }; - backup = lib.mkOption { description = '' Backup configuration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = "nextcloud"; sourceDirectories = [ cfg.dataDir ]; - excludePatterns = [".rnd"]; + excludePatterns = [ ".rnd" ]; }; }; }; @@ -722,34 +741,36 @@ in # Very important for a bunch of scripts to load correctly. Otherwise you get Content-Security-Policy errors. See https://docs.nextcloud.com/server/13/admin_manual/configuration_server/harden_server.html#enable-http-strict-transport-security https = !(isNull cfg.ssl); - extraApps = if isNull cfg.extraApps then {} else cfg.extraApps nextcloudApps; + extraApps = if isNull cfg.extraApps then { } else cfg.extraApps nextcloudApps; extraAppsEnable = true; appstoreEnable = true; - settings = let - protocol = if !(isNull cfg.ssl) then "https" else "http"; - in { - "default_phone_region" = cfg.defaultPhoneRegion; + settings = + let + protocol = if !(isNull cfg.ssl) then "https" else "http"; + in + { + "default_phone_region" = cfg.defaultPhoneRegion; - "overwrite.cli.url" = "${protocol}://${fqdn}"; - "overwritehost" = fqdnWithPort; - # 'trusted_domains' needed otherwise we get this issue https://help.nextcloud.com/t/the-polling-url-does-not-start-with-https-despite-the-login-url-started-with-https/137576/2 - # TODO: could instead set extraTrustedDomains - "trusted_domains" = [ fqdn ]; - "trusted_proxies" = [ "127.0.0.1" ]; - # TODO: could instead set overwriteProtocol - "overwriteprotocol" = protocol; # Needed if behind a reverse_proxy - "overwritecondaddr" = ""; # We need to set it to empty otherwise overwriteprotocol does not work. - "debug" = cfg.debug; - "loglevel" = if !cfg.debug then 2 else 0; - "filelocking.debug" = cfg.debug; + "overwrite.cli.url" = "${protocol}://${fqdn}"; + "overwritehost" = fqdnWithPort; + # 'trusted_domains' needed otherwise we get this issue https://help.nextcloud.com/t/the-polling-url-does-not-start-with-https-despite-the-login-url-started-with-https/137576/2 + # TODO: could instead set extraTrustedDomains + "trusted_domains" = [ fqdn ]; + "trusted_proxies" = [ "127.0.0.1" ]; + # TODO: could instead set overwriteProtocol + "overwriteprotocol" = protocol; # Needed if behind a reverse_proxy + "overwritecondaddr" = ""; # We need to set it to empty otherwise overwriteprotocol does not work. + "debug" = cfg.debug; + "loglevel" = if !cfg.debug then 2 else 0; + "filelocking.debug" = cfg.debug; - # Use persistent SQL connections. - "dbpersistent" = "true"; + # Use persistent SQL connections. + "dbpersistent" = "true"; - # https://help.nextcloud.com/t/very-slow-sync-for-small-files/11064/13 - "chunkSize" = "5120MB"; - }; + # https://help.nextcloud.com/t/very-slow-sync-for-small-files/11064/13 + "chunkSize" = "5120MB"; + }; phpOptions = { # The OPcache interned strings buffer is nearly full with 8, bump to 16. @@ -773,7 +794,8 @@ in "redis.session.locking_enabled" = "1"; "redis.session.lock_retries" = "-1"; "redis.session.lock_wait_time" = "10000"; - } // lib.optionalAttrs (! (isNull cfg.tracing)) { + } + // lib.optionalAttrs (!(isNull cfg.tracing)) { # "xdebug.remote_enable" = "on"; # "xdebug.remote_host" = "127.0.0.1"; # "xdebug.remote_port" = "9000"; @@ -785,7 +807,7 @@ in "xdebug.start_with_request" = "trigger"; }; - poolSettings = lib.mkIf (! (isNull cfg.phpFpmPoolSettings)) cfg.phpFpmPoolSettings; + poolSettings = lib.mkIf (!(isNull cfg.phpFpmPoolSettings)) cfg.phpFpmPoolSettings; phpExtraExtensions = all: [ all.xdebug ]; }; @@ -801,7 +823,7 @@ in # [1]: https://help.nextcloud.com/t/download-aborts-after-time-or-large-file/25044/6 # [2]: https://stackoverflow.com/a/50891625/1013628 extraConfig = '' - proxy_buffering off; + proxy_buffering off; ''; }; @@ -810,10 +832,10 @@ in pkgs.ffmpeg-headless ]; - services.postgresql.settings = lib.mkIf (! (isNull cfg.postgresSettings)) cfg.postgresSettings; + services.postgresql.settings = lib.mkIf (!(isNull cfg.postgresSettings)) cfg.postgresSettings; systemd.services.phpfpm-nextcloud.preStart = '' - mkdir -p /var/log/xdebug; chown -R nextcloud: /var/log/xdebug + mkdir -p /var/log/xdebug; chown -R nextcloud: /var/log/xdebug ''; systemd.services.phpfpm-nextcloud.requires = cfg.mountPointServices; systemd.services.phpfpm-nextcloud.after = cfg.mountPointServices; @@ -837,7 +859,9 @@ in port = cfg.phpFpmPrometheusExporter.port; listenAddress = "127.0.0.1"; extraFlags = [ - "--phpfpm.scrape-uri=tcp://127.0.0.1:${toString (cfg.phpFpmPrometheusExporter.port -1)}/status?full" + "--phpfpm.scrape-uri=tcp://127.0.0.1:${ + toString (cfg.phpFpmPrometheusExporter.port - 1) + }/status?full" ]; }; @@ -849,20 +873,22 @@ in # # I also tried to server the status page at /status.php # but fcgi doesn't like the returned headers. - "pm.status_listen" = "127.0.0.1:${toString (cfg.phpFpmPrometheusExporter.port -1)}"; + "pm.status_listen" = "127.0.0.1:${toString (cfg.phpFpmPrometheusExporter.port - 1)}"; }; }; services.prometheus.scrapeConfigs = [ { job_name = "phpfpm-nextcloud"; - static_configs = [{ - targets = ["127.0.0.1:${toString cfg.phpFpmPrometheusExporter.port}"]; - labels = { - "hostname" = config.networking.hostName; - "domain" = cfg.domain; - }; - }]; + static_configs = [ + { + targets = [ "127.0.0.1:${toString cfg.phpFpmPrometheusExporter.port}" ]; + labels = { + "hostname" = config.networking.hostName; + "domain" = cfg.domain; + }; + } + ]; } ]; }) @@ -896,7 +922,7 @@ in locations."/" = { extraConfig = '' - allow ${cfg.apps.onlyoffice.localNetworkIPRange}; + allow ${cfg.apps.onlyoffice.localNetworkIPRange}; ''; }; }; @@ -956,7 +982,7 @@ in let debug = if cfg.debug or cfg.apps.previewgenerator.debug then "-vvv" else ""; in - "${occ} ${debug} preview:pre-generate"; + "${occ} ${debug} preview:pre-generate"; }; }) @@ -964,13 +990,14 @@ in systemd.services.nextcloud-setup.script = '' ${occ} app:install files_external || : ${occ} app:enable files_external - '' + lib.optionalString (cfg.apps.externalStorage.userLocalMount != null) ( + '' + + lib.optionalString (cfg.apps.externalStorage.userLocalMount != null) ( let cfg' = cfg.apps.externalStorage.userLocalMount; jq = "${pkgs.jq}/bin/jq"; in - # sh - '' + # sh + '' exists=$(${occ} files_external:list --output=json | ${jq} 'any(.[]; .mount_point == "${cfg'.mountName}" and .configuration.datadir == "${cfg'.directory}")') if [[ "$exists" == "false" ]]; then ${occ} files_external:create \ @@ -979,7 +1006,8 @@ in null::null \ --config datadir='${cfg'.directory}' fi - ''); + '' + ); }) (lib.mkIf (cfg.enable && cfg.apps.ldap.enable) { @@ -988,74 +1016,76 @@ in let cfg' = cfg.apps.ldap; cID = "s" + toString cfg'.configID; - in '' - ${occ} app:install user_ldap || : - ${occ} app:enable user_ldap + in + '' + ${occ} app:install user_ldap || : + ${occ} app:enable user_ldap - ${occ} config:app:set user_ldap ${cID}ldap_configuration_active --value=0 + ${occ} config:app:set user_ldap ${cID}ldap_configuration_active --value=0 - # The following CLI commands follow - # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way + # The following CLI commands follow + # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way - ${occ} ldap:set-config "${cID}" 'ldapHost' \ - '${cfg'.host}' - ${occ} ldap:set-config "${cID}" 'ldapPort' \ - '${toString cfg'.port}' - ${occ} ldap:set-config "${cID}" 'ldapAgentName' \ - 'uid=${cfg'.adminName},ou=people,${cfg'.dcdomain}' - ${occ} ldap:set-config "${cID}" 'ldapAgentPassword' \ - "$(cat ${cfg'.adminPassword.result.path})" - ${occ} ldap:set-config "${cID}" 'ldapBase' \ - '${cfg'.dcdomain}' - ${occ} ldap:set-config "${cID}" 'ldapBaseGroups' \ - '${cfg'.dcdomain}' - ${occ} ldap:set-config "${cID}" 'ldapBaseUsers' \ - '${cfg'.dcdomain}' - ${occ} ldap:set-config "${cID}" 'ldapEmailAttribute' \ - 'mail' - ${occ} ldap:set-config "${cID}" 'ldapGroupFilter' \ - '(&(|(objectclass=groupOfUniqueNames))(|(cn=${cfg'.userGroup})))' - ${occ} ldap:set-config "${cID}" 'ldapGroupFilterGroups' \ - '${cfg'.userGroup}' - ${occ} ldap:set-config "${cID}" 'ldapGroupFilterObjectclass' \ - 'groupOfUniqueNames' - ${occ} ldap:set-config "${cID}" 'ldapGroupMemberAssocAttr' \ - 'uniqueMember' - ${occ} ldap:set-config "${cID}" 'ldapLoginFilter' \ - '(&(&(objectclass=person)(memberOf=cn=${cfg'.userGroup},ou=groups,${cfg'.dcdomain}))(|(uid=%uid)(|(mail=%uid)(objectclass=%uid))))' - ${occ} ldap:set-config "${cID}" 'ldapLoginFilterAttributes' \ - 'mail;objectclass' - ${occ} ldap:set-config "${cID}" 'ldapUserDisplayName' \ - 'givenname' - ${occ} ldap:set-config "${cID}" 'ldapUserFilter' \ - '(&(objectclass=person)(memberOf=cn=${cfg'.userGroup},ou=groups,${cfg'.dcdomain}))' - ${occ} ldap:set-config "${cID}" 'ldapUserFilterMode' \ - '1' - ${occ} ldap:set-config "${cID}" 'ldapUserFilterObjectclass' \ - 'person' - # Makes the user_id used when creating a user through LDAP which means the ID used in - # Nextcloud is compatible with the one returned by a (possibly added in the future) SSO - # provider. - ${occ} ldap:set-config "${cID}" 'ldapExpertUsernameAttr' \ - 'uid' + ${occ} ldap:set-config "${cID}" 'ldapHost' \ + '${cfg'.host}' + ${occ} ldap:set-config "${cID}" 'ldapPort' \ + '${toString cfg'.port}' + ${occ} ldap:set-config "${cID}" 'ldapAgentName' \ + 'uid=${cfg'.adminName},ou=people,${cfg'.dcdomain}' + ${occ} ldap:set-config "${cID}" 'ldapAgentPassword' \ + "$(cat ${cfg'.adminPassword.result.path})" + ${occ} ldap:set-config "${cID}" 'ldapBase' \ + '${cfg'.dcdomain}' + ${occ} ldap:set-config "${cID}" 'ldapBaseGroups' \ + '${cfg'.dcdomain}' + ${occ} ldap:set-config "${cID}" 'ldapBaseUsers' \ + '${cfg'.dcdomain}' + ${occ} ldap:set-config "${cID}" 'ldapEmailAttribute' \ + 'mail' + ${occ} ldap:set-config "${cID}" 'ldapGroupFilter' \ + '(&(|(objectclass=groupOfUniqueNames))(|(cn=${cfg'.userGroup})))' + ${occ} ldap:set-config "${cID}" 'ldapGroupFilterGroups' \ + '${cfg'.userGroup}' + ${occ} ldap:set-config "${cID}" 'ldapGroupFilterObjectclass' \ + 'groupOfUniqueNames' + ${occ} ldap:set-config "${cID}" 'ldapGroupMemberAssocAttr' \ + 'uniqueMember' + ${occ} ldap:set-config "${cID}" 'ldapLoginFilter' \ + '(&(&(objectclass=person)(memberOf=cn=${cfg'.userGroup},ou=groups,${cfg'.dcdomain}))(|(uid=%uid)(|(mail=%uid)(objectclass=%uid))))' + ${occ} ldap:set-config "${cID}" 'ldapLoginFilterAttributes' \ + 'mail;objectclass' + ${occ} ldap:set-config "${cID}" 'ldapUserDisplayName' \ + 'givenname' + ${occ} ldap:set-config "${cID}" 'ldapUserFilter' \ + '(&(objectclass=person)(memberOf=cn=${cfg'.userGroup},ou=groups,${cfg'.dcdomain}))' + ${occ} ldap:set-config "${cID}" 'ldapUserFilterMode' \ + '1' + ${occ} ldap:set-config "${cID}" 'ldapUserFilterObjectclass' \ + 'person' + # Makes the user_id used when creating a user through LDAP which means the ID used in + # Nextcloud is compatible with the one returned by a (possibly added in the future) SSO + # provider. + ${occ} ldap:set-config "${cID}" 'ldapExpertUsernameAttr' \ + 'uid' - ${occ} ldap:test-config -- "${cID}" + ${occ} ldap:test-config -- "${cID}" - # Only one active at the same time + # Only one active at the same time - ALL_CONFIG="$(${occ} ldap:show-config --output=json)" - for configid in $(echo "$ALL_CONFIG" | jq --raw-output "keys[]"); do - echo "Deactivating $configid" - ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' \ - '0' - done + ALL_CONFIG="$(${occ} ldap:show-config --output=json)" + for configid in $(echo "$ALL_CONFIG" | jq --raw-output "keys[]"); do + echo "Deactivating $configid" + ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' \ + '0' + done - ${occ} ldap:set-config "${cID}" 'ldapConfigurationActive' \ - '1' - ''; + ${occ} ldap:set-config "${cID}" 'ldapConfigurationActive' \ + '1' + ''; }) - (let + ( + let scopes = [ "openid" "profile" @@ -1063,235 +1093,238 @@ in "groups" "nextcloud_userinfo" ]; - in lib.mkIf (cfg.enable && cfg.apps.sso.enable) { - assertions = [ - { - assertion = cfg.ssl != null; - message = "To integrate SSO, SSL must be enabled, set the shb.nextcloud.ssl option."; - } - ]; - - services.nextcloud.extraApps = { - inherit (nextcloudApps) oidc_login; - }; - - systemd.services.nextcloud-setup-pre = { - wantedBy = [ "multi-user.target" ]; - before = [ "nextcloud-setup.service" ]; - serviceConfig.Type = "oneshot"; - serviceConfig.User = "nextcloud"; - script = - '' - mkdir -p ${cfg.dataDir}/config - cat < "${cfg.dataDir}/config/secretFile" + in + lib.mkIf (cfg.enable && cfg.apps.sso.enable) { + assertions = [ { - "oidc_login_client_secret": "$(cat ${cfg.apps.sso.secret.result.path})" + assertion = cfg.ssl != null; + message = "To integrate SSO, SSL must be enabled, set the shb.nextcloud.ssl option."; } - EOF + ]; + + services.nextcloud.extraApps = { + inherit (nextcloudApps) oidc_login; + }; + + systemd.services.nextcloud-setup-pre = { + wantedBy = [ "multi-user.target" ]; + before = [ "nextcloud-setup.service" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.User = "nextcloud"; + script = '' + mkdir -p ${cfg.dataDir}/config + cat < "${cfg.dataDir}/config/secretFile" + { + "oidc_login_client_secret": "$(cat ${cfg.apps.sso.secret.result.path})" + } + EOF ''; - }; + }; - services.nextcloud = { - secretFile = "${cfg.dataDir}/config/secretFile"; + services.nextcloud = { + secretFile = "${cfg.dataDir}/config/secretFile"; - # See all options at https://github.com/pulsejet/nextcloud-oidc-login - # Other important url/links are: - # ${fqdn}/.well-known/openid-configuration - # https://www.authelia.com/reference/guides/attributes/#custom-attributes - # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud_oidc_authelia.md - # https://www.authelia.com/integration/openid-connect/nextcloud/#authelia - # https://www.openidconnect.net/ - settings = { - allow_user_to_change_display_name = false; - lost_password_link = "disabled"; - oidc_login_provider_url = ssoFqdnWithPort; - oidc_login_client_id = cfg.apps.sso.clientID; + # See all options at https://github.com/pulsejet/nextcloud-oidc-login + # Other important url/links are: + # ${fqdn}/.well-known/openid-configuration + # https://www.authelia.com/reference/guides/attributes/#custom-attributes + # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud_oidc_authelia.md + # https://www.authelia.com/integration/openid-connect/nextcloud/#authelia + # https://www.openidconnect.net/ + settings = { + allow_user_to_change_display_name = false; + lost_password_link = "disabled"; + oidc_login_provider_url = ssoFqdnWithPort; + oidc_login_client_id = cfg.apps.sso.clientID; - # Automatically redirect the login page to the provider. - oidc_login_auto_redirect = !cfg.apps.sso.fallbackDefaultAuth; - # Authelia at least does not support this. - oidc_login_end_session_redirect = false; - # Redirect to this page after logging out the user - oidc_login_logout_url = ssoFqdnWithPort; - oidc_login_button_text = "Log in with ${cfg.apps.sso.provider}"; - oidc_login_hide_password_form = false; - # Now, Authelia provides the info using the UserInfo request. - oidc_login_use_id_token = false; - oidc_login_attributes = { - id = "preferred_username"; - name = "name"; - mail = "email"; - groups = "groups"; - is_admin = "is_nextcloud_admin"; + # Automatically redirect the login page to the provider. + oidc_login_auto_redirect = !cfg.apps.sso.fallbackDefaultAuth; + # Authelia at least does not support this. + oidc_login_end_session_redirect = false; + # Redirect to this page after logging out the user + oidc_login_logout_url = ssoFqdnWithPort; + oidc_login_button_text = "Log in with ${cfg.apps.sso.provider}"; + oidc_login_hide_password_form = false; + # Now, Authelia provides the info using the UserInfo request. + oidc_login_use_id_token = false; + oidc_login_attributes = { + id = "preferred_username"; + name = "name"; + mail = "email"; + groups = "groups"; + is_admin = "is_nextcloud_admin"; + }; + oidc_login_allowed_groups = [ cfg.apps.ldap.userGroup ]; + oidc_login_default_group = "oidc"; + oidc_login_use_external_storage = false; + oidc_login_scope = lib.concatStringsSep " " scopes; + oidc_login_proxy_ldap = false; + # Enable creation of users new to Nextcloud from OIDC login. A user may be known to the + # IdP but not (yet) known to Nextcloud. This setting controls what to do in this case. + # * 'true' (default): if the user authenticates to the IdP but is not known to Nextcloud, + # then they will be returned to the login screen and not allowed entry; + # * 'false': if the user authenticates but is not yet known to Nextcloud, then the user + # will be automatically created; note that with this setting, you will be allowing (or + # relying on) a third-party (the IdP) to create new users + oidc_login_disable_registration = false; + oidc_login_redir_fallback = cfg.apps.sso.fallbackDefaultAuth; + # oidc_login_alt_login_page = "assets/login.php"; + oidc_login_tls_verify = true; + # If you get your groups from the oidc_login_attributes, you might want to create them if + # they are not already existing, Default is `false`. This creates groups for all groups + # the user is associated with in LDAP. It's too much. + oidc_create_groups = false; + oidc_login_webdav_enabled = false; + oidc_login_password_authentication = false; + oidc_login_public_key_caching_time = 86400; + oidc_login_min_time_between_jwks_requests = 10; + oidc_login_well_known_caching_time = 86400; + # If true, nextcloud will download user avatars on login. This may lead to security issues + # as the server does not control which URLs will be requested. Use with care. + oidc_login_update_avatar = false; + oidc_login_code_challenge_method = "S256"; }; - oidc_login_allowed_groups = [ cfg.apps.ldap.userGroup ]; - oidc_login_default_group = "oidc"; - oidc_login_use_external_storage = false; - oidc_login_scope = lib.concatStringsSep " " scopes; - oidc_login_proxy_ldap = false; - # Enable creation of users new to Nextcloud from OIDC login. A user may be known to the - # IdP but not (yet) known to Nextcloud. This setting controls what to do in this case. - # * 'true' (default): if the user authenticates to the IdP but is not known to Nextcloud, - # then they will be returned to the login screen and not allowed entry; - # * 'false': if the user authenticates but is not yet known to Nextcloud, then the user - # will be automatically created; note that with this setting, you will be allowing (or - # relying on) a third-party (the IdP) to create new users - oidc_login_disable_registration = false; - oidc_login_redir_fallback = cfg.apps.sso.fallbackDefaultAuth; - # oidc_login_alt_login_page = "assets/login.php"; - oidc_login_tls_verify = true; - # If you get your groups from the oidc_login_attributes, you might want to create them if - # they are not already existing, Default is `false`. This creates groups for all groups - # the user is associated with in LDAP. It's too much. - oidc_create_groups = false; - oidc_login_webdav_enabled = false; - oidc_login_password_authentication = false; - oidc_login_public_key_caching_time = 86400; - oidc_login_min_time_between_jwks_requests = 10; - oidc_login_well_known_caching_time = 86400; - # If true, nextcloud will download user avatars on login. This may lead to security issues - # as the server does not control which URLs will be requested. Use with care. - oidc_login_update_avatar = false; - oidc_login_code_challenge_method = "S256"; }; - }; - shb.authelia.extraDefinitions = { - user_attributes."is_nextcloud_admin".expression = ''type(groups) == list && "${cfg.apps.sso.adminGroup}" in groups''; - }; - shb.authelia.extraOidcClaimsPolicies."nextcloud_userinfo" = { - custom_claims = { - is_nextcloud_admin = {}; + shb.authelia.extraDefinitions = { + user_attributes."is_nextcloud_admin".expression = + ''type(groups) == list && "${cfg.apps.sso.adminGroup}" in groups''; + }; + shb.authelia.extraOidcClaimsPolicies."nextcloud_userinfo" = { + custom_claims = { + is_nextcloud_admin = { }; + }; + }; + shb.authelia.extraOidcScopes."nextcloud_userinfo" = { + claims = [ "is_nextcloud_admin" ]; }; - }; - shb.authelia.extraOidcScopes."nextcloud_userinfo" = { - claims = [ "is_nextcloud_admin" ]; - }; - shb.authelia.oidcClients = lib.mkIf (cfg.apps.sso.provider == "Authelia") [ - { - client_id = cfg.apps.sso.clientID; - client_name = "Nextcloud"; - client_secret.source = cfg.apps.sso.secretForAuthelia.result.path; - claims_policy = "nextcloud_userinfo"; - public = false; - authorization_policy = cfg.apps.sso.authorization_policy; - require_pkce = "true"; - pkce_challenge_method = "S256"; - redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ]; - inherit scopes; - response_types = [ "code" ]; - grant_types = [ "authorization_code" ]; - access_token_signed_response_alg = "none"; - userinfo_signed_response_alg = "none"; - token_endpoint_auth_method = "client_secret_basic"; - } - ]; - }) + shb.authelia.oidcClients = lib.mkIf (cfg.apps.sso.provider == "Authelia") [ + { + client_id = cfg.apps.sso.clientID; + client_name = "Nextcloud"; + client_secret.source = cfg.apps.sso.secretForAuthelia.result.path; + claims_policy = "nextcloud_userinfo"; + public = false; + authorization_policy = cfg.apps.sso.authorization_policy; + require_pkce = "true"; + pkce_challenge_method = "S256"; + redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ]; + inherit scopes; + response_types = [ "code" ]; + grant_types = [ "authorization_code" ]; + access_token_signed_response_alg = "none"; + userinfo_signed_response_alg = "none"; + token_endpoint_auth_method = "client_secret_basic"; + } + ]; + } + ) (lib.mkIf (cfg.enable && cfg.autoDisableMaintenanceModeOnStart) { - systemd.services.nextcloud-setup.preStart = - lib.mkBefore '' + systemd.services.nextcloud-setup.preStart = lib.mkBefore '' if [[ -e /var/lib/nextcloud/config/config.php ]]; then ${occ} maintenance:mode --no-interaction --quiet --off fi - ''; + ''; }) (lib.mkIf (cfg.enable && cfg.alwaysApplyExpensiveMigrations) { - systemd.services.nextcloud-setup.script = - '' + systemd.services.nextcloud-setup.script = '' if [[ -e /var/lib/nextcloud/config/config.php ]]; then ${occ} maintenance:repair --include-expensive fi - ''; + ''; }) # Great source of inspiration: # https://github.com/Shawn8901/nix-configuration/blob/538c18d9ecbf7c7e649b1540c0d40881bada6690/modules/nixos/private/nextcloud/memories.nix#L226 - (lib.mkIf cfg.apps.memories.enable - (let + (lib.mkIf cfg.apps.memories.enable ( + let cfg' = cfg.apps.memories; - exiftool = pkgs.exiftool.overrideAttrs (f: p: { - version = "12.70"; - src = pkgs.fetchurl { - url = "https://exiftool.org/Image-ExifTool-12.70.tar.gz"; - hash = "sha256-TLJSJEXMPj870TkExq6uraX8Wl4kmNerrSlX3LQsr/4="; - }; - }); - in - { - assertions = [ - { - assertion = true; - message = "Memories app has an issue for now, see https://github.com/ibizaman/selfhostblocks/issues/476."; - } - ]; - - services.nextcloud.extraApps = { - inherit (nextcloudApps) memories; - }; - - systemd.services.nextcloud-cron = { - # required for memories - # see https://github.com/pulsejet/memories/blob/master/docs/troubleshooting.md#issues-with-nixos - path = [ pkgs.perl ]; - }; - - services.nextcloud = { - # See all options at https://memories.gallery/system-config/ - settings = { - "memories.exiftool" = "${exiftool}/bin/exiftool"; - "memories.exiftool_no_local" = false; - "memories.index.mode" = "3"; - "memories.index.path" = cfg'.photosPath; - "memories.timeline.default_path" = cfg'.photosPath; - - "memories.vod.disable" = !cfg'.vaapi; - "memories.vod.vaapi" = cfg'.vaapi; - "memories.vod.ffmpeg" = "${pkgs.ffmpeg-headless}/bin/ffmpeg"; - "memories.vod.ffprobe" = "${pkgs.ffmpeg-headless}/bin/ffprobe"; - "memories.vod.use_transpose" = true; - "memories.vod.use_transpose.force_sw" = cfg'.vaapi; # AMD and old Intel can't use hardware here. - - "memories.db.triggers.fcu" = true; - "memories.readonly" = true; - "preview_ffmpeg_path" = "${pkgs.ffmpeg-headless}/bin/ffmpeg"; + exiftool = pkgs.exiftool.overrideAttrs ( + f: p: { + version = "12.70"; + src = pkgs.fetchurl { + url = "https://exiftool.org/Image-ExifTool-12.70.tar.gz"; + hash = "sha256-TLJSJEXMPj870TkExq6uraX8Wl4kmNerrSlX3LQsr/4="; }; - }; + } + ); + in + { + assertions = [ + { + assertion = true; + message = "Memories app has an issue for now, see https://github.com/ibizaman/selfhostblocks/issues/476."; + } + ]; - systemd.services.phpfpm-nextcloud.serviceConfig = lib.mkIf cfg'.vaapi { - DeviceAllow = [ "/dev/dri/renderD128 rwm" ]; - PrivateDevices = lib.mkForce false; - }; - })) + services.nextcloud.extraApps = { + inherit (nextcloudApps) memories; + }; - (lib.mkIf cfg.apps.recognize.enable - (let + systemd.services.nextcloud-cron = { + # required for memories + # see https://github.com/pulsejet/memories/blob/master/docs/troubleshooting.md#issues-with-nixos + path = [ pkgs.perl ]; + }; + + services.nextcloud = { + # See all options at https://memories.gallery/system-config/ + settings = { + "memories.exiftool" = "${exiftool}/bin/exiftool"; + "memories.exiftool_no_local" = false; + "memories.index.mode" = "3"; + "memories.index.path" = cfg'.photosPath; + "memories.timeline.default_path" = cfg'.photosPath; + + "memories.vod.disable" = !cfg'.vaapi; + "memories.vod.vaapi" = cfg'.vaapi; + "memories.vod.ffmpeg" = "${pkgs.ffmpeg-headless}/bin/ffmpeg"; + "memories.vod.ffprobe" = "${pkgs.ffmpeg-headless}/bin/ffprobe"; + "memories.vod.use_transpose" = true; + "memories.vod.use_transpose.force_sw" = cfg'.vaapi; # AMD and old Intel can't use hardware here. + + "memories.db.triggers.fcu" = true; + "memories.readonly" = true; + "preview_ffmpeg_path" = "${pkgs.ffmpeg-headless}/bin/ffmpeg"; + }; + }; + + systemd.services.phpfpm-nextcloud.serviceConfig = lib.mkIf cfg'.vaapi { + DeviceAllow = [ "/dev/dri/renderD128 rwm" ]; + PrivateDevices = lib.mkForce false; + }; + } + )) + + (lib.mkIf cfg.apps.recognize.enable ( + let cfg' = cfg.apps.recognize; in - { - services.nextcloud.extraApps = { - inherit (nextcloudApps) recognize; - }; + { + services.nextcloud.extraApps = { + inherit (nextcloudApps) recognize; + }; - systemd.services.nextcloud-setup.script = - '' - ${occ} config:app:set recognize nice_binary --value ${pkgs.coreutils}/bin/nice - ${occ} config:app:set recognize node_binary --value ${pkgs.nodejs}/bin/node - ${occ} config:app:set recognize faces.enabled --value true - ${occ} config:app:set recognize faces.batchSize --value 50 - ${occ} config:app:set recognize imagenet.enabled --value true - ${occ} config:app:set recognize imagenet.batchSize --value 100 - ${occ} config:app:set recognize landmarks.batchSize --value 100 - ${occ} config:app:set recognize landmarks.enabled --value true - ${occ} config:app:set recognize tensorflow.cores --value 1 - ${occ} config:app:set recognize tensorflow.gpu --value false - ${occ} config:app:set recognize tensorflow.purejs --value false - ${occ} config:app:set recognize musicnn.enabled --value true - ${occ} config:app:set recognize musicnn.batchSize --value 100 - ''; - })) + systemd.services.nextcloud-setup.script = '' + ${occ} config:app:set recognize nice_binary --value ${pkgs.coreutils}/bin/nice + ${occ} config:app:set recognize node_binary --value ${pkgs.nodejs}/bin/node + ${occ} config:app:set recognize faces.enabled --value true + ${occ} config:app:set recognize faces.batchSize --value 50 + ${occ} config:app:set recognize imagenet.enabled --value true + ${occ} config:app:set recognize imagenet.batchSize --value 100 + ${occ} config:app:set recognize landmarks.batchSize --value 100 + ${occ} config:app:set recognize landmarks.enabled --value true + ${occ} config:app:set recognize tensorflow.cores --value 1 + ${occ} config:app:set recognize tensorflow.gpu --value false + ${occ} config:app:set recognize tensorflow.purejs --value false + ${occ} config:app:set recognize musicnn.enabled --value true + ${occ} config:app:set recognize musicnn.batchSize --value 100 + ''; + } + )) ]; } diff --git a/modules/services/open-webui.nix b/modules/services/open-webui.nix index b2af7cb..e6b1488 100644 --- a/modules/services/open-webui.nix +++ b/modules/services/open-webui.nix @@ -1,11 +1,22 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.shb.open-webui; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; roleClaim = "openwebui_groups"; - oauthScopes = [ "openid" "email" "profile" "groups" "${roleClaim}" ]; + oauthScopes = [ + "openid" + "email" + "profile" + "groups" + "${roleClaim}" + ]; in { imports = [ @@ -42,19 +53,19 @@ in environment = lib.mkOption { type = lib.types.attrsOf lib.types.str; description = "Extra environment variables. See https://docs.openwebui.com/getting-started/env-configuration"; - default = {}; + default = { }; example = '' - { - WEBUI_NAME = "SelfHostBlocks"; + { + WEBUI_NAME = "SelfHostBlocks"; - OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}"; - RAG_EMBEDDING_MODEL = "nomic-embed-text:v1.5"; + OLLAMA_BASE_URL = "http://127.0.0.1:''${toString config.services.ollama.port}"; + RAG_EMBEDDING_MODEL = "nomic-embed-text:v1.5"; - ENABLE_OPENAI_API = "True"; - OPENAI_API_BASE_URL = "http://127.0.0.1:''${toString config.services.llama-cpp.port}"; - ENABLE_WEB_SEARCH = "True"; - RAG_EMBEDDING_ENGINE = "openai"; - } + ENABLE_OPENAI_API = "True"; + OPENAI_API_BASE_URL = "http://127.0.0.1:''${toString config.services.llama-cpp.port}"; + ENABLE_WEB_SEARCH = "True"; + RAG_EMBEDDING_ENGINE = "openai"; + } ''; }; @@ -62,7 +73,7 @@ in description = '' Setup LDAP integration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { userGroup = lib.mkOption { @@ -84,7 +95,7 @@ in description = '' Setup SSO integration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = { enable = lib.mkEnableOption "SSO integration."; @@ -103,7 +114,10 @@ in }; authorization_policy = lib.mkOption { - type = lib.types.enum [ "one_factor" "two_factor" ]; + type = lib.types.enum [ + "one_factor" + "two_factor" + ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; @@ -136,7 +150,7 @@ in description = '' Backup state directory. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = "open-webui"; @@ -149,124 +163,127 @@ in }; }; - config = (lib.mkMerge [ - (lib.mkIf cfg.enable { - users.users.open-webui = { - isSystemUser = true; - group = "open-webui"; - }; - users.groups.open-webui = {}; - - services.open-webui = { - enable = true; - - host = "127.0.0.1"; - inherit (cfg) port; - - environment = { - WEBUI_URL = "https://${cfg.subdomain}.${cfg.domain}"; - - ENABLE_PERSISTENT_CONFIG = "False"; - - ANONYMIZED_TELEMETRY = "False"; - DO_NOT_TRACK = "True"; - SCARF_NO_ANALYTICS = "True"; - - ENABLE_VERSION_UPDATE_CHECK = "False"; - } // cfg.environment; - }; - - systemd.services.open-webui.path = [ - pkgs.ffmpeg-headless - ]; - - shb.nginx.vhosts = [ - { - inherit (cfg) subdomain domain ssl; - upstream = "http://127.0.0.1:${toString cfg.port}/"; - extraConfig = '' - proxy_read_timeout 300s; - proxy_send_timeout 300s; - ''; - } - ]; - }) - (lib.mkIf (cfg.enable && cfg.sso.enable) { - shb.lldap.ensureGroups = { - ${cfg.ldap.userGroup} = {}; - ${cfg.ldap.adminGroup} = {}; - }; - - services.open-webui = { - package = pkgs.open-webui.overrideAttrs (finalAttrs: { - patches = [ - ../../patches/0001-selfhostblocks-never-onboard.patch - ../../patches/0002-selfhostblocks-do-not-allow-unauthorized-roles.patch - ]; - }); - environment = { - ENABLE_SIGNUP = "False"; - WEBUI_AUTH = "True"; - ENABLE_FORWARD_USER_INFO_HEADERS = "True"; - ENABLE_OAUTH_SIGNUP = "True"; - OAUTH_UPDATE_PICTURE_ON_LOGIN = "True"; - OAUTH_CLIENT_ID = cfg.sso.clientID; - OPENID_PROVIDER_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration"; - OAUTH_PROVIDER_NAME = "Single Sign-On"; - OAUTH_USERNAME_CLAIM = "preferred_username"; - ENABLE_OAUTH_ROLE_MANAGEMENT = "True"; - OAUTH_ALLOWED_ROLES = "user,admin"; - OAUTH_ADMIN_ROLES = "admin"; - OAUTH_ROLES_CLAIM = roleClaim; - OAUTH_SCOPES = lib.concatStringsSep " " oauthScopes; + config = ( + lib.mkMerge [ + (lib.mkIf cfg.enable { + users.users.open-webui = { + isSystemUser = true; + group = "open-webui"; }; - }; + users.groups.open-webui = { }; - shb.authelia.extraDefinitions = { - user_attributes.${roleClaim}.expression = - ''"${cfg.ldap.adminGroup}" in groups ? ["admin"] : ("${cfg.ldap.userGroup}" in groups ? ["user"] : [""])''; - }; - shb.authelia.extraOidcClaimsPolicies.${roleClaim} = { - custom_claims = { - "${roleClaim}" = {}; + services.open-webui = { + enable = true; + + host = "127.0.0.1"; + inherit (cfg) port; + + environment = { + WEBUI_URL = "https://${cfg.subdomain}.${cfg.domain}"; + + ENABLE_PERSISTENT_CONFIG = "False"; + + ANONYMIZED_TELEMETRY = "False"; + DO_NOT_TRACK = "True"; + SCARF_NO_ANALYTICS = "True"; + + ENABLE_VERSION_UPDATE_CHECK = "False"; + } + // cfg.environment; }; - }; - shb.authelia.extraOidcScopes."${roleClaim}" = { - claims = [ "${roleClaim}" ]; - }; - shb.authelia.oidcClients = [ - { - client_id = cfg.sso.clientID; - client_name = "Open WebUI"; - client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; - claims_policy = "${roleClaim}"; - public = false; - authorization_policy = cfg.sso.authorization_policy; - redirect_uris = [ - "https://${cfg.subdomain}.${cfg.domain}/oauth/oidc/callback" - ]; - scopes = oauthScopes; - } - ]; + systemd.services.open-webui.path = [ + pkgs.ffmpeg-headless + ]; - systemd.services.open-webui.serviceConfig.EnvironmentFile = "/run/open-webui/secrets.env"; - systemd.tmpfiles.rules = [ - "d '/run/open-webui' 0750 root root - -" - ]; - systemd.services.open-webui-pre = { - script = lib.shb.replaceSecrets { - userConfig = { - OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path; + shb.nginx.vhosts = [ + { + inherit (cfg) subdomain domain ssl; + upstream = "http://127.0.0.1:${toString cfg.port}/"; + extraConfig = '' + proxy_read_timeout 300s; + proxy_send_timeout 300s; + ''; + } + ]; + }) + (lib.mkIf (cfg.enable && cfg.sso.enable) { + shb.lldap.ensureGroups = { + ${cfg.ldap.userGroup} = { }; + ${cfg.ldap.adminGroup} = { }; + }; + + services.open-webui = { + package = pkgs.open-webui.overrideAttrs (finalAttrs: { + patches = [ + ../../patches/0001-selfhostblocks-never-onboard.patch + ../../patches/0002-selfhostblocks-do-not-allow-unauthorized-roles.patch + ]; + }); + environment = { + ENABLE_SIGNUP = "False"; + WEBUI_AUTH = "True"; + ENABLE_FORWARD_USER_INFO_HEADERS = "True"; + ENABLE_OAUTH_SIGNUP = "True"; + OAUTH_UPDATE_PICTURE_ON_LOGIN = "True"; + OAUTH_CLIENT_ID = cfg.sso.clientID; + OPENID_PROVIDER_URL = "${cfg.sso.authEndpoint}/.well-known/openid-configuration"; + OAUTH_PROVIDER_NAME = "Single Sign-On"; + OAUTH_USERNAME_CLAIM = "preferred_username"; + ENABLE_OAUTH_ROLE_MANAGEMENT = "True"; + OAUTH_ALLOWED_ROLES = "user,admin"; + OAUTH_ADMIN_ROLES = "admin"; + OAUTH_ROLES_CLAIM = roleClaim; + OAUTH_SCOPES = lib.concatStringsSep " " oauthScopes; }; - resultPath = "/run/open-webui/secrets.env"; - generator = lib.shb.toEnvVar; }; - serviceConfig.Type = "oneshot"; - wantedBy = [ "multi-user.target" ]; - before = [ "open-webui.service" ]; - requiredBy = [ "open-webui.service" ]; - }; - }) - ]); + + shb.authelia.extraDefinitions = { + user_attributes.${roleClaim}.expression = + ''"${cfg.ldap.adminGroup}" in groups ? ["admin"] : ("${cfg.ldap.userGroup}" in groups ? ["user"] : [""])''; + }; + shb.authelia.extraOidcClaimsPolicies.${roleClaim} = { + custom_claims = { + "${roleClaim}" = { }; + }; + }; + shb.authelia.extraOidcScopes."${roleClaim}" = { + claims = [ "${roleClaim}" ]; + }; + + shb.authelia.oidcClients = [ + { + client_id = cfg.sso.clientID; + client_name = "Open WebUI"; + client_secret.source = cfg.sso.sharedSecretForAuthelia.result.path; + claims_policy = "${roleClaim}"; + public = false; + authorization_policy = cfg.sso.authorization_policy; + redirect_uris = [ + "https://${cfg.subdomain}.${cfg.domain}/oauth/oidc/callback" + ]; + scopes = oauthScopes; + } + ]; + + systemd.services.open-webui.serviceConfig.EnvironmentFile = "/run/open-webui/secrets.env"; + systemd.tmpfiles.rules = [ + "d '/run/open-webui' 0750 root root - -" + ]; + systemd.services.open-webui-pre = { + script = lib.shb.replaceSecrets { + userConfig = { + OAUTH_CLIENT_SECRET.source = cfg.sso.sharedSecret.result.path; + }; + resultPath = "/run/open-webui/secrets.env"; + generator = lib.shb.toEnvVar; + }; + serviceConfig.Type = "oneshot"; + wantedBy = [ "multi-user.target" ]; + before = [ "open-webui.service" ]; + requiredBy = [ "open-webui.service" ]; + }; + }) + ] + ); } diff --git a/modules/services/pinchflat.nix b/modules/services/pinchflat.nix index 86d444d..6d509bb 100644 --- a/modules/services/pinchflat.nix +++ b/modules/services/pinchflat.nix @@ -1,10 +1,15 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.shb.pinchflat; inherit (lib) types; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; in { imports = [ @@ -57,7 +62,10 @@ in }; timeZone = lib.mkOption { - type = lib.types.oneOf [ lib.types.str lib.shb.secretFileType ]; + type = lib.types.oneOf [ + lib.types.str + lib.shb.secretFileType + ]; description = "Timezone of this instance."; example = "America/Los_Angeles"; }; @@ -66,7 +74,7 @@ in description = '' Setup LDAP integration. ''; - default = {}; + default = { }; type = types.submodule { options = { enable = lib.mkEnableOption "LDAP integration." // { @@ -86,7 +94,7 @@ in description = '' Setup SSO integration. ''; - default = {}; + default = { }; type = types.submodule { options = { enable = lib.mkEnableOption "SSO integration."; @@ -99,7 +107,10 @@ in }; authorization_policy = lib.mkOption { - type = types.enum [ "one_factor" "two_factor" ]; + type = types.enum [ + "one_factor" + "two_factor" + ]; description = "Require one factor (password) or two factor (device) authentication."; default = "one_factor"; }; @@ -111,7 +122,7 @@ in description = '' Backup media directory `shb.mediaDir`. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = "pinchflat"; @@ -142,7 +153,9 @@ in # This should be using a contract instead of setting the option directly. shb.lldap = lib.mkIf config.shb.lldap.enable { - ensureGroups = { ${cfg.ldap.userGroup} = {}; }; + ensureGroups = { + ${cfg.ldap.userGroup} = { }; + }; }; systemd.services.pinchflat-pre = { @@ -160,7 +173,7 @@ in requiredBy = [ "pinchflat.service" ]; }; - shb.nginx.vhosts = [ + shb.nginx.vhosts = [ { inherit (cfg) subdomain domain ssl; inherit (cfg.sso) authEndpoint; @@ -181,7 +194,7 @@ in job_name = "pinchflat"; static_configs = [ { - targets = ["127.0.0.1:${toString cfg.port}"]; + targets = [ "127.0.0.1:${toString cfg.port}" ]; labels = { "hostname" = config.networking.hostName; "domain" = cfg.domain; diff --git a/modules/services/vaultwarden.nix b/modules/services/vaultwarden.nix index 81cfc40..b985387 100644 --- a/modules/services/vaultwarden.nix +++ b/modules/services/vaultwarden.nix @@ -1,13 +1,22 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.shb.vaultwarden; - contracts = pkgs.callPackage ../contracts {}; + contracts = pkgs.callPackage ../contracts { }; fqdn = "${cfg.subdomain}.${cfg.domain}"; - dataFolder = if lib.versionOlder (config.system.stateVersion or "24.11") "24.11" then "/var/lib/bitwarden_rs" else "/var/lib/vaultwarden"; + dataFolder = + if lib.versionOlder (config.system.stateVersion or "24.11") "24.11" then + "/var/lib/bitwarden_rs" + else + "/var/lib/vaultwarden"; in { imports = [ @@ -55,7 +64,10 @@ in mode = "0440"; owner = "vaultwarden"; group = "postgres"; - restartUnits = [ "vaultwarden.service" "postgresql.service" ]; + restartUnits = [ + "vaultwarden.service" + "postgresql.service" + ]; }; }; }; @@ -63,53 +75,59 @@ in smtp = lib.mkOption { description = "SMTP options."; default = null; - type = lib.types.nullOr (lib.types.submodule { - options = { - from_address = lib.mkOption { - type = lib.types.str; - description = "SMTP address from which the emails originate."; - example = "vaultwarden@mydomain.com"; - }; - from_name = lib.mkOption { - type = lib.types.str; - description = "SMTP name from which the emails originate."; - default = "Vaultwarden"; - }; - host = lib.mkOption { - type = lib.types.str; - description = "SMTP host to send the emails to."; - }; - security = lib.mkOption { - type = lib.types.enum [ "starttls" "force_tls" "off" ]; - description = "Security expected by SMTP host."; - default = "starttls"; - }; - port = lib.mkOption { - type = lib.types.port; - description = "SMTP port to send the emails to."; - default = 25; - }; - username = lib.mkOption { - type = lib.types.str; - description = "Username to connect to the SMTP host."; - }; - auth_mechanism = lib.mkOption { - type = lib.types.enum [ "Login" ]; - description = "Auth mechanism."; - default = "Login"; - }; - password = lib.mkOption { - description = "File containing the password to connect to the SMTP host."; - type = lib.types.submodule { - options = contracts.secret.mkRequester { - mode = "0400"; - owner = "vaultwarden"; - restartUnits = [ "vaultwarden.service" ]; + type = lib.types.nullOr ( + lib.types.submodule { + options = { + from_address = lib.mkOption { + type = lib.types.str; + description = "SMTP address from which the emails originate."; + example = "vaultwarden@mydomain.com"; + }; + from_name = lib.mkOption { + type = lib.types.str; + description = "SMTP name from which the emails originate."; + default = "Vaultwarden"; + }; + host = lib.mkOption { + type = lib.types.str; + description = "SMTP host to send the emails to."; + }; + security = lib.mkOption { + type = lib.types.enum [ + "starttls" + "force_tls" + "off" + ]; + description = "Security expected by SMTP host."; + default = "starttls"; + }; + port = lib.mkOption { + type = lib.types.port; + description = "SMTP port to send the emails to."; + default = 25; + }; + username = lib.mkOption { + type = lib.types.str; + description = "Username to connect to the SMTP host."; + }; + auth_mechanism = lib.mkOption { + type = lib.types.enum [ "Login" ]; + description = "Auth mechanism."; + default = "Login"; + }; + password = lib.mkOption { + description = "File containing the password to connect to the SMTP host."; + type = lib.types.submodule { + options = contracts.secret.mkRequester { + mode = "0400"; + owner = "vaultwarden"; + restartUnits = [ "vaultwarden.service" ]; + }; }; }; }; - }; - }); + } + ); }; mount = lib.mkOption { @@ -127,14 +145,16 @@ in ``` ''; readOnly = true; - default = { path = dataFolder; }; + default = { + path = dataFolder; + }; }; backup = lib.mkOption { description = '' Backup configuration. ''; - default = {}; + default = { }; type = lib.types.submodule { options = contracts.backup.mkRequester { user = "vaultwarden"; @@ -170,7 +190,8 @@ in ROCKET_LOG = if cfg.debug then "trace" else "info"; ROCKET_ADDRESS = "127.0.0.1"; ROCKET_PORT = cfg.port; - } // lib.optionalAttrs (cfg.smtp != null) { + } + // lib.optionalAttrs (cfg.smtp != null) { SMTP_FROM = cfg.smtp.from_address; SMTP_FROM_NAME = cfg.smtp.from_name; SMTP_HOST = cfg.smtp.host; @@ -189,27 +210,32 @@ in ]; # Needed to be able to write template config. systemd.services.vaultwarden.serviceConfig.ProtectHome = lib.mkForce false; - systemd.services.vaultwarden.preStart = - lib.shb.replaceSecrets { - userConfig = { - DATABASE_URL.source = cfg.databasePassword.result.path; - DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden"; - } // lib.optionalAttrs (cfg.smtp != null) { - SMTP_PASSWORD.source = cfg.smtp.password.result.path; - }; - resultPath = "${dataFolder}/vaultwarden.env"; - generator = lib.shb.toEnvVar; + systemd.services.vaultwarden.preStart = lib.shb.replaceSecrets { + userConfig = { + DATABASE_URL.source = cfg.databasePassword.result.path; + DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden"; + } + // lib.optionalAttrs (cfg.smtp != null) { + SMTP_PASSWORD.source = cfg.smtp.password.result.path; }; + resultPath = "${dataFolder}/vaultwarden.env"; + generator = lib.shb.toEnvVar; + }; shb.nginx.vhosts = [ { - inherit (cfg) subdomain domain authEndpoint ssl; + inherit (cfg) + subdomain + domain + authEndpoint + ssl + ; upstream = "http://127.0.0.1:${toString config.services.vaultwarden.config.ROCKET_PORT}"; autheliaRules = lib.mkIf (cfg.authEndpoint != null) [ { domain = "${fqdn}"; policy = "two_factor"; - subject = ["group:vaultwarden_admin"]; + subject = [ "group:vaultwarden_admin" ]; resources = [ "^/admin" ]; diff --git a/test/blocks/authelia.nix b/test/blocks/authelia.nix index 2f72a1b..181d6ea 100644 --- a/test/blocks/authelia.nix +++ b/test/blocks/authelia.nix @@ -8,178 +8,185 @@ in basic = lib.shb.runNixOSTest { name = "authelia-basic"; - nodes.machine = { config, pkgs, ... }: { - imports = [ - (pkgs'.path + "/nixos/modules/profiles/headless.nix") - (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") - ../../modules/blocks/authelia.nix - ../../modules/blocks/hardcodedsecret.nix - ]; - - networking.hosts = { - "127.0.0.1" = [ - "machine.com" - "client1.machine.com" - "client2.machine.com" - "ldap.machine.com" - "authelia.machine.com" + nodes.machine = + { config, pkgs, ... }: + { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + ../../modules/blocks/authelia.nix + ../../modules/blocks/hardcodedsecret.nix ]; - }; - shb.lldap = { - enable = true; - dcdomain = "dc=example,dc=com"; - subdomain = "ldap"; - domain = "machine.com"; - ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; - jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; - }; - - shb.hardcodedsecret.ldapUserPassword = { - request = config.shb.lldap.ldapUserPassword.request; - settings.content = ldapAdminPassword; - }; - shb.hardcodedsecret.jwtSecret = { - request = config.shb.lldap.jwtSecret.request; - settings.content = "jwtsecret"; - }; - - shb.authelia = { - enable = true; - subdomain = "authelia"; - domain = "machine.com"; - ldapHostname = "${config.shb.lldap.subdomain}.${config.shb.lldap.domain}"; - ldapPort = config.shb.lldap.ldapPort; - dcdomain = config.shb.lldap.dcdomain; - secrets = { - jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result; - ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result; - sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result; - storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result; - identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result; - identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result; + networking.hosts = { + "127.0.0.1" = [ + "machine.com" + "client1.machine.com" + "client2.machine.com" + "ldap.machine.com" + "authelia.machine.com" + ]; }; - oidcClients = [ - { - client_id = "client1"; - client_name = "My Client 1"; - client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$LR2wY11djfLrVQixdlLJew$rPByqFt6JfbIIAITxzAXckwh51QgV8E5YZmA8rXOzkMfBUcMq7cnOKEXF6MAFbjZaGf3J/B1OzLWZTCuZtALVw"; - public = false; - authorization_policy = "one_factor"; - redirect_uris = [ "http://client1.machine.com/redirect" ]; - } - { - client_id = "client2"; - client_name = "My Client 2"; - client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$76EqVU1N9K.iTOvD4WJ6ww$hqNJU.UHphiCjMChSqk27lUTjDqreuMuyV/u39Esc6HyiRXp5Ecx89ypJ5M0xk3Na97vbgDpwz7il5uwzQ4bfw"; - public = false; - authorization_policy = "one_factor"; - redirect_uris = [ "http://client2.machine.com/redirect" ]; - } - ]; - }; + shb.lldap = { + enable = true; + dcdomain = "dc=example,dc=com"; + subdomain = "ldap"; + domain = "machine.com"; + ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; + jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; + }; - shb.hardcodedsecret.autheliaJwtSecret = { - request = config.shb.authelia.secrets.jwtSecret.request; - settings.content = "jwtSecret"; - }; - shb.hardcodedsecret.ldapAdminPassword = { - request = config.shb.authelia.secrets.ldapAdminPassword.request; - settings.content = ldapAdminPassword; - }; - shb.hardcodedsecret.sessionSecret = { - request = config.shb.authelia.secrets.sessionSecret.request; - settings.content = "sessionSecret"; - }; - shb.hardcodedsecret.storageEncryptionKey = { - request = config.shb.authelia.secrets.storageEncryptionKey.request; - settings.content = "storageEncryptionKey"; - }; - shb.hardcodedsecret.identityProvidersOIDCHMACSecret = { - request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; - settings.content = "identityProvidersOIDCHMACSecret"; - }; - shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = { - request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; - settings.source = (pkgs.runCommand "gen-private-key" {} '' - mkdir $out - ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096 - '') + "/private.pem"; - }; + shb.hardcodedsecret.ldapUserPassword = { + request = config.shb.lldap.ldapUserPassword.request; + settings.content = ldapAdminPassword; + }; + shb.hardcodedsecret.jwtSecret = { + request = config.shb.lldap.jwtSecret.request; + settings.content = "jwtsecret"; + }; - specialisation = { - withDebug.configuration = { - shb.authelia.debug = true; + shb.authelia = { + enable = true; + subdomain = "authelia"; + domain = "machine.com"; + ldapHostname = "${config.shb.lldap.subdomain}.${config.shb.lldap.domain}"; + ldapPort = config.shb.lldap.ldapPort; + dcdomain = config.shb.lldap.dcdomain; + secrets = { + jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result; + ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result; + sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result; + storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result; + identityProvidersOIDCHMACSecret.result = + config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result; + identityProvidersOIDCIssuerPrivateKey.result = + config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result; + }; + + oidcClients = [ + { + client_id = "client1"; + client_name = "My Client 1"; + client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$LR2wY11djfLrVQixdlLJew$rPByqFt6JfbIIAITxzAXckwh51QgV8E5YZmA8rXOzkMfBUcMq7cnOKEXF6MAFbjZaGf3J/B1OzLWZTCuZtALVw"; + public = false; + authorization_policy = "one_factor"; + redirect_uris = [ "http://client1.machine.com/redirect" ]; + } + { + client_id = "client2"; + client_name = "My Client 2"; + client_secret.source = pkgs.writeText "secret" "$pbkdf2-sha512$310000$76EqVU1N9K.iTOvD4WJ6ww$hqNJU.UHphiCjMChSqk27lUTjDqreuMuyV/u39Esc6HyiRXp5Ecx89ypJ5M0xk3Na97vbgDpwz7il5uwzQ4bfw"; + public = false; + authorization_policy = "one_factor"; + redirect_uris = [ "http://client2.machine.com/redirect" ]; + } + ]; + }; + + shb.hardcodedsecret.autheliaJwtSecret = { + request = config.shb.authelia.secrets.jwtSecret.request; + settings.content = "jwtSecret"; + }; + shb.hardcodedsecret.ldapAdminPassword = { + request = config.shb.authelia.secrets.ldapAdminPassword.request; + settings.content = ldapAdminPassword; + }; + shb.hardcodedsecret.sessionSecret = { + request = config.shb.authelia.secrets.sessionSecret.request; + settings.content = "sessionSecret"; + }; + shb.hardcodedsecret.storageEncryptionKey = { + request = config.shb.authelia.secrets.storageEncryptionKey.request; + settings.content = "storageEncryptionKey"; + }; + shb.hardcodedsecret.identityProvidersOIDCHMACSecret = { + request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; + settings.content = "identityProvidersOIDCHMACSecret"; + }; + shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = { + request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; + settings.source = + (pkgs.runCommand "gen-private-key" { } '' + mkdir $out + ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096 + '') + + "/private.pem"; + }; + + specialisation = { + withDebug.configuration = { + shb.authelia.debug = true; + }; }; }; - }; - testScript = { nodes, ... }: - let - specializations = "${nodes.machine.system.build.toplevel}/specialisation"; - in - '' - import json + testScript = + { nodes, ... }: + let + specializations = "${nodes.machine.system.build.toplevel}/specialisation"; + in + '' + import json - start_all() + start_all() - def tests(): - machine.wait_for_unit("lldap.service") - machine.wait_for_unit("authelia-authelia.machine.com.target") - machine.wait_for_open_port(9091) + def tests(): + machine.wait_for_unit("lldap.service") + machine.wait_for_unit("authelia-authelia.machine.com.target") + machine.wait_for_open_port(9091) - endpoints = json.loads(machine.succeed("curl -s http://machine.com/.well-known/openid-configuration")) - auth_endpoint = endpoints['authorization_endpoint'] - print(f"auth_endpoint: {auth_endpoint}") - if auth_endpoint != "http://machine.com/api/oidc/authorization": - raise Exception("Unexpected auth_endpoint") + endpoints = json.loads(machine.succeed("curl -s http://machine.com/.well-known/openid-configuration")) + auth_endpoint = endpoints['authorization_endpoint'] + print(f"auth_endpoint: {auth_endpoint}") + if auth_endpoint != "http://machine.com/api/oidc/authorization": + raise Exception("Unexpected auth_endpoint") - resp = machine.succeed( - "curl -f -s '" - + auth_endpoint - + "?client_id=other" - + "&redirect_uri=http://client1.machine.com/redirect" - + "&scope=openid%20profile%20email" - + "&response_type=code" - + "&state=99999999'" - ) - print(resp) - if resp != "": - raise Exception("unexpected response") + resp = machine.succeed( + "curl -f -s '" + + auth_endpoint + + "?client_id=other" + + "&redirect_uri=http://client1.machine.com/redirect" + + "&scope=openid%20profile%20email" + + "&response_type=code" + + "&state=99999999'" + ) + print(resp) + if resp != "": + raise Exception("unexpected response") - resp = machine.succeed( - "curl -f -s '" - + auth_endpoint - + "?client_id=client1" - + "&redirect_uri=http://client1.machine.com/redirect" - + "&scope=openid%20profile%20email" - + "&response_type=code" - + "&state=11111111'" - ) - print(resp) - if "Found" not in resp: - raise Exception("unexpected response") + resp = machine.succeed( + "curl -f -s '" + + auth_endpoint + + "?client_id=client1" + + "&redirect_uri=http://client1.machine.com/redirect" + + "&scope=openid%20profile%20email" + + "&response_type=code" + + "&state=11111111'" + ) + print(resp) + if "Found" not in resp: + raise Exception("unexpected response") - resp = machine.succeed( - "curl -f -s '" - + auth_endpoint - + "?client_id=client2" - + "&redirect_uri=http://client2.machine.com/redirect" - + "&scope=openid%20profile%20email" - + "&response_type=code" - + "&state=22222222'" - ) - print(resp) - if "Found" not in resp: - raise Exception("unexpected response") + resp = machine.succeed( + "curl -f -s '" + + auth_endpoint + + "?client_id=client2" + + "&redirect_uri=http://client2.machine.com/redirect" + + "&scope=openid%20profile%20email" + + "&response_type=code" + + "&state=22222222'" + ) + print(resp) + if "Found" not in resp: + raise Exception("unexpected response") - with subtest("no debug"): - tests() + with subtest("no debug"): + tests() - with subtest("with debug"): - machine.succeed('${specializations}/withDebug/bin/switch-to-configuration test') - tests() - ''; + with subtest("with debug"): + machine.succeed('${specializations}/withDebug/bin/switch-to-configuration test') + tests() + ''; }; } diff --git a/test/blocks/lib.nix b/test/blocks/lib.nix index bea83ed..7b6ab61 100644 --- a/test/blocks/lib.nix +++ b/test/blocks/lib.nix @@ -24,7 +24,9 @@ in configWithTemplates = lib.shb.withReplacements userConfig; - nonSecretConfigFile = pkgs.writeText "config.yaml.template" (lib.generators.toJSON {} configWithTemplates); + nonSecretConfigFile = pkgs.writeText "config.yaml.template" ( + lib.generators.toJSON { } configWithTemplates + ); replacements = lib.shb.getReplacements userConfig; @@ -37,105 +39,113 @@ in replaceInTemplateJSON = lib.shb.replaceSecrets { inherit userConfig; resultPath = "/var/lib/config.json"; - generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json {}); + generator = lib.shb.replaceSecretsFormatAdapter (pkgs.formats.json { }); }; replaceInTemplateJSONGen = lib.shb.replaceSecrets { inherit userConfig; resultPath = "/var/lib/config_gen.json"; - generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toJSON {}); + generator = lib.shb.replaceSecretsGeneratorAdapter (lib.generators.toJSON { }); }; replaceInTemplateXML = lib.shb.replaceSecrets { inherit userConfig; resultPath = "/var/lib/config.xml"; - generator = lib.shb.replaceSecretsFormatAdapter (lib.shb.formatXML {enclosingRoot = "Root";}); + generator = lib.shb.replaceSecretsFormatAdapter (lib.shb.formatXML { enclosingRoot = "Root"; }); }; in - lib.shb.runNixOSTest { - name = "lib-template"; - nodes.machine = { config, pkgs, ... }: - { - imports = [ - (pkgs'.path + "/nixos/modules/profiles/headless.nix") - (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") - { - options = { - libtest.config = lib.mkOption { - type = lib.types.attrsOf (lib.types.oneOf [ lib.types.str lib.secretFileType ]); - }; + lib.shb.runNixOSTest { + name = "lib-template"; + nodes.machine = + { config, pkgs, ... }: + { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + { + options = { + libtest.config = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.oneOf [ + lib.types.str + lib.secretFileType + ] + ); }; - } - ]; + }; + } + ]; - system.activationScripts = { - libtest = replaceInTemplate; - libtestJSON = replaceInTemplateJSON; - libtestJSONGen = replaceInTemplateJSONGen; - libtestXML = replaceInTemplateXML; - }; + system.activationScripts = { + libtest = replaceInTemplate; + libtestJSON = replaceInTemplateJSON; + libtestJSONGen = replaceInTemplateJSONGen; + libtestXML = replaceInTemplateXML; }; + }; - testScript = { nodes, ... }: '' - import json - from collections import ChainMap - from xml.etree import ElementTree + testScript = + { nodes, ... }: + '' + import json + from collections import ChainMap + from xml.etree import ElementTree - start_all() - machine.wait_for_file("/var/lib/config.yaml") - machine.wait_for_file("/var/lib/config.json") - machine.wait_for_file("/var/lib/config_gen.json") - machine.wait_for_file("/var/lib/config.xml") + start_all() + machine.wait_for_file("/var/lib/config.yaml") + machine.wait_for_file("/var/lib/config.json") + machine.wait_for_file("/var/lib/config_gen.json") + machine.wait_for_file("/var/lib/config.xml") - def xml_to_dict_recursive(root): - all_descendants = list(root) - if len(all_descendants) == 0: - return {root.tag: root.text} - else: - merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants)) - return {root.tag: dict(merged_dict)} + def xml_to_dict_recursive(root): + all_descendants = list(root) + if len(all_descendants) == 0: + return {root.tag: root.text} + else: + merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants)) + return {root.tag: dict(merged_dict)} - wantedConfig = json.loads('${lib.generators.toJSON {} wantedConfig}') + wantedConfig = json.loads('${lib.generators.toJSON { } wantedConfig}') - with subtest("config"): - print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}")) + with subtest("config"): + print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}")) - gotConfig = machine.succeed("cat /var/lib/config.yaml") - print(gotConfig) - gotConfig = json.loads(gotConfig) + gotConfig = machine.succeed("cat /var/lib/config.yaml") + print(gotConfig) + gotConfig = json.loads(gotConfig) - if wantedConfig != gotConfig: - raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig)) + if wantedConfig != gotConfig: + raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig)) - with subtest("config JSON Gen"): - print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSONGen" replaceInTemplateJSONGen}")) + with subtest("config JSON Gen"): + print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSONGen" replaceInTemplateJSONGen}")) - gotConfig = machine.succeed("cat /var/lib/config_gen.json") - print(gotConfig) - gotConfig = json.loads(gotConfig) + gotConfig = machine.succeed("cat /var/lib/config_gen.json") + print(gotConfig) + gotConfig = json.loads(gotConfig) - if wantedConfig != gotConfig: - raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig)) + if wantedConfig != gotConfig: + raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig)) - with subtest("config JSON"): - print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSON" replaceInTemplateJSON}")) + with subtest("config JSON"): + print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateJSON" replaceInTemplateJSON}")) - gotConfig = machine.succeed("cat /var/lib/config.json") - print(gotConfig) - gotConfig = json.loads(gotConfig) + gotConfig = machine.succeed("cat /var/lib/config.json") + print(gotConfig) + gotConfig = json.loads(gotConfig) - if wantedConfig != gotConfig: - raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig)) + if wantedConfig != gotConfig: + raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig)) - with subtest("config XML"): - print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateXML" replaceInTemplateXML}")) + with subtest("config XML"): + print(machine.succeed("cat ${pkgs.writeText "replaceInTemplateXML" replaceInTemplateXML}")) - gotConfig = machine.succeed("cat /var/lib/config.xml") - print(gotConfig) - gotConfig = xml_to_dict_recursive(ElementTree.XML(gotConfig))['Root'] + gotConfig = machine.succeed("cat /var/lib/config.xml") + print(gotConfig) + gotConfig = xml_to_dict_recursive(ElementTree.XML(gotConfig))['Root'] - if wantedConfig != gotConfig: - raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig)) + if wantedConfig != gotConfig: + raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig)) ''; - }; + }; } diff --git a/test/blocks/lldap.nix b/test/blocks/lldap.nix index 6b288be..6cc1ab1 100644 --- a/test/blocks/lldap.nix +++ b/test/blocks/lldap.nix @@ -9,145 +9,148 @@ in auth = lib.shb.runNixOSTest { name = "ldap-auth"; - nodes.server = { config, pkgs, ... }: { - imports = [ - (pkgs'.path + "/nixos/modules/profiles/headless.nix") - (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") - { - options = { - shb.ssl.enable = lib.mkEnableOption "ssl"; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + { + options = { + shb.ssl.enable = lib.mkEnableOption "ssl"; + }; + } + ../../modules/blocks/hardcodedsecret.nix + ../../modules/blocks/lldap.nix + ]; + + shb.lldap = { + enable = true; + dcdomain = "dc=example,dc=com"; + subdomain = "ldap"; + domain = "example.com"; + ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; + jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; + + ensureUsers = { + "charlie" = { + email = "charlie@example.com"; + password.result = config.shb.hardcodedsecret."charlie".result; + }; }; - } - ../../modules/blocks/hardcodedsecret.nix - ../../modules/blocks/lldap.nix - ]; - shb.lldap = { - enable = true; - dcdomain = "dc=example,dc=com"; - subdomain = "ldap"; - domain = "example.com"; - ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; - jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; - - ensureUsers = { - "charlie" = { - email = "charlie@example.com"; - password.result = config.shb.hardcodedsecret."charlie".result; + ensureGroups = { + "family" = { }; }; }; + shb.hardcodedsecret.ldapUserPassword = { + request = config.shb.lldap.ldapUserPassword.request; + settings.content = password; + }; + shb.hardcodedsecret.jwtSecret = { + request = config.shb.lldap.jwtSecret.request; + settings.content = "jwtSecret"; + }; + shb.hardcodedsecret."charlie" = { + request = config.shb.lldap.ensureUsers."charlie".password.request; + settings.content = charliePassword; + }; - ensureGroups = { - "family" = {}; + networking.firewall.allowedTCPPorts = [ 80 ]; # nginx port + + environment.systemPackages = [ pkgs.openldap ]; + + specialisation = { + withDebug.configuration = { + shb.lldap.debug = true; + }; }; }; - shb.hardcodedsecret.ldapUserPassword = { - request = config.shb.lldap.ldapUserPassword.request; - settings.content = password; - }; - shb.hardcodedsecret.jwtSecret = { - request = config.shb.lldap.jwtSecret.request; - settings.content = "jwtSecret"; - }; - shb.hardcodedsecret."charlie" = { - request = config.shb.lldap.ensureUsers."charlie".password.request; - settings.content = charliePassword; - }; - networking.firewall.allowedTCPPorts = [ 80 ]; # nginx port - - environment.systemPackages = [ pkgs.openldap ]; - - specialisation = { - withDebug.configuration = { - shb.lldap.debug = true; - }; - }; - }; - - nodes.client = {}; + nodes.client = { }; # Inspired from https://github.com/lldap/lldap/blob/33f50d13a2e2d24a3e6bb05a148246bc98090df0/example_configs/lldap-ha-auth.sh - testScript = { nodes, ... }: - let - specializations = "${nodes.server.system.build.toplevel}/specialisation"; - in - '' - import json + testScript = + { nodes, ... }: + let + specializations = "${nodes.server.system.build.toplevel}/specialisation"; + in + '' + import json - start_all() + start_all() - def tests(): - server.wait_for_unit("lldap.service") - server.wait_for_open_port(${toString nodes.server.shb.lldap.webUIListenPort}) - server.wait_for_open_port(${toString nodes.server.shb.lldap.ldapPort}) + def tests(): + server.wait_for_unit("lldap.service") + server.wait_for_open_port(${toString nodes.server.shb.lldap.webUIListenPort}) + server.wait_for_open_port(${toString nodes.server.shb.lldap.ldapPort}) - with subtest("fail without authenticating"): - client.fail( - "curl -f -s -X GET" - + """ -H "Content-type: application/json" """ - + """ -H "Host: ldap.example.com" """ - + " http://server/api/graphql" - ) + with subtest("fail without authenticating"): + client.fail( + "curl -f -s -X GET" + + """ -H "Content-type: application/json" """ + + """ -H "Host: ldap.example.com" """ + + " http://server/api/graphql" + ) - with subtest("fail authenticating with wrong credentials"): - resp = client.fail( - "curl -f -s -X POST" - + """ -H "Content-type: application/json" """ - + """ -H "Host: ldap.example.com" """ - + " http://server/auth/simple/login" - + """ -d '{"username": "admin", "password": "wrong"}'""" - ) + with subtest("fail authenticating with wrong credentials"): + resp = client.fail( + "curl -f -s -X POST" + + """ -H "Content-type: application/json" """ + + """ -H "Host: ldap.example.com" """ + + " http://server/auth/simple/login" + + """ -d '{"username": "admin", "password": "wrong"}'""" + ) - print(resp) + print(resp) - with subtest("succeed with correct authentication"): - token = json.loads(client.succeed( - "curl -f -s -X POST " - + """ -H "Content-type: application/json" """ - + """ -H "Host: ldap.example.com" """ - + " http://server/auth/simple/login " - + """ -d '{"username": "admin", "password": "${password}"}' """ - ))['token'] + with subtest("succeed with correct authentication"): + token = json.loads(client.succeed( + "curl -f -s -X POST " + + """ -H "Content-type: application/json" """ + + """ -H "Host: ldap.example.com" """ + + " http://server/auth/simple/login " + + """ -d '{"username": "admin", "password": "${password}"}' """ + ))['token'] - data = json.loads(client.succeed( - "curl -f -s -X POST " - + """ -H "Content-type: application/json" """ - + """ -H "Host: ldap.example.com" """ - + """ -H "Authorization: Bearer {token}" """.format(token=token) - + " http://server/api/graphql " - + """ -d '{"variables": {"id": "admin"}, "query":"query($id:String!){user(userId:$id){displayName groups{displayName}}}"}' """ - ))['data'] + data = json.loads(client.succeed( + "curl -f -s -X POST " + + """ -H "Content-type: application/json" """ + + """ -H "Host: ldap.example.com" """ + + """ -H "Authorization: Bearer {token}" """.format(token=token) + + " http://server/api/graphql " + + """ -d '{"variables": {"id": "admin"}, "query":"query($id:String!){user(userId:$id){displayName groups{displayName}}}"}' """ + ))['data'] - assert data['user']['displayName'] == "Administrator" - assert data['user']['groups'][0]['displayName'] == "lldap_admin" + assert data['user']['displayName'] == "Administrator" + assert data['user']['groups'][0]['displayName'] == "lldap_admin" - with subtest("succeed charlie"): - resp = client.succeed( - "curl -f -s -X POST " - + """ -H "Content-type: application/json" """ - + """ -H "Host: ldap.example.com" """ - + " http://server/auth/simple/login " - + """ -d '{"username": "charlie", "password": "${charliePassword}"}' """ - ) - print(resp) + with subtest("succeed charlie"): + resp = client.succeed( + "curl -f -s -X POST " + + """ -H "Content-type: application/json" """ + + """ -H "Host: ldap.example.com" """ + + " http://server/auth/simple/login " + + """ -d '{"username": "charlie", "password": "${charliePassword}"}' """ + ) + print(resp) - with subtest("ldap user search"): - resp = server.succeed('ldapsearch -H ldap://127.0.0.1:${toString nodes.server.shb.lldap.ldapPort} -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w ${password}') - print(resp) + with subtest("ldap user search"): + resp = server.succeed('ldapsearch -H ldap://127.0.0.1:${toString nodes.server.shb.lldap.ldapPort} -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w ${password}') + print(resp) - if "uid=admin" not in resp: - raise Exception("Expected to find admin") + if "uid=admin" not in resp: + raise Exception("Expected to find admin") - if "uid=charlie" not in resp: - raise Exception("Expected to find charlie") + if "uid=charlie" not in resp: + raise Exception("Expected to find charlie") - with subtest("no debug"): - tests() + with subtest("no debug"): + tests() - with subtest("with debug"): - server.succeed('${specializations}/withDebug/bin/switch-to-configuration test') - tests() - ''; + with subtest("with debug"): + server.succeed('${specializations}/withDebug/bin/switch-to-configuration test') + tests() + ''; }; } diff --git a/test/blocks/mitmdump.nix b/test/blocks/mitmdump.nix index b6c0e11..d1ff0cb 100644 --- a/test/blocks/mitmdump.nix +++ b/test/blocks/mitmdump.nix @@ -1,137 +1,148 @@ { pkgs, lib, ... }: let - serve = port: text: lib.getExe (pkgs.writers.writePython3Bin "serve" - { - libraries = [ pkgs.python3Packages.systemd ]; - } - (let - content = pkgs.writeText "content" text; - in '' - from http.server import BaseHTTPRequestHandler, HTTPServer - from systemd.daemon import notify + serve = + port: text: + lib.getExe ( + pkgs.writers.writePython3Bin "serve" + { + libraries = [ pkgs.python3Packages.systemd ]; + } + ( + let + content = pkgs.writeText "content" text; + in + '' + from http.server import BaseHTTPRequestHandler, HTTPServer + from systemd.daemon import notify - with open("${content}", "rb") as f: - content = f.read() + with open("${content}", "rb") as f: + content = f.read() - class HardcodedHandler(BaseHTTPRequestHandler): - def do_GET(self): - reponse = content + self.path.encode('utf-8') - self.send_response(200) - self.send_header("Content-Type", "text/plain") - self.send_header("Content-Length", str(len(reponse))) - self.end_headers() - print("answering to GET request") - self.wfile.write(reponse) + class HardcodedHandler(BaseHTTPRequestHandler): + def do_GET(self): + reponse = content + self.path.encode('utf-8') + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.send_header("Content-Length", str(len(reponse))) + self.end_headers() + print("answering to GET request") + self.wfile.write(reponse) - def log_message(self, format, *args): - pass # optional: suppress logging + def log_message(self, format, *args): + pass # optional: suppress logging - if __name__ == "__main__": - notify('STATUS=Starting up...') - server_address = ('127.0.0.1', ${toString port}) - httpd = HTTPServer(server_address, HardcodedHandler) - print("Serving hardcoded page on http://127.0.0.1:${toString port}") - notify('READY=1') - httpd.serve_forever() - '') - ); + if __name__ == "__main__": + notify('STATUS=Starting up...') + server_address = ('127.0.0.1', ${toString port}) + httpd = HTTPServer(server_address, HardcodedHandler) + print("Serving hardcoded page on http://127.0.0.1:${toString port}") + notify('READY=1') + httpd.serve_forever() + '' + ) + ); in { default = lib.shb.runNixOSTest { name = "mitmdump-default"; - nodes.machine = { config, pkgs, ... }: { - imports = [ - ../../modules/blocks/mitmdump.nix - ]; - - systemd.services.test1 = { - serviceConfig.ExecStart = serve 8000 "test1"; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "notify"; - StandardOutput = "journal"; - StandardError = "journal"; - }; - }; - - systemd.services.test2 = { - serviceConfig.ExecStart = serve 8002 "test2"; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "notify"; - StandardOutput = "journal"; - StandardError = "journal"; - }; - }; - - shb.mitmdump.instances."test1" = { - listenPort = 8001; - upstreamPort = 8000; - after = [ "test1.service" ]; - }; - - shb.mitmdump.instances."test2" = { - listenPort = 8003; - upstreamPort = 8002; - after = [ "test2.service" ]; - enabledAddons = [ config.shb.mitmdump.addons.logger ]; - extraArgs = [ - "--set" "verbose_pattern=/verbose" + nodes.machine = + { config, pkgs, ... }: + { + imports = [ + ../../modules/blocks/mitmdump.nix ]; + + systemd.services.test1 = { + serviceConfig.ExecStart = serve 8000 "test1"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "notify"; + StandardOutput = "journal"; + StandardError = "journal"; + }; + }; + + systemd.services.test2 = { + serviceConfig.ExecStart = serve 8002 "test2"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "notify"; + StandardOutput = "journal"; + StandardError = "journal"; + }; + }; + + shb.mitmdump.instances."test1" = { + listenPort = 8001; + upstreamPort = 8000; + after = [ "test1.service" ]; + }; + + shb.mitmdump.instances."test2" = { + listenPort = 8003; + upstreamPort = 8002; + after = [ "test2.service" ]; + enabledAddons = [ config.shb.mitmdump.addons.logger ]; + extraArgs = [ + "--set" + "verbose_pattern=/verbose" + ]; + }; }; - }; - testScript = { nodes, ... }: '' - start_all() + testScript = + { nodes, ... }: + '' + start_all() - machine.wait_for_unit("test1.service") - machine.wait_for_unit("test2.service") - machine.wait_for_unit("mitmdump-test1.service") - machine.wait_for_unit("mitmdump-test2.service") + machine.wait_for_unit("test1.service") + machine.wait_for_unit("test2.service") + machine.wait_for_unit("mitmdump-test1.service") + machine.wait_for_unit("mitmdump-test2.service") - resp = machine.succeed("curl http://127.0.0.1:8000") - print(resp) - if resp != "test1/": - raise Exception("wanted 'test1'") + resp = machine.succeed("curl http://127.0.0.1:8000") + print(resp) + if resp != "test1/": + raise Exception("wanted 'test1'") - resp = machine.succeed("curl -v http://127.0.0.1:8001") - print(resp) - if resp != "test1/": - raise Exception("wanted 'test1'") + resp = machine.succeed("curl -v http://127.0.0.1:8001") + print(resp) + if resp != "test1/": + raise Exception("wanted 'test1'") - resp = machine.succeed("curl http://127.0.0.1:8002") - print(resp) - if resp != "test2/": - raise Exception("wanted 'test2'") + resp = machine.succeed("curl http://127.0.0.1:8002") + print(resp) + if resp != "test2/": + raise Exception("wanted 'test2'") - resp = machine.succeed("curl http://127.0.0.1:8003/notverbose") - print(resp) - if resp != "test2/notverbose": - raise Exception("wanted 'test2/notverbose'") + resp = machine.succeed("curl http://127.0.0.1:8003/notverbose") + print(resp) + if resp != "test2/notverbose": + raise Exception("wanted 'test2/notverbose'") - resp = machine.succeed("curl http://127.0.0.1:8003/verbose") - print(resp) - if resp != "test2/verbose": - raise Exception("wanted 'test2/verbose'") + resp = machine.succeed("curl http://127.0.0.1:8003/verbose") + print(resp) + if resp != "test2/verbose": + raise Exception("wanted 'test2/verbose'") - dump = machine.succeed("journalctl -b -u mitmdump-test1.service") - print(dump) - if "HTTP/1.0 200 OK" not in dump: - raise Exception("expected to see HTTP/1.0 200 OK") - if "test1" not in dump: - raise Exception("expected to see test1") + dump = machine.succeed("journalctl -b -u mitmdump-test1.service") + print(dump) + if "HTTP/1.0 200 OK" not in dump: + raise Exception("expected to see HTTP/1.0 200 OK") + if "test1" not in dump: + raise Exception("expected to see test1") - dump = machine.succeed("journalctl -b -u mitmdump-test2.service") - print(dump) - if "HTTP/1.0 200 OK" not in dump: - raise Exception("expected to see HTTP/1.0 200 OK") - if "test2/notverbose" in dump: - raise Exception("expected not to see test2/notverbose") - if "test2/verbose" not in dump: - raise Exception("expected to see test2/verbose") - ''; + dump = machine.succeed("journalctl -b -u mitmdump-test2.service") + print(dump) + if "HTTP/1.0 200 OK" not in dump: + raise Exception("expected to see HTTP/1.0 200 OK") + if "test2/notverbose" in dump: + raise Exception("expected not to see test2/notverbose") + if "test2/verbose" not in dump: + raise Exception("expected to see test2/verbose") + ''; }; } diff --git a/test/blocks/postgresql.nix b/test/blocks/postgresql.nix index 340e411..eab060d 100644 --- a/test/blocks/postgresql.nix +++ b/test/blocks/postgresql.nix @@ -6,177 +6,193 @@ in peerWithoutUser = lib.shb.runNixOSTest { name = "postgresql-peerWithoutUser"; - nodes.machine = { config, pkgs, ... }: { - imports = [ - (pkgs'.path + "/nixos/modules/profiles/headless.nix") - (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") - ../../modules/blocks/postgresql.nix - ]; + nodes.machine = + { config, pkgs, ... }: + { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + ../../modules/blocks/postgresql.nix + ]; - shb.postgresql.ensures = [ - { - username = "me"; - database = "me"; - } - ]; - }; + shb.postgresql.ensures = [ + { + username = "me"; + database = "me"; + } + ]; + }; - testScript = { nodes, ... }: '' - start_all() - machine.wait_for_unit("postgresql.service") - machine.wait_for_open_port(5432) + testScript = + { nodes, ... }: + '' + start_all() + machine.wait_for_unit("postgresql.service") + machine.wait_for_open_port(5432) - def peer_cmd(user, database): - return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) + def peer_cmd(user, database): + return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) - with subtest("cannot login because of missing user"): - machine.fail(peer_cmd("me", "me"), timeout=10) + with subtest("cannot login because of missing user"): + machine.fail(peer_cmd("me", "me"), timeout=10) - with subtest("cannot login with unknown user"): - machine.fail(peer_cmd("notme", "me"), timeout=10) + with subtest("cannot login with unknown user"): + machine.fail(peer_cmd("notme", "me"), timeout=10) - with subtest("cannot login to unknown database"): - machine.fail(peer_cmd("me", "notmine"), timeout=10) - ''; + with subtest("cannot login to unknown database"): + machine.fail(peer_cmd("me", "notmine"), timeout=10) + ''; }; peerAuth = lib.shb.runNixOSTest { name = "postgresql-peerAuth"; - nodes.machine = { config, pkgs, ... }: { - imports = [ - (pkgs'.path + "/nixos/modules/profiles/headless.nix") - (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") - ../../modules/blocks/postgresql.nix - ]; + nodes.machine = + { config, pkgs, ... }: + { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + ../../modules/blocks/postgresql.nix + ]; - users.users.me = { - isSystemUser = true; - group = "me"; - extraGroups = [ "sudoers" ]; + users.users.me = { + isSystemUser = true; + group = "me"; + extraGroups = [ "sudoers" ]; + }; + users.groups.me = { }; + + shb.postgresql.ensures = [ + { + username = "me"; + database = "me"; + } + ]; }; - users.groups.me = {}; - shb.postgresql.ensures = [ - { - username = "me"; - database = "me"; - } - ]; - }; + testScript = + { nodes, ... }: + '' + start_all() + machine.wait_for_unit("postgresql.service") + machine.wait_for_open_port(5432) - testScript = { nodes, ... }: '' - start_all() - machine.wait_for_unit("postgresql.service") - machine.wait_for_open_port(5432) + def peer_cmd(user, database): + return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) - def peer_cmd(user, database): - return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) + def tcpip_cmd(user, database, port): + return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port) - def tcpip_cmd(user, database, port): - return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port) + with subtest("can login with provisioned user and database"): + machine.succeed(peer_cmd("me", "me"), timeout=10) - with subtest("can login with provisioned user and database"): - machine.succeed(peer_cmd("me", "me"), timeout=10) + with subtest("cannot login with unknown user"): + machine.fail(peer_cmd("notme", "me"), timeout=10) - with subtest("cannot login with unknown user"): - machine.fail(peer_cmd("notme", "me"), timeout=10) + with subtest("cannot login to unknown database"): + machine.fail(peer_cmd("me", "notmine"), timeout=10) - with subtest("cannot login to unknown database"): - machine.fail(peer_cmd("me", "notmine"), timeout=10) - - with subtest("cannot login with tcpip"): - machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10) - ''; + with subtest("cannot login with tcpip"): + machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10) + ''; }; tcpIPWithoutPasswordAuth = lib.shb.runNixOSTest { name = "postgresql-tcpIpWithoutPasswordAuth"; - nodes.machine = { config, pkgs, ... }: { - imports = [ - (pkgs'.path + "/nixos/modules/profiles/headless.nix") - (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") - ../../modules/blocks/postgresql.nix - ]; + nodes.machine = + { config, pkgs, ... }: + { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + ../../modules/blocks/postgresql.nix + ]; - shb.postgresql.enableTCPIP = true; - shb.postgresql.ensures = [ - { - username = "me"; - database = "me"; - } - ]; - }; + shb.postgresql.enableTCPIP = true; + shb.postgresql.ensures = [ + { + username = "me"; + database = "me"; + } + ]; + }; - testScript = { nodes, ... }: '' - start_all() - machine.wait_for_unit("postgresql.service") - machine.wait_for_open_port(5432) + testScript = + { nodes, ... }: + '' + start_all() + machine.wait_for_unit("postgresql.service") + machine.wait_for_open_port(5432) - def peer_cmd(user, database): - return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) + def peer_cmd(user, database): + return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) - def tcpip_cmd(user, database, port): - return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port) + def tcpip_cmd(user, database, port): + return "psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port) - with subtest("cannot login without existing user"): - machine.fail(peer_cmd("me", "me"), timeout=10) + with subtest("cannot login without existing user"): + machine.fail(peer_cmd("me", "me"), timeout=10) - with subtest("cannot login with user without password"): - machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10) - ''; + with subtest("cannot login with user without password"): + machine.fail(tcpip_cmd("me", "me", "5432"), timeout=10) + ''; }; tcpIPPasswordAuth = lib.shb.runNixOSTest { name = "postgresql-tcpIPPasswordAuth"; - nodes.machine = { config, pkgs, ... }: { - imports = [ - (pkgs'.path + "/nixos/modules/profiles/headless.nix") - (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") - ../../modules/blocks/postgresql.nix - ]; + nodes.machine = + { config, pkgs, ... }: + { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + ../../modules/blocks/postgresql.nix + ]; - users.users.me = { - isSystemUser = true; - group = "me"; - extraGroups = [ "sudoers" ]; + users.users.me = { + isSystemUser = true; + group = "me"; + extraGroups = [ "sudoers" ]; + }; + users.groups.me = { }; + + system.activationScripts.secret = '' + echo secretpw > /run/dbsecret + ''; + shb.postgresql.enableTCPIP = true; + shb.postgresql.ensures = [ + { + username = "me"; + database = "me"; + passwordFile = "/run/dbsecret"; + } + ]; }; - users.groups.me = {}; - system.activationScripts.secret = '' - echo secretpw > /run/dbsecret + testScript = + { nodes, ... }: + '' + start_all() + machine.wait_for_unit("postgresql.service") + machine.wait_for_open_port(5432) + + def peer_cmd(user, database): + return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) + + def tcpip_cmd(user, database, port, password): + return "PGPASSWORD={password} psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port, password=password) + + with subtest("can peer login with provisioned user and database"): + machine.succeed(peer_cmd("me", "me"), timeout=10) + + with subtest("can tcpip login with provisioned user and database"): + machine.succeed(tcpip_cmd("me", "me", "5432", "secretpw"), timeout=10) + + with subtest("cannot tcpip login with wrong password"): + machine.fail(tcpip_cmd("me", "me", "5432", "oops"), timeout=10) ''; - shb.postgresql.enableTCPIP = true; - shb.postgresql.ensures = [ - { - username = "me"; - database = "me"; - passwordFile = "/run/dbsecret"; - } - ]; - }; - - testScript = { nodes, ... }: '' - start_all() - machine.wait_for_unit("postgresql.service") - machine.wait_for_open_port(5432) - - def peer_cmd(user, database): - return "sudo -u me psql -U {user} {db} --command \"\"".format(user=user, db=database) - - def tcpip_cmd(user, database, port, password): - return "PGPASSWORD={password} psql -h 127.0.0.1 -p {port} -U {user} {db} --command \"\"".format(user=user, db=database, port=port, password=password) - - with subtest("can peer login with provisioned user and database"): - machine.succeed(peer_cmd("me", "me"), timeout=10) - - with subtest("can tcpip login with provisioned user and database"): - machine.succeed(tcpip_cmd("me", "me", "5432", "secretpw"), timeout=10) - - with subtest("cannot tcpip login with wrong password"): - machine.fail(tcpip_cmd("me", "me", "5432", "oops"), timeout=10) - ''; }; } diff --git a/test/blocks/restic.nix b/test/blocks/restic.nix index 40c05dd..76265d1 100644 --- a/test/blocks/restic.nix +++ b/test/blocks/restic.nix @@ -1,179 +1,188 @@ { pkgs, lib, ... }: let - testLib = pkgs.callPackage ../common.nix {}; + testLib = pkgs.callPackage ../common.nix { }; - commonTest = user: lib.shb.runNixOSTest { - name = "restic_backupAndRestore_${user}"; + commonTest = + user: + lib.shb.runNixOSTest { + name = "restic_backupAndRestore_${user}"; - nodes.machine = { config, ... }: { - imports = [ - testLib.baseImports + nodes.machine = + { config, ... }: + { + imports = [ + testLib.baseImports - ../../modules/blocks/hardcodedsecret.nix - ../../modules/blocks/restic.nix - ]; + ../../modules/blocks/hardcodedsecret.nix + ../../modules/blocks/restic.nix + ]; - shb.hardcodedsecret.A = { - request = { - owner = "root"; - group = "keys"; - mode = "0440"; - }; - settings.content = "secretA"; - }; - shb.hardcodedsecret.B = { - request = { - owner = "root"; - group = "keys"; - mode = "0440"; - }; - settings.content = "secretB"; - }; - - shb.hardcodedsecret.passphrase = { - request = config.shb.restic.instances."testinstance".settings.passphrase.request; - settings.content = "secretB"; - }; - - shb.restic.instances."testinstance" = { - settings = { - enable = true; - - passphrase.result = config.shb.hardcodedsecret.passphrase.result; - - repository = { - path = "/opt/repos/A"; - timerConfig = { - OnCalendar = "00:00:00"; - RandomizedDelaySec = "5h"; + shb.hardcodedsecret.A = { + request = { + owner = "root"; + group = "keys"; + mode = "0440"; }; - # Those are not needed by the repository but are still included - # so we can test them in the hooks section. - secrets = { - A.source = config.shb.hardcodedsecret.A.result.path; - B.source = config.shb.hardcodedsecret.B.result.path; + settings.content = "secretA"; + }; + shb.hardcodedsecret.B = { + request = { + owner = "root"; + group = "keys"; + mode = "0440"; + }; + settings.content = "secretB"; + }; + + shb.hardcodedsecret.passphrase = { + request = config.shb.restic.instances."testinstance".settings.passphrase.request; + settings.content = "secretB"; + }; + + shb.restic.instances."testinstance" = { + settings = { + enable = true; + + passphrase.result = config.shb.hardcodedsecret.passphrase.result; + + repository = { + path = "/opt/repos/A"; + timerConfig = { + OnCalendar = "00:00:00"; + RandomizedDelaySec = "5h"; + }; + # Those are not needed by the repository but are still included + # so we can test them in the hooks section. + secrets = { + A.source = config.shb.hardcodedsecret.A.result.path; + B.source = config.shb.hardcodedsecret.B.result.path; + }; + }; + }; + + request = { + inherit user; + + sourceDirectories = [ + "/opt/files/A" + "/opt/files/B" + ]; + + hooks.beforeBackup = [ + '' + echo $RUNTIME_DIRECTORY + if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then + if ! [ -f /run/secrets_restic/restic-backups-testinstance_opt_repos_A ]; then + exit 10 + fi + if [ -z "$A" ] || ! [ "$A" = "secretA" ]; then + echo "A:$A" + exit 11 + fi + if [ -z "$B" ] || ! [ "$B" = "secretB" ]; then + echo "B:$B" + exit 12 + fi + fi + '' + ]; }; }; }; - request = { - inherit user; + extraPythonPackages = p: [ p.dictdiffer ]; + skipTypeCheck = true; - sourceDirectories = [ - "/opt/files/A" - "/opt/files/B" - ]; + testScript = + { nodes, ... }: + let + provider = nodes.machine.shb.restic.instances."testinstance"; + backupService = provider.result.backupService; + restoreScript = provider.result.restoreScript; + in + '' + from dictdiffer import diff + + def list_files(dir): + files_and_content = {} + + files = machine.succeed(f""" + find {dir} -type f + """).split("\n")[:-1] + + for f in files: + content = machine.succeed(f""" + cat {f} + """).strip() + files_and_content[f] = content + + return files_and_content + + def assert_files(dir, files): + result = list(diff(list_files(dir), files)) + if len(result) > 0: + raise Exception("Unexpected files:", result) + + with subtest("Create initial content"): + machine.succeed(""" + mkdir -p /opt/files/A + mkdir -p /opt/files/B + + echo repoA_fileA_1 > /opt/files/A/fileA + echo repoA_fileB_1 > /opt/files/A/fileB + echo repoB_fileA_1 > /opt/files/B/fileA + echo repoB_fileB_1 > /opt/files/B/fileB + + chown ${user}: -R /opt/files + chmod go-rwx -R /opt/files + """) + + assert_files("/opt/files", { + '/opt/files/B/fileA': 'repoB_fileA_1', + '/opt/files/B/fileB': 'repoB_fileB_1', + '/opt/files/A/fileA': 'repoA_fileA_1', + '/opt/files/A/fileB': 'repoA_fileB_1', + }) + + with subtest("First backup in repo A"): + machine.succeed("systemctl start ${backupService}") + + with subtest("New content"): + machine.succeed(""" + echo repoA_fileA_2 > /opt/files/A/fileA + echo repoA_fileB_2 > /opt/files/A/fileB + echo repoB_fileA_2 > /opt/files/B/fileA + echo repoB_fileB_2 > /opt/files/B/fileB + """) + + assert_files("/opt/files", { + '/opt/files/B/fileA': 'repoB_fileA_2', + '/opt/files/B/fileB': 'repoB_fileB_2', + '/opt/files/A/fileA': 'repoA_fileA_2', + '/opt/files/A/fileB': 'repoA_fileB_2', + }) + + with subtest("Delete content"): + machine.succeed(""" + rm -r /opt/files/A /opt/files/B + """) + + assert_files("/opt/files", {}) + + with subtest("Restore initial content from repo A"): + machine.succeed(""" + ${restoreScript} restore latest + """) + + assert_files("/opt/files", { + '/opt/files/B/fileA': 'repoB_fileA_1', + '/opt/files/B/fileB': 'repoB_fileB_1', + '/opt/files/A/fileA': 'repoA_fileA_1', + '/opt/files/A/fileB': 'repoA_fileB_1', + }) + ''; - hooks.beforeBackup = ['' - echo $RUNTIME_DIRECTORY - if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then - if ! [ -f /run/secrets_restic/restic-backups-testinstance_opt_repos_A ]; then - exit 10 - fi - if [ -z "$A" ] || ! [ "$A" = "secretA" ]; then - echo "A:$A" - exit 11 - fi - if [ -z "$B" ] || ! [ "$B" = "secretB" ]; then - echo "B:$B" - exit 12 - fi - fi - '']; - }; - }; }; - - extraPythonPackages = p: [ p.dictdiffer ]; - skipTypeCheck = true; - - testScript = { nodes, ... }: let - provider = nodes.machine.shb.restic.instances."testinstance"; - backupService = provider.result.backupService; - restoreScript = provider.result.restoreScript; - in '' - from dictdiffer import diff - - def list_files(dir): - files_and_content = {} - - files = machine.succeed(f""" - find {dir} -type f - """).split("\n")[:-1] - - for f in files: - content = machine.succeed(f""" - cat {f} - """).strip() - files_and_content[f] = content - - return files_and_content - - def assert_files(dir, files): - result = list(diff(list_files(dir), files)) - if len(result) > 0: - raise Exception("Unexpected files:", result) - - with subtest("Create initial content"): - machine.succeed(""" - mkdir -p /opt/files/A - mkdir -p /opt/files/B - - echo repoA_fileA_1 > /opt/files/A/fileA - echo repoA_fileB_1 > /opt/files/A/fileB - echo repoB_fileA_1 > /opt/files/B/fileA - echo repoB_fileB_1 > /opt/files/B/fileB - - chown ${user}: -R /opt/files - chmod go-rwx -R /opt/files - """) - - assert_files("/opt/files", { - '/opt/files/B/fileA': 'repoB_fileA_1', - '/opt/files/B/fileB': 'repoB_fileB_1', - '/opt/files/A/fileA': 'repoA_fileA_1', - '/opt/files/A/fileB': 'repoA_fileB_1', - }) - - with subtest("First backup in repo A"): - machine.succeed("systemctl start ${backupService}") - - with subtest("New content"): - machine.succeed(""" - echo repoA_fileA_2 > /opt/files/A/fileA - echo repoA_fileB_2 > /opt/files/A/fileB - echo repoB_fileA_2 > /opt/files/B/fileA - echo repoB_fileB_2 > /opt/files/B/fileB - """) - - assert_files("/opt/files", { - '/opt/files/B/fileA': 'repoB_fileA_2', - '/opt/files/B/fileB': 'repoB_fileB_2', - '/opt/files/A/fileA': 'repoA_fileA_2', - '/opt/files/A/fileB': 'repoA_fileB_2', - }) - - with subtest("Delete content"): - machine.succeed(""" - rm -r /opt/files/A /opt/files/B - """) - - assert_files("/opt/files", {}) - - with subtest("Restore initial content from repo A"): - machine.succeed(""" - ${restoreScript} restore latest - """) - - assert_files("/opt/files", { - '/opt/files/B/fileA': 'repoB_fileA_1', - '/opt/files/B/fileB': 'repoB_fileB_1', - '/opt/files/A/fileA': 'repoA_fileA_1', - '/opt/files/A/fileB': 'repoA_fileB_1', - }) - ''; - - }; in { backupAndRestoreRoot = commonTest "root"; diff --git a/test/blocks/ssl.nix b/test/blocks/ssl.nix index 3f38704..8d4c9cf 100644 --- a/test/blocks/ssl.nix +++ b/test/blocks/ssl.nix @@ -6,95 +6,100 @@ in test = lib.shb.runNixOSTest { name = "ssl-test"; - nodes.server = { config, pkgs, ... }: { - imports = [ - (pkgs'.path + "/nixos/modules/profiles/headless.nix") - (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") - ../../modules/blocks/ssl.nix - ]; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + (pkgs'.path + "/nixos/modules/profiles/headless.nix") + (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix") + ../../modules/blocks/ssl.nix + ]; - users.users = { - user1 = { - group = "group1"; - isSystemUser = true; - }; - user2 = { - group = "group2"; - isSystemUser = true; - }; - }; - users.groups = { - group1 = {}; - group2 = {}; - }; - - shb.certs = { - cas.selfsigned = { - myca = { - name = "My CA"; + users.users = { + user1 = { + group = "group1"; + isSystemUser = true; }; - myotherca = { - name = "My Other CA"; - }; - }; - certs.selfsigned = { - top = { - ca = config.shb.certs.cas.selfsigned.myca; - - domain = "example.com"; - group = "nginx"; - }; - subdomain = { - ca = config.shb.certs.cas.selfsigned.myca; - - domain = "subdomain.example.com"; - group = "nginx"; - }; - multi = { - ca = config.shb.certs.cas.selfsigned.myca; - - domain = "multi1.example.com"; - extraDomains = [ "multi2.example.com" "multi3.example.com" ]; - group = "nginx"; - }; - - cert1 = { - ca = config.shb.certs.cas.selfsigned.myca; - - domain = "cert1.example.com"; - }; - cert2 = { - ca = config.shb.certs.cas.selfsigned.myca; - - domain = "cert2.example.com"; + user2 = { group = "group2"; + isSystemUser = true; }; }; - }; + users.groups = { + group1 = { }; + group2 = { }; + }; - # The configuration below is to create a webserver that uses the server certificate. - networking.hosts."127.0.0.1" = [ - "example.com" - "subdomain.example.com" - "wrong.example.com" - "multi1.example.com" - "multi2.example.com" - "multi3.example.com" - ]; - - services.nginx.enable = true; - services.nginx.virtualHosts = - let - mkVirtualHost = response: cert: { - onlySSL = true; - sslCertificate = cert.paths.cert; - sslCertificateKey = cert.paths.key; - locations."/".extraConfig = '' - add_header Content-Type text/plain; - return 200 '${response}'; - ''; + shb.certs = { + cas.selfsigned = { + myca = { + name = "My CA"; + }; + myotherca = { + name = "My Other CA"; + }; }; - in + certs.selfsigned = { + top = { + ca = config.shb.certs.cas.selfsigned.myca; + + domain = "example.com"; + group = "nginx"; + }; + subdomain = { + ca = config.shb.certs.cas.selfsigned.myca; + + domain = "subdomain.example.com"; + group = "nginx"; + }; + multi = { + ca = config.shb.certs.cas.selfsigned.myca; + + domain = "multi1.example.com"; + extraDomains = [ + "multi2.example.com" + "multi3.example.com" + ]; + group = "nginx"; + }; + + cert1 = { + ca = config.shb.certs.cas.selfsigned.myca; + + domain = "cert1.example.com"; + }; + cert2 = { + ca = config.shb.certs.cas.selfsigned.myca; + + domain = "cert2.example.com"; + group = "group2"; + }; + }; + }; + + # The configuration below is to create a webserver that uses the server certificate. + networking.hosts."127.0.0.1" = [ + "example.com" + "subdomain.example.com" + "wrong.example.com" + "multi1.example.com" + "multi2.example.com" + "multi3.example.com" + ]; + + services.nginx.enable = true; + services.nginx.virtualHosts = + let + mkVirtualHost = response: cert: { + onlySSL = true; + sslCertificate = cert.paths.cert; + sslCertificateKey = cert.paths.key; + locations."/".extraConfig = '' + add_header Content-Type text/plain; + return 200 '${response}'; + ''; + }; + in { "example.com" = mkVirtualHost "Top domain" config.shb.certs.certs.selfsigned.top; "subdomain.example.com" = mkVirtualHost "Subdomain" config.shb.certs.certs.selfsigned.subdomain; @@ -102,26 +107,27 @@ in "multi2.example.com" = mkVirtualHost "multi2" config.shb.certs.certs.selfsigned.multi; "multi3.example.com" = mkVirtualHost "multi3" config.shb.certs.certs.selfsigned.multi; }; - systemd.services.nginx = { - after = [ - config.shb.certs.certs.selfsigned.top.systemdService - config.shb.certs.certs.selfsigned.subdomain.systemdService - config.shb.certs.certs.selfsigned.multi.systemdService - config.shb.certs.certs.selfsigned.cert1.systemdService - config.shb.certs.certs.selfsigned.cert2.systemdService - ]; - requires = [ - config.shb.certs.certs.selfsigned.top.systemdService - config.shb.certs.certs.selfsigned.subdomain.systemdService - config.shb.certs.certs.selfsigned.multi.systemdService - config.shb.certs.certs.selfsigned.cert1.systemdService - config.shb.certs.certs.selfsigned.cert2.systemdService - ]; + systemd.services.nginx = { + after = [ + config.shb.certs.certs.selfsigned.top.systemdService + config.shb.certs.certs.selfsigned.subdomain.systemdService + config.shb.certs.certs.selfsigned.multi.systemdService + config.shb.certs.certs.selfsigned.cert1.systemdService + config.shb.certs.certs.selfsigned.cert2.systemdService + ]; + requires = [ + config.shb.certs.certs.selfsigned.top.systemdService + config.shb.certs.certs.selfsigned.subdomain.systemdService + config.shb.certs.certs.selfsigned.multi.systemdService + config.shb.certs.certs.selfsigned.cert1.systemdService + config.shb.certs.certs.selfsigned.cert2.systemdService + ]; + }; }; - }; # Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix - testScript = { nodes, ... }: + testScript = + { nodes, ... }: let myca = nodes.server.shb.certs.cas.selfsigned.myca; myotherca = nodes.server.shb.certs.cas.selfsigned.myotherca; @@ -131,7 +137,7 @@ in cert1 = nodes.server.shb.certs.certs.selfsigned.cert1; cert2 = nodes.server.shb.certs.certs.selfsigned.cert2; in - '' + '' start_all() # Make sure certs are generated. @@ -203,6 +209,6 @@ in server.fail("curl --cacert /etc/static/ssl/certs/ca-bundle.crt --fail-with-body -v https://subdomain.example.com") server.fail("curl --cacert /etc/static/ssl/certs/ca-certificates.crt --fail-with-body -v https://example.com") server.fail("curl --cacert /etc/static/ssl/certs/ca-certificates.crt --fail-with-body -v https://subdomain.example.com") - ''; + ''; }; } diff --git a/test/common.nix b/test/common.nix index e62f2d5..aac81a6 100644 --- a/test/common.nix +++ b/test/common.nix @@ -1,7 +1,14 @@ { pkgs, lib }: let inherit (lib) hasAttr mkOption optionalString; - inherit (lib.types) bool enum listOf nullOr submodule str; + inherit (lib.types) + bool + enum + listOf + nullOr + submodule + str + ; baseImports = { imports = [ @@ -10,15 +17,17 @@ let ]; }; - accessScript = lib.makeOverridable ({ - hasSSL - , waitForServices ? s: [] - , waitForPorts ? p: [] - , waitForUnixSocket ? u: [] - , waitForUrls ? u: [] - , extraScript ? {...}: "" - , redirectSSO ? false - }: { nodes, ... }: + accessScript = lib.makeOverridable ( + { + hasSSL, + waitForServices ? s: [ ], + waitForPorts ? p: [ ], + waitForUnixSocket ? u: [ ], + waitForUrls ? u: [ ], + extraScript ? { ... }: "", + redirectSSO ? false, + }: + { nodes, ... }: let cfg = nodes.server.test; @@ -35,34 +44,34 @@ let lldapEnabled = (hasAttr "lldap" nodes.server.shb) && nodes.server.shb.lldap.enable; in '' - import json - import os - import pathlib + import json + import os + import pathlib - start_all() + start_all() - def curl(target, format, endpoint, data="", extra=""): - cmd = ("curl --show-error --location" - + " --cookie-jar cookie.txt" - + " --cookie cookie.txt" - + " --connect-to ${fqdn}:443:server:443" - + " --connect-to ${fqdn}:80:server:80" - # Client must be able to resolve talking to auth server - + " --connect-to auth.${cfg.domain}:443:server:443" - + (f" --data '{data}'" if data != "" else "") - + (f" --silent --output /dev/null --write-out '{format}'" if format != "" else "") - + (f" {extra}" if extra != "" else "") - + f" {endpoint}") - print(cmd) - _, r = target.execute(cmd) - print(r) - try: - return json.loads(r) - except: - return r + def curl(target, format, endpoint, data="", extra=""): + cmd = ("curl --show-error --location" + + " --cookie-jar cookie.txt" + + " --cookie cookie.txt" + + " --connect-to ${fqdn}:443:server:443" + + " --connect-to ${fqdn}:80:server:80" + # Client must be able to resolve talking to auth server + + " --connect-to auth.${cfg.domain}:443:server:443" + + (f" --data '{data}'" if data != "" else "") + + (f" --silent --output /dev/null --write-out '{format}'" if format != "" else "") + + (f" {extra}" if extra != "" else "") + + f" {endpoint}") + print(cmd) + _, r = target.execute(cmd) + print(r) + try: + return json.loads(r) + except: + return r - def unline_with(j, s): - return j.join((x.strip() for x in s.split("\n"))) + def unline_with(j, s): + return j.join((x.strip() for x in s.split("\n"))) '' + lib.strings.concatMapStrings (s: ''server.wait_for_unit("${s}")'' + "\n") ( waitForServices args @@ -72,19 +81,25 @@ let + lib.strings.concatMapStrings (p: ''server.wait_for_open_port(${toString p})'' + "\n") ( waitForPorts args # TODO: when the SSO block exists, replace this hardcoded port. - ++ (lib.optionals autheliaEnabled [ 9091 /* nodes.server.services.authelia.instances."auth.${domain}".settings.server.port */ ]) + ++ (lib.optionals autheliaEnabled [ + 9091 # nodes.server.services.authelia.instances."auth.${domain}".settings.server.port + ]) + ) + + lib.strings.concatMapStrings (u: ''server.wait_for_open_unix_socket("${u}")'' + "\n") ( + waitForUnixSocket args ) - + lib.strings.concatMapStrings (u: ''server.wait_for_open_unix_socket("${u}")'' + "\n") (waitForUnixSocket args) + '' - if ${if hasSSL args then "True" else "False"}: - server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt") - client.succeed("rm -r /etc/ssl/certs") - client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt") + if ${if hasSSL args then "True" else "False"}: + server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt") + client.succeed("rm -r /etc/ssl/certs") + client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt") '' # Making a curl request to an URL needs to happen after we copied the certificates over, # otherwise curl will not be able to verify the "legitimacy of the server". - + lib.strings.concatMapStrings (u: '' + + lib.strings.concatMapStrings ( + u: + '' import time done = False @@ -97,416 +112,472 @@ let done = response.get('code') == 200 if not done: raise Exception(f"Response was never 200, got last: {response}") - '' + "\n") ( - waitForUrls args + '' + + "\n" + ) (waitForUrls args) + + ( + if (!redirectSSO) then + '' + with subtest("access"): + response = curl(client, """{"code":%{response_code}}""", "${proto_fqdn}") + + if response['code'] != 200: + raise Exception(f"Code is {response['code']}") + '' + else + '' + with subtest("unauthenticated access is not granted"): + response = curl(client, """{"code":%{response_code},"auth_host":"%{urle.host}","auth_query":"%{urle.query}","all":%{json}}""", "${proto_fqdn}") + + if response['code'] != 200: + raise Exception(f"Code is {response['code']}") + if response['auth_host'] != "auth.${cfg.domain}": + raise Exception(f"auth host should be auth.${cfg.domain} but is {response['auth_host']}") + if response['auth_query'] != "rd=${proto_fqdn}/": + raise Exception(f"auth query should be rd=${proto_fqdn}/ but is {response['auth_query']}") + '' + ) + + ( + let + script = extraScript args; + in + lib.optionalString (script != "") script ) - + (if (! redirectSSO) then '' - with subtest("access"): - response = curl(client, """{"code":%{response_code}}""", "${proto_fqdn}") - - if response['code'] != 200: - raise Exception(f"Code is {response['code']}") - '' else '' - with subtest("unauthenticated access is not granted"): - response = curl(client, """{"code":%{response_code},"auth_host":"%{urle.host}","auth_query":"%{urle.query}","all":%{json}}""", "${proto_fqdn}") - - if response['code'] != 200: - raise Exception(f"Code is {response['code']}") - if response['auth_host'] != "auth.${cfg.domain}": - raise Exception(f"auth host should be auth.${cfg.domain} but is {response['auth_host']}") - if response['auth_query'] != "rd=${proto_fqdn}/": - raise Exception(f"auth query should be rd=${proto_fqdn}/ but is {response['auth_query']}") - '') - + (let - script = extraScript args; - in - lib.optionalString (script != "") script) + (optionalString (hasAttr "test" nodes.server && hasAttr "login" nodes.server.test) '' - with subtest("Login from server"): - code, logs = server.execute("login_playwright") - print(logs) - try: - server.copy_from_vm("trace") - except: - print("No trace found on server") - if code != 0: - raise Exception("login_playwright did not succeed") + with subtest("Login from server"): + code, logs = server.execute("login_playwright") + print(logs) + try: + server.copy_from_vm("trace") + except: + print("No trace found on server") + if code != 0: + raise Exception("login_playwright did not succeed") '') + (optionalString (hasAttr "test" nodes.client && hasAttr "login" nodes.client.test) '' - with subtest("Login from client"): - code, logs = client.execute("login_playwright") - print(logs) - try: - client.copy_from_vm("trace") - except: - print("No trace found on client") - if code != 0: - raise Exception("login_playwright did not succeed") + with subtest("Login from client"): + code, logs = client.execute("login_playwright") + print(logs) + try: + client.copy_from_vm("trace") + except: + print("No trace found on client") + if code != 0: + raise Exception("login_playwright did not succeed") '') ); - backupScript = args: (accessScript args).override { - extraScript = { proto_fqdn, ... }: '' - with subtest("backup"): - server.succeed("systemctl start restic-backups-testinstance_opt_repos_A") - ''; - }; + backupScript = + args: + (accessScript args).override { + extraScript = + { proto_fqdn, ... }: + '' + with subtest("backup"): + server.succeed("systemctl start restic-backups-testinstance_opt_repos_A") + ''; + }; in { inherit baseImports accessScript; - runNixOSTest = args: pkgs.testers.runNixOSTest ({ - interactive.sshBackdoor.enable = true; - } // args); + runNixOSTest = + args: + pkgs.testers.runNixOSTest ( + { + interactive.sshBackdoor.enable = true; + } + // args + ); - mkScripts = args: + mkScripts = args: { + access = accessScript args; + backup = backupScript args; + }; + + baseModule = + { config, ... }: { - access = accessScript args; - backup = backupScript args; - }; - - baseModule = { config, ... }: { - options.test = { - domain = mkOption { - type = str; - default = "example.com"; + options.test = { + domain = mkOption { + type = str; + default = "example.com"; + }; + subdomain = mkOption { + type = str; + }; + fqdn = mkOption { + type = str; + readOnly = true; + default = "${config.test.subdomain}.${config.test.domain}"; + }; + hasSSL = mkOption { + type = bool; + default = false; + }; + proto = mkOption { + type = str; + readOnly = true; + default = if config.test.hasSSL then "https" else "http"; + }; + proto_fqdn = mkOption { + type = str; + readOnly = true; + default = "${config.test.proto}://${config.test.fqdn}"; + }; }; - subdomain = mkOption { - type = str; - }; - fqdn = mkOption { - type = str; - readOnly = true; - default = "${config.test.subdomain}.${config.test.domain}"; - }; - hasSSL = mkOption { - type = bool; - default = false; - }; - proto = mkOption { - type = str; - readOnly = true; - default = if config.test.hasSSL then "https" else "http"; - }; - proto_fqdn = mkOption { - type = str; - readOnly = true; - default = "${config.test.proto}://${config.test.fqdn}"; - }; - }; - imports = [ - baseImports - ../modules/blocks/authelia.nix - ../modules/blocks/hardcodedsecret.nix - ../modules/blocks/mitmdump.nix - ../modules/blocks/nginx.nix - ../modules/blocks/postgresql.nix - ]; - config = { - # HTTP(s) server port. - networking.firewall.allowedTCPPorts = [ 80 443 ]; - shb.nginx.accessLog = true; - - networking.hosts = { - "192.168.1.2" = [ config.test.fqdn "auth.${config.test.domain}" ]; - }; - }; - }; - - clientLoginModule = { config, pkgs, ... }: let - cfg = config.test.login; - in { - options.test.login = { - browser = mkOption { - type = enum [ "firefox" "chromium" "webkit" ]; - default = "firefox"; - }; - usernameFieldLabelRegex = mkOption { - type = str; - default = "[Uu]sername"; - }; - usernameFieldSelector = mkOption { - type = str; - default = "get_by_label(re.compile('${cfg.usernameFieldLabelRegex}'))"; - }; - passwordFieldLabelRegex = mkOption { - type = str; - default = "[Pp]assword"; - }; - passwordFieldSelector = mkOption { - type = str; - default = "get_by_label(re.compile('${cfg.passwordFieldLabelRegex}'))"; - }; - loginButtonNameRegex = mkOption { - type = str; - default = "[Ll]ogin"; - }; - testLoginWith = mkOption { - type = listOf (submodule { - options = { - username = mkOption { - type = nullOr str; - default = null; - }; - password = mkOption { - type = nullOr str; - default = null; - }; - nextPageExpect = mkOption { - type = listOf str; - }; - }; - }); - }; - startUrl = mkOption { - type = str; - default = "http://${config.test.fqdn}"; - }; - beforeHook = mkOption { - type = str; - default = ""; - }; - }; - config = { - networking.hosts = { - "192.168.1.2" = [ config.test.fqdn "auth.${config.test.domain}" ]; - }; - - environment.variables = { - PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers; - }; - - environment.systemPackages = [ - (pkgs.writers.writePython3Bin "login_playwright" - { - libraries = [ pkgs.python3Packages.playwright ]; - flakeIgnore = [ "F401" "E501" ]; - } - (let - testCfg = pkgs.writeText "users.json" (builtins.toJSON cfg); - in '' - import json - import re - import sys - from playwright.sync_api import expect - from playwright.sync_api import sync_playwright - - - browsers = { - "chromium": {'args': ["--headless", "--disable-gpu"], 'channel': 'chromium'}, - "firefox": {'args': ["--reporter", "html"]}, - "webkit": {}, - } - - with open("${testCfg}") as f: - testCfg = json.load(f) - - browser_name = testCfg['browser'] - browser_args = browsers.get(browser_name) - print(f"Running test on {browser_name} {' '.join(browser_args)}") - - with sync_playwright() as p: - browser = getattr(p, browser_name).launch(**browser_args) - - for i, u in enumerate(testCfg["testLoginWith"]): - print(f"Testing for user {u['username']} and password {u['password']}") - - context = browser.new_context(ignore_https_errors=True) - context.set_default_navigation_timeout(2 * 60 * 1000) - context.tracing.start(screenshots=True, snapshots=True, sources=True) - try: - page = context.new_page() - print(f"Going to {testCfg['startUrl']}") - page.goto(testCfg['startUrl']) - - if testCfg.get("beforeHook") is not None: - exec(testCfg.get("beforeHook")) - - if u['username'] is not None: - print(f"Filling field username with {u['username']}") - page.${cfg.usernameFieldSelector}.fill(u['username']) - if u['password'] is not None: - print(f"Filling field password with {u['password']}") - page.${cfg.passwordFieldSelector}.fill(u['password']) - - # Assumes we don't need to login, so skip this. - if u['username'] is not None or u['password'] is not None: - print(f"Clicking button {testCfg['loginButtonNameRegex']}") - page.get_by_role("button", name=re.compile(testCfg['loginButtonNameRegex'])).click() - - for line in u['nextPageExpect']: - print(f"Running: {line}") - print(f"Page has title: {page.title()}") - exec(line) - finally: - print(f'Saving trace at trace/{i}.zip') - context.tracing.stop(path=f"trace/{i}.zip") - - browser.close() - '') - ) + imports = [ + baseImports + ../modules/blocks/authelia.nix + ../modules/blocks/hardcodedsecret.nix + ../modules/blocks/mitmdump.nix + ../modules/blocks/nginx.nix + ../modules/blocks/postgresql.nix ]; - }; - }; + config = { + # HTTP(s) server port. + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + shb.nginx.accessLog = true; - backup = backupOption: { config, ... }: { - imports = [ - ../modules/blocks/restic.nix - ]; - shb.restic.instances."testinstance" = { - request = backupOption.request; - settings = { - enable = true; - passphrase.result = config.shb.hardcodedsecret.backupPassphrase.result; - repository = { - path = "/opt/repos/A"; - timerConfig = { - OnCalendar = "00:00:00"; - RandomizedDelaySec = "5h"; + networking.hosts = { + "192.168.1.2" = [ + config.test.fqdn + "auth.${config.test.domain}" + ]; + }; + }; + }; + + clientLoginModule = + { config, pkgs, ... }: + let + cfg = config.test.login; + in + { + options.test.login = { + browser = mkOption { + type = enum [ + "firefox" + "chromium" + "webkit" + ]; + default = "firefox"; + }; + usernameFieldLabelRegex = mkOption { + type = str; + default = "[Uu]sername"; + }; + usernameFieldSelector = mkOption { + type = str; + default = "get_by_label(re.compile('${cfg.usernameFieldLabelRegex}'))"; + }; + passwordFieldLabelRegex = mkOption { + type = str; + default = "[Pp]assword"; + }; + passwordFieldSelector = mkOption { + type = str; + default = "get_by_label(re.compile('${cfg.passwordFieldLabelRegex}'))"; + }; + loginButtonNameRegex = mkOption { + type = str; + default = "[Ll]ogin"; + }; + testLoginWith = mkOption { + type = listOf (submodule { + options = { + username = mkOption { + type = nullOr str; + default = null; + }; + password = mkOption { + type = nullOr str; + default = null; + }; + nextPageExpect = mkOption { + type = listOf str; + }; + }; + }); + }; + startUrl = mkOption { + type = str; + default = "http://${config.test.fqdn}"; + }; + beforeHook = mkOption { + type = str; + default = ""; + }; + }; + config = { + networking.hosts = { + "192.168.1.2" = [ + config.test.fqdn + "auth.${config.test.domain}" + ]; + }; + + environment.variables = { + PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers; + }; + + environment.systemPackages = [ + (pkgs.writers.writePython3Bin "login_playwright" + { + libraries = [ pkgs.python3Packages.playwright ]; + flakeIgnore = [ + "F401" + "E501" + ]; + } + ( + let + testCfg = pkgs.writeText "users.json" (builtins.toJSON cfg); + in + '' + import json + import re + import sys + from playwright.sync_api import expect + from playwright.sync_api import sync_playwright + + + browsers = { + "chromium": {'args': ["--headless", "--disable-gpu"], 'channel': 'chromium'}, + "firefox": {'args': ["--reporter", "html"]}, + "webkit": {}, + } + + with open("${testCfg}") as f: + testCfg = json.load(f) + + browser_name = testCfg['browser'] + browser_args = browsers.get(browser_name) + print(f"Running test on {browser_name} {' '.join(browser_args)}") + + with sync_playwright() as p: + browser = getattr(p, browser_name).launch(**browser_args) + + for i, u in enumerate(testCfg["testLoginWith"]): + print(f"Testing for user {u['username']} and password {u['password']}") + + context = browser.new_context(ignore_https_errors=True) + context.set_default_navigation_timeout(2 * 60 * 1000) + context.tracing.start(screenshots=True, snapshots=True, sources=True) + try: + page = context.new_page() + print(f"Going to {testCfg['startUrl']}") + page.goto(testCfg['startUrl']) + + if testCfg.get("beforeHook") is not None: + exec(testCfg.get("beforeHook")) + + if u['username'] is not None: + print(f"Filling field username with {u['username']}") + page.${cfg.usernameFieldSelector}.fill(u['username']) + if u['password'] is not None: + print(f"Filling field password with {u['password']}") + page.${cfg.passwordFieldSelector}.fill(u['password']) + + # Assumes we don't need to login, so skip this. + if u['username'] is not None or u['password'] is not None: + print(f"Clicking button {testCfg['loginButtonNameRegex']}") + page.get_by_role("button", name=re.compile(testCfg['loginButtonNameRegex'])).click() + + for line in u['nextPageExpect']: + print(f"Running: {line}") + print(f"Page has title: {page.title()}") + exec(line) + finally: + print(f'Saving trace at trace/{i}.zip') + context.tracing.stop(path=f"trace/{i}.zip") + + browser.close() + '' + ) + ) + ]; + }; + }; + + backup = + backupOption: + { config, ... }: + { + imports = [ + ../modules/blocks/restic.nix + ]; + shb.restic.instances."testinstance" = { + request = backupOption.request; + settings = { + enable = true; + passphrase.result = config.shb.hardcodedsecret.backupPassphrase.result; + repository = { + path = "/opt/repos/A"; + timerConfig = { + OnCalendar = "00:00:00"; + RandomizedDelaySec = "5h"; + }; }; }; }; - }; - shb.hardcodedsecret.backupPassphrase = { - request = config.shb.restic.instances."testinstance".settings.passphrase.request; - settings.content = "PassPhrase"; - }; - }; - - certs = { config, ... }: { - imports = [ - ../modules/blocks/ssl.nix - ]; - - shb.certs = { - cas.selfsigned.myca = { - name = "My CA"; + shb.hardcodedsecret.backupPassphrase = { + request = config.shb.restic.instances."testinstance".settings.passphrase.request; + settings.content = "PassPhrase"; }; - certs.selfsigned = { - n = { - ca = config.shb.certs.cas.selfsigned.myca; - domain = "*.${config.test.domain}"; - group = "nginx"; + }; + + certs = + { config, ... }: + { + imports = [ + ../modules/blocks/ssl.nix + ]; + + shb.certs = { + cas.selfsigned.myca = { + name = "My CA"; + }; + certs.selfsigned = { + n = { + ca = config.shb.certs.cas.selfsigned.myca; + domain = "*.${config.test.domain}"; + group = "nginx"; + }; + }; + }; + + systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ]; + systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ]; + }; + + ldap = + { config, pkgs, ... }: + { + imports = [ + ../modules/blocks/lldap.nix + ]; + + networking.hosts = { + "127.0.0.1" = [ "ldap.${config.test.domain}" ]; + }; + + shb.hardcodedsecret.ldapUserPassword = { + request = config.shb.lldap.ldapUserPassword.request; + settings.content = "ldapUserPassword"; + }; + shb.hardcodedsecret.jwtSecret = { + request = config.shb.lldap.jwtSecret.request; + settings.content = "jwtSecrets"; + }; + + shb.lldap = { + enable = true; + inherit (config.test) domain; + subdomain = "ldap"; + ldapPort = 3890; + webUIListenPort = 17170; + dcdomain = "dc=example,dc=com"; + ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; + jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; + debug = false; # Enable this if needed, but beware it is _very_ verbose. + + ensureUsers = { + alice = { + email = "alice@example.com"; + groups = [ "user_group" ]; + password.result.path = pkgs.writeText "alicePassword" "AlicePassword"; + }; + bob = { + email = "bob@example.com"; + groups = [ + "user_group" + "admin_group" + ]; + password.result.path = pkgs.writeText "bobPassword" "BobPassword"; + }; + charlie = { + email = "charlie@example.com"; + groups = [ "other_group" ]; + password.result.path = pkgs.writeText "charliePassword" "CharliePassword"; + }; + }; + + ensureGroups = { + user_group = { }; + admin_group = { }; + other_group = { }; }; }; }; - systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ]; - systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ]; - }; + sso = + ssl: + { config, pkgs, ... }: + { + imports = [ + ../modules/blocks/authelia.nix + ]; - ldap = { config, pkgs, ... }: { - imports = [ - ../modules/blocks/lldap.nix - ]; + networking.hosts = { + "127.0.0.1" = [ "${config.shb.authelia.subdomain}.${config.shb.authelia.domain}" ]; + }; - networking.hosts = { - "127.0.0.1" = [ "ldap.${config.test.domain}" ]; - }; + shb.authelia = { + enable = true; + inherit (config.test) domain; + subdomain = "auth"; + ssl = config.shb.certs.certs.selfsigned.n; + debug = true; - shb.hardcodedsecret.ldapUserPassword = { - request = config.shb.lldap.ldapUserPassword.request; - settings.content = "ldapUserPassword"; - }; - shb.hardcodedsecret.jwtSecret = { - request = config.shb.lldap.jwtSecret.request; - settings.content = "jwtSecrets"; - }; + ldapHostname = "127.0.0.1"; + ldapPort = config.shb.lldap.ldapPort; + dcdomain = config.shb.lldap.dcdomain; - shb.lldap = { - enable = true; - inherit (config.test) domain; - subdomain = "ldap"; - ldapPort = 3890; - webUIListenPort = 17170; - dcdomain = "dc=example,dc=com"; - ldapUserPassword.result = config.shb.hardcodedsecret.ldapUserPassword.result; - jwtSecret.result = config.shb.hardcodedsecret.jwtSecret.result; - debug = false; # Enable this if needed, but beware it is _very_ verbose. - - ensureUsers = { - alice = { - email = "alice@example.com"; - groups = [ "user_group" ]; - password.result.path = pkgs.writeText "alicePassword" "AlicePassword"; - }; - bob = { - email = "bob@example.com"; - groups = [ "user_group" "admin_group" ]; - password.result.path = pkgs.writeText "bobPassword" "BobPassword"; - }; - charlie = { - email = "charlie@example.com"; - groups = [ "other_group" ]; - password.result.path = pkgs.writeText "charliePassword" "CharliePassword"; + secrets = { + jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result; + ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result; + sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result; + storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result; + identityProvidersOIDCHMACSecret.result = + config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result; + identityProvidersOIDCIssuerPrivateKey.result = + config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result; }; }; - ensureGroups = { - user_group = {}; - admin_group = {}; - other_group = {}; + shb.hardcodedsecret.autheliaJwtSecret = { + request = config.shb.authelia.secrets.jwtSecret.request; + settings.content = "jwtSecret"; + }; + shb.hardcodedsecret.ldapAdminPassword = { + request = config.shb.authelia.secrets.ldapAdminPassword.request; + settings.content = "ldapUserPassword"; + }; + shb.hardcodedsecret.sessionSecret = { + request = config.shb.authelia.secrets.sessionSecret.request; + settings.content = "sessionSecret"; + }; + shb.hardcodedsecret.storageEncryptionKey = { + request = config.shb.authelia.secrets.storageEncryptionKey.request; + settings.content = "storageEncryptionKey"; + }; + shb.hardcodedsecret.identityProvidersOIDCHMACSecret = { + request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; + settings.content = "identityProvidersOIDCHMACSecret"; + }; + shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = { + request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; + settings.source = + (pkgs.runCommand "gen-private-key" { } '' + mkdir $out + ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096 + '') + + "/private.pem"; }; }; - }; - - sso = ssl: { config, pkgs, ... }: { - imports = [ - ../modules/blocks/authelia.nix - ]; - - networking.hosts = { - "127.0.0.1" = [ "${config.shb.authelia.subdomain}.${config.shb.authelia.domain}" ]; - }; - - shb.authelia = { - enable = true; - inherit (config.test) domain; - subdomain = "auth"; - ssl = config.shb.certs.certs.selfsigned.n; - debug = true; - - ldapHostname = "127.0.0.1"; - ldapPort = config.shb.lldap.ldapPort; - dcdomain = config.shb.lldap.dcdomain; - - secrets = { - jwtSecret.result = config.shb.hardcodedsecret.autheliaJwtSecret.result; - ldapAdminPassword.result = config.shb.hardcodedsecret.ldapAdminPassword.result; - sessionSecret.result = config.shb.hardcodedsecret.sessionSecret.result; - storageEncryptionKey.result = config.shb.hardcodedsecret.storageEncryptionKey.result; - identityProvidersOIDCHMACSecret.result = config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result; - identityProvidersOIDCIssuerPrivateKey.result = config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result; - }; - }; - - shb.hardcodedsecret.autheliaJwtSecret = { - request = config.shb.authelia.secrets.jwtSecret.request; - settings.content = "jwtSecret"; - }; - shb.hardcodedsecret.ldapAdminPassword = { - request = config.shb.authelia.secrets.ldapAdminPassword.request; - settings.content = "ldapUserPassword"; - }; - shb.hardcodedsecret.sessionSecret = { - request = config.shb.authelia.secrets.sessionSecret.request; - settings.content = "sessionSecret"; - }; - shb.hardcodedsecret.storageEncryptionKey = { - request = config.shb.authelia.secrets.storageEncryptionKey.request; - settings.content = "storageEncryptionKey"; - }; - shb.hardcodedsecret.identityProvidersOIDCHMACSecret = { - request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request; - settings.content = "identityProvidersOIDCHMACSecret"; - }; - shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey = { - request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request; - settings.source = (pkgs.runCommand "gen-private-key" {} '' - mkdir $out - ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096 - '') + "/private.pem"; - }; - }; } diff --git a/test/contracts/backup.nix b/test/contracts/backup.nix index 0370290..2bb3d3c 100644 --- a/test/contracts/backup.nix +++ b/test/contracts/backup.nix @@ -1,57 +1,75 @@ { pkgs, ... }: let - contracts = pkgs.callPackage ../../modules/contracts {}; + contracts = pkgs.callPackage ../../modules/contracts { }; in { restic_root = contracts.test.backup { name = "restic_root"; username = "root"; - providerRoot = [ "shb" "restic" "instances" "mytest" ]; + providerRoot = [ + "shb" + "restic" + "instances" + "mytest" + ]; modules = [ ../../modules/blocks/restic.nix ../../modules/blocks/hardcodedsecret.nix ]; - settings = { repository, config, ... }: { - enable = true; - passphrase.result = config.shb.hardcodedsecret.passphrase.result; - repository = { - path = repository; - timerConfig = { - OnCalendar = "00:00:00"; + settings = + { repository, config, ... }: + { + enable = true; + passphrase.result = config.shb.hardcodedsecret.passphrase.result; + repository = { + path = repository; + timerConfig = { + OnCalendar = "00:00:00"; + }; }; }; - }; - extraConfig = { username, config, ... }: { - shb.hardcodedsecret.passphrase = { - request = config.shb.restic.instances."mytest".settings.passphrase.request; - settings.content = "passphrase"; + extraConfig = + { username, config, ... }: + { + shb.hardcodedsecret.passphrase = { + request = config.shb.restic.instances."mytest".settings.passphrase.request; + settings.content = "passphrase"; + }; }; - }; }; restic_nonroot = contracts.test.backup { name = "restic_nonroot"; username = "me"; - providerRoot = [ "shb" "restic" "instances" "mytest" ]; + providerRoot = [ + "shb" + "restic" + "instances" + "mytest" + ]; modules = [ ../../modules/blocks/restic.nix ../../modules/blocks/hardcodedsecret.nix ]; - settings = { repository, config, ... }: { - enable = true; - passphrase.result = config.shb.hardcodedsecret.passphrase.result; - repository = { - path = repository; - timerConfig = { - OnCalendar = "00:00:00"; + settings = + { repository, config, ... }: + { + enable = true; + passphrase.result = config.shb.hardcodedsecret.passphrase.result; + repository = { + path = repository; + timerConfig = { + OnCalendar = "00:00:00"; + }; }; }; - }; - extraConfig = { username, config, ... }: { - shb.hardcodedsecret.passphrase = { - request = config.shb.restic.instances."mytest".settings.passphrase.request; - settings.content = "passphrase"; + extraConfig = + { username, config, ... }: + { + shb.hardcodedsecret.passphrase = { + request = config.shb.restic.instances."mytest".settings.passphrase.request; + settings.content = "passphrase"; + }; }; - }; }; } diff --git a/test/contracts/databasebackup.nix b/test/contracts/databasebackup.nix index aba1575..c5b490b 100644 --- a/test/contracts/databasebackup.nix +++ b/test/contracts/databasebackup.nix @@ -1,38 +1,51 @@ { pkgs, ... }: let - contracts = pkgs.callPackage ../../modules/contracts {}; + contracts = pkgs.callPackage ../../modules/contracts { }; in { restic_postgres = contracts.test.databasebackup { name = "restic_postgres"; - requesterRoot = [ "shb" "postgresql" "databasebackup" ]; - providerRoot = [ "shb" "restic" "databases" "postgresql" ]; + requesterRoot = [ + "shb" + "postgresql" + "databasebackup" + ]; + providerRoot = [ + "shb" + "restic" + "databases" + "postgresql" + ]; modules = [ ../../modules/blocks/postgresql.nix ../../modules/blocks/restic.nix ../../modules/blocks/hardcodedsecret.nix ]; - settings = { repository, config, ... }: { - enable = true; - passphrase.result = config.shb.hardcodedsecret.passphrase.result; - repository = { - path = repository; - timerConfig = { - OnCalendar = "00:00:00"; + settings = + { repository, config, ... }: + { + enable = true; + passphrase.result = config.shb.hardcodedsecret.passphrase.result; + repository = { + path = repository; + timerConfig = { + OnCalendar = "00:00:00"; + }; }; }; - }; - extraConfig = { config, database, ... }: { - shb.postgresql.ensures = [ - { - inherit database; - username = database; - } - ]; - shb.hardcodedsecret.passphrase = { - request = config.shb.restic.databases.postgresql.settings.passphrase.request; - settings.content = "passphrase"; + extraConfig = + { config, database, ... }: + { + shb.postgresql.ensures = [ + { + inherit database; + username = database; + } + ]; + shb.hardcodedsecret.passphrase = { + request = config.shb.restic.databases.postgresql.settings.passphrase.request; + settings.content = "passphrase"; + }; }; - }; }; } diff --git a/test/contracts/secret.nix b/test/contracts/secret.nix index c594027..d482edd 100644 --- a/test/contracts/secret.nix +++ b/test/contracts/secret.nix @@ -1,12 +1,15 @@ { pkgs, ... }: let - contracts = pkgs.callPackage ../../modules/contracts {}; + contracts = pkgs.callPackage ../../modules/contracts { }; in { hardcoded_root_root = contracts.test.secret { name = "hardcoded"; modules = [ ../../modules/blocks/hardcodedsecret.nix ]; - configRoot = [ "shb" "hardcodedsecret" ]; + configRoot = [ + "shb" + "hardcodedsecret" + ]; settingsCfg = secret: { content = secret; }; @@ -15,7 +18,10 @@ in hardcoded_user_group = contracts.test.secret { name = "hardcoded"; modules = [ ../../modules/blocks/hardcodedsecret.nix ]; - configRoot = [ "shb" "hardcodedsecret" ]; + configRoot = [ + "shb" + "hardcodedsecret" + ]; settingsCfg = secret: { content = secret; }; diff --git a/test/modules/davfs.nix b/test/modules/davfs.nix index 12709b3..bc1bae5 100644 --- a/test/modules/davfs.nix +++ b/test/modules/davfs.nix @@ -1,26 +1,31 @@ { pkgs, lib, ... }: let - anyOpt = default: lib.mkOption { - type = lib.types.anything; - inherit default; - }; + anyOpt = + default: + lib.mkOption { + type = lib.types.anything; + inherit default; + }; - testConfig = m: + testConfig = + m: let - cfg = (lib.evalModules { - specialArgs = { inherit pkgs; }; - modules = [ - { - options = { - systemd = anyOpt {}; - services = anyOpt {}; - }; - } - ../../modules/blocks/davfs.nix - m - ]; - }).config; - in { + cfg = + (lib.evalModules { + specialArgs = { inherit pkgs; }; + modules = [ + { + options = { + systemd = anyOpt { }; + services = anyOpt { }; + }; + } + ../../modules/blocks/davfs.nix + m + ]; + }).config; + in + { inherit (cfg) systemd services; }; in @@ -28,8 +33,8 @@ in testDavfsNoOptions = { expected = { services.davfs2.enable = false; - systemd.mounts = []; + systemd.mounts = [ ]; }; - expr = testConfig {}; + expr = testConfig { }; }; } diff --git a/test/modules/lib.nix b/test/modules/lib.nix index 717bbf3..0627819 100644 --- a/test/modules/lib.nix +++ b/test/modules/lib.nix @@ -17,11 +17,12 @@ in c = "%SECRET_${root}C%"; }; in - (item "") // { - nestedAttr = item "NESTEDATTR_"; - nestedList = [ (item "NESTEDLIST_0_") ]; - doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ]; - }; + (item "") + // { + nestedAttr = item "NESTEDATTR_"; + nestedList = [ (item "NESTEDLIST_0_") ]; + doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ]; + }; expr = let item = { @@ -33,13 +34,14 @@ in c.other = "other"; }; in - lib.shb.withReplacements ( - item // { - nestedAttr = item; - nestedList = [ item ]; - doubleNestedList = [ { n = item; } ]; - } - ); + lib.shb.withReplacements ( + item + // { + nestedAttr = item; + nestedList = [ item ]; + doubleNestedList = [ { n = item; } ]; + } + ); }; testLibWithReplacementsRootList = { @@ -51,12 +53,12 @@ in c = "%SECRET_${root}C%"; }; in - [ - (item "0_") - (item "1_") - [ (item "2_0_") ] - [ { n = (item "3_0_N_"); } ] - ]; + [ + (item "0_") + (item "1_") + [ (item "2_0_") ] + [ { n = (item "3_0_N_"); } ] + ]; expr = let item = { @@ -68,12 +70,12 @@ in c.other = "other"; }; in - lib.shb.withReplacements [ - item - item - [ item ] - [ { n = item; } ] - ]; + lib.shb.withReplacements [ + item + item + [ item ] + [ { n = item; } ] + ]; }; testLibGetReplacements = { @@ -84,10 +86,10 @@ in (nameValuePair "%SECRET_${root}C%" "prefix-$(cat /path/C)-suffix") ]; in - (secrets "") ++ - (secrets "DOUBLENESTEDLIST_0_N_") ++ - (secrets "NESTEDATTR_") ++ - (secrets "NESTEDLIST_0_"); + (secrets "") + ++ (secrets "DOUBLENESTEDLIST_0_N_") + ++ (secrets "NESTEDATTR_") + ++ (secrets "NESTEDLIST_0_"); expr = let item = { @@ -99,13 +101,16 @@ in c.other = "other"; }; in - map lib.shb.genReplacement (lib.shb.getReplacements ( - item // { + map lib.shb.genReplacement ( + lib.shb.getReplacements ( + item + // { nestedAttr = item; nestedList = [ item ]; doubleNestedList = [ { n = item; } ]; } - )); + ) + ); }; testParseXML = { @@ -119,10 +124,10 @@ in }; expr = lib.shb.parseXML '' - - 1 - 1 - + + 1 + 1 + ''; }; } diff --git a/test/services/arr.nix b/test/services/arr.nix index 71e5bdc..acd4889 100644 --- a/test/services/arr.nix +++ b/test/services/arr.nix @@ -4,163 +4,206 @@ let loginUrl = "/UI/Login"; # TODO: Test login - commonTestScript = appname: cfgPathFn: lib.shb.mkScripts { - hasSSL = { node, ... }: !(isNull node.config.shb.arr.${appname}.ssl); - waitForServices = { ... }: [ - "${appname}.service" - "nginx.service" - ]; - waitForPorts = { node, ... }: [ - node.config.shb.arr.${appname}.settings.Port - ]; - extraScript = { node, fqdn, proto_fqdn, ... }: let - shbapp = node.config.shb.arr.${appname}; - cfgPath = cfgPathFn shbapp; - apiKey = if (shbapp.settings ? ApiKey) then "01234567890123456789" else null; - in '' - # These curl requests still return a 200 even with sso redirect. - with subtest("health"): - response = curl(client, """{"code":%{response_code}}""", "${fqdn}${healthUrl}") - print("response =", response) + commonTestScript = + appname: cfgPathFn: + lib.shb.mkScripts { + hasSSL = { node, ... }: !(isNull node.config.shb.arr.${appname}.ssl); + waitForServices = + { ... }: + [ + "${appname}.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + node.config.shb.arr.${appname}.settings.Port + ]; + extraScript = + { + node, + fqdn, + proto_fqdn, + ... + }: + let + shbapp = node.config.shb.arr.${appname}; + cfgPath = cfgPathFn shbapp; + apiKey = if (shbapp.settings ? ApiKey) then "01234567890123456789" else null; + in + '' + # These curl requests still return a 200 even with sso redirect. + with subtest("health"): + response = curl(client, """{"code":%{response_code}}""", "${fqdn}${healthUrl}") + print("response =", response) - if response['code'] != 200: - raise Exception(f"Code is {response['code']}") + if response['code'] != 200: + raise Exception(f"Code is {response['code']}") - with subtest("login"): - response = curl(client, """{"code":%{response_code}}""", "${fqdn}${loginUrl}") + with subtest("login"): + response = curl(client, """{"code":%{response_code}}""", "${fqdn}${loginUrl}") - if response['code'] != 200: - raise Exception(f"Code is {response['code']}") - '' + lib.optionalString (apiKey != null) '' + if response['code'] != 200: + raise Exception(f"Code is {response['code']}") + '' + + lib.optionalString (apiKey != null) '' - with subtest("apikey"): - config = server.succeed("cat ${cfgPath}") - if "${apiKey}" not in config: - raise Exception(f"Unexpected API Key. Want '${apiKey}', got '{config}'") - ''; - }; - - basic = appname: { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/services/arr.nix - ]; - - test = { - subdomain = appname; + with subtest("apikey"): + config = server.succeed("cat ${cfgPath}") + if "${apiKey}" not in config: + raise Exception(f"Unexpected API Key. Want '${apiKey}', got '{config}'") + ''; }; - shb.arr.${appname} = { - enable = true; - inherit (config.test) subdomain domain; - - settings.ApiKey.source = pkgs.writeText "APIKey" "01234567890123456789"; # Needs to be >=20 characters. - }; - }; - - clientLogin = appname: { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - - test = { - subdomain = appname; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - usernameFieldLabelRegex = "[Uu]sername"; - passwordFieldLabelRegex = "^ *[Pp]assword"; - loginButtonNameRegex = "[Ll]og [Ii]n"; - testLoginWith = [ - { nextPageExpect = [ - "expect(page).to_have_title(re.compile('${appname}', re.IGNORECASE))" - ]; } - ]; - }; - }; - - basicTest = appname: cfgPathFn: lib.shb.runNixOSTest { - name = "arr_${appname}_basic"; - - nodes.client = { + basic = + appname: + { config, ... }: + { imports = [ - (clientLogin appname) + lib.shb.baseModule + ../../modules/services/arr.nix ]; + + test = { + subdomain = appname; + }; + + shb.arr.${appname} = { + enable = true; + inherit (config.test) subdomain domain; + + settings.ApiKey.source = pkgs.writeText "APIKey" "01234567890123456789"; # Needs to be >=20 characters. + }; }; - nodes.server = { + + clientLogin = + appname: + { config, ... }: + { imports = [ - (basic appname) + lib.shb.baseModule + lib.shb.clientLoginModule ]; + + test = { + subdomain = appname; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + usernameFieldLabelRegex = "[Uu]sername"; + passwordFieldLabelRegex = "^ *[Pp]assword"; + loginButtonNameRegex = "[Ll]og [Ii]n"; + testLoginWith = [ + { + nextPageExpect = [ + "expect(page).to_have_title(re.compile('${appname}', re.IGNORECASE))" + ]; + } + ]; + }; }; - testScript = (commonTestScript appname cfgPathFn).access; - }; + basicTest = + appname: cfgPathFn: + lib.shb.runNixOSTest { + name = "arr_${appname}_basic"; - backupTest = appname: cfgPathFn: lib.shb.runNixOSTest { - name = "arr_${appname}_backup"; + nodes.client = { + imports = [ + (clientLogin appname) + ]; + }; + nodes.server = { + imports = [ + (basic appname) + ]; + }; - nodes.server = { config, ... }: { - imports = [ - (basic appname) - (lib.shb.backup config.shb.arr.${appname}.backup) - ]; + testScript = (commonTestScript appname cfgPathFn).access; }; - nodes.client = {}; + backupTest = + appname: cfgPathFn: + lib.shb.runNixOSTest { + name = "arr_${appname}_backup"; - testScript = (commonTestScript appname cfgPathFn).backup; - }; + nodes.server = + { config, ... }: + { + imports = [ + (basic appname) + (lib.shb.backup config.shb.arr.${appname}.backup) + ]; + }; - https = appname: { config, ...}: { - shb.arr.${appname} = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; + nodes.client = { }; - httpsTest = appname: cfgPathFn: lib.shb.runNixOSTest { - name = "arr_${appname}_https"; - - nodes.server = { config, pkgs, ... }: { - imports = [ - (basic appname) - lib.shb.certs - (https appname) - ]; + testScript = (commonTestScript appname cfgPathFn).backup; }; - nodes.client = {}; - - testScript = (commonTestScript appname cfgPathFn).access; - }; - - sso = appname: { config, ...}: { - shb.arr.${appname} = { - authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - }; - }; - - ssoTest = appname: cfgPathFn: lib.shb.runNixOSTest { - name = "arr_${appname}_sso"; - - nodes.server = { config, pkgs, ... }: { - imports = [ - (basic appname) - lib.shb.certs - (https appname) - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - (sso appname) - ]; + https = + appname: + { config, ... }: + { + shb.arr.${appname} = { + ssl = config.shb.certs.certs.selfsigned.n; + }; }; - nodes.client = {}; + httpsTest = + appname: cfgPathFn: + lib.shb.runNixOSTest { + name = "arr_${appname}_https"; - testScript = (commonTestScript appname cfgPathFn).access.override { - redirectSSO = true; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + (basic appname) + lib.shb.certs + (https appname) + ]; + }; + + nodes.client = { }; + + testScript = (commonTestScript appname cfgPathFn).access; + }; + + sso = + appname: + { config, ... }: + { + shb.arr.${appname} = { + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; + }; + + ssoTest = + appname: cfgPathFn: + lib.shb.runNixOSTest { + name = "arr_${appname}_sso"; + + nodes.server = + { config, pkgs, ... }: + { + imports = [ + (basic appname) + lib.shb.certs + (https appname) + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + (sso appname) + ]; + }; + + nodes.client = { }; + + testScript = (commonTestScript appname cfgPathFn).access.override { + redirectSSO = true; + }; }; - }; radarrCfgFn = cfg: "${cfg.dataDir}/config.xml"; sonarrCfgFn = cfg: "${cfg.dataDir}/config.xml"; @@ -170,33 +213,33 @@ let jackettCfgFn = cfg: "${cfg.dataDir}/ServerConfig.json"; in { - radarr_basic = basicTest "radarr" radarrCfgFn; + radarr_basic = basicTest "radarr" radarrCfgFn; radarr_backup = backupTest "radarr" radarrCfgFn; - radarr_https = httpsTest "radarr" radarrCfgFn; - radarr_sso = ssoTest "radarr" radarrCfgFn; + radarr_https = httpsTest "radarr" radarrCfgFn; + radarr_sso = ssoTest "radarr" radarrCfgFn; - sonarr_basic = basicTest "sonarr" sonarrCfgFn; + sonarr_basic = basicTest "sonarr" sonarrCfgFn; sonarr_backup = backupTest "sonarr" sonarrCfgFn; - sonarr_https = httpsTest "sonarr" sonarrCfgFn; - sonarr_sso = ssoTest "sonarr" sonarrCfgFn; + sonarr_https = httpsTest "sonarr" sonarrCfgFn; + sonarr_sso = ssoTest "sonarr" sonarrCfgFn; - bazarr_basic = basicTest "bazarr" bazarrCfgFn; + bazarr_basic = basicTest "bazarr" bazarrCfgFn; bazarr_backup = backupTest "bazarr" bazarrCfgFn; - bazarr_https = httpsTest "bazarr" bazarrCfgFn; - bazarr_sso = ssoTest "bazarr" bazarrCfgFn; + bazarr_https = httpsTest "bazarr" bazarrCfgFn; + bazarr_sso = ssoTest "bazarr" bazarrCfgFn; - readarr_basic = basicTest "readarr" readarrCfgFn; + readarr_basic = basicTest "readarr" readarrCfgFn; readarr_backup = backupTest "readarr" readarrCfgFn; - readarr_https = httpsTest "readarr" readarrCfgFn; - readarr_sso = ssoTest "readarr" readarrCfgFn; + readarr_https = httpsTest "readarr" readarrCfgFn; + readarr_sso = ssoTest "readarr" readarrCfgFn; - lidarr_basic = basicTest "lidarr" lidarrCfgFn; + lidarr_basic = basicTest "lidarr" lidarrCfgFn; lidarr_backup = backupTest "lidarr" lidarrCfgFn; - lidarr_https = httpsTest "lidarr" lidarrCfgFn; - lidarr_sso = ssoTest "lidarr" lidarrCfgFn; + lidarr_https = httpsTest "lidarr" lidarrCfgFn; + lidarr_sso = ssoTest "lidarr" lidarrCfgFn; - jackett_basic = basicTest "jackett" jackettCfgFn; + jackett_basic = basicTest "jackett" jackettCfgFn; jackett_backup = backupTest "jackett" jackettCfgFn; - jackett_https = httpsTest "jackett" jackettCfgFn; - jackett_sso = ssoTest "jackett" jackettCfgFn; + jackett_https = httpsTest "jackett" jackettCfgFn; + jackett_sso = ssoTest "jackett" jackettCfgFn; } diff --git a/test/services/audiobookshelf.nix b/test/services/audiobookshelf.nix index 6a465fd..0edb4ec 100644 --- a/test/services/audiobookshelf.nix +++ b/test/services/audiobookshelf.nix @@ -2,89 +2,106 @@ let commonTestScript = lib.shb.accessScript { hasSSL = { node, ... }: !(isNull node.config.shb.audiobookshelf.ssl); - waitForServices = { ... }: [ - "audiobookshelf.service" - "nginx.service" - ]; - waitForPorts = { node, ... }: [ - node.config.shb.audiobookshelf.webPort - ]; + waitForServices = + { ... }: + [ + "audiobookshelf.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + node.config.shb.audiobookshelf.webPort + ]; # TODO: Test login # extraScript = { ... }: '' # ''; }; - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/services/audiobookshelf.nix - ]; - - test = { - subdomain = "a"; - }; - shb.audiobookshelf = { - enable = true; - inherit (config.test) subdomain domain; - }; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - virtualisation.memorySize = 4096; - - test = { - subdomain = "a"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - usernameFieldLabelRegex = "[Uu]sername"; - passwordFieldLabelRegex = "[Pp]assword"; - loginButtonNameRegex = "[Ll]og [Ii]n"; - testLoginWith = [ - # Failure is after so we're not throttled too much. - { username = "root"; password = "rootpw"; nextPageExpect = [ - "expect(page.get_by_text('Wrong username or password')).to_be_visible()" - ]; } - # { username = adminUser; password = adminPass; nextPageExpect = [ - # "expect(page.get_by_text('Wrong username or password')).not_to_be_visible()" - # "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" - # "expect(page).to_have_title(re.compile('Dashboard'))" - # ]; } + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/services/audiobookshelf.nix ]; - }; - }; - https = { config, ... }: { - shb.audiobookshelf = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; - - sso = { config, ... }: { - shb.audiobookshelf = { - sso = { + test = { + subdomain = "a"; + }; + shb.audiobookshelf = { enable = true; - endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - sharedSecret.result = config.shb.hardcodedsecret.audiobookshelfSSOPassword.result; - sharedSecretForAuthelia.result = config.shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia.result; + inherit (config.test) subdomain domain; }; }; - shb.hardcodedsecret.audiobookshelfSSOPassword = { - request = config.shb.audiobookshelf.sso.sharedSecret.request; - settings.content = "ssoPassword"; + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "a"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + usernameFieldLabelRegex = "[Uu]sername"; + passwordFieldLabelRegex = "[Pp]assword"; + loginButtonNameRegex = "[Ll]og [Ii]n"; + testLoginWith = [ + # Failure is after so we're not throttled too much. + { + username = "root"; + password = "rootpw"; + nextPageExpect = [ + "expect(page.get_by_text('Wrong username or password')).to_be_visible()" + ]; + } + # { username = adminUser; password = adminPass; nextPageExpect = [ + # "expect(page.get_by_text('Wrong username or password')).not_to_be_visible()" + # "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" + # "expect(page).to_have_title(re.compile('Dashboard'))" + # ]; } + ]; + }; }; - shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia = { - request = config.shb.audiobookshelf.sso.sharedSecretForAuthelia.request; - settings.content = "ssoPassword"; + https = + { config, ... }: + { + shb.audiobookshelf = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + sso = + { config, ... }: + { + shb.audiobookshelf = { + sso = { + enable = true; + endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + sharedSecret.result = config.shb.hardcodedsecret.audiobookshelfSSOPassword.result; + sharedSecretForAuthelia.result = + config.shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia.result; + }; + }; + + shb.hardcodedsecret.audiobookshelfSSOPassword = { + request = config.shb.audiobookshelf.sso.sharedSecret.request; + settings.content = "ssoPassword"; + }; + + shb.hardcodedsecret.audiobookshelfSSOPasswordAuthelia = { + request = config.shb.audiobookshelf.sso.sharedSecretForAuthelia.request; + settings.content = "ssoPassword"; + }; }; - }; in { basic = lib.shb.runNixOSTest { @@ -116,7 +133,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript; }; @@ -124,18 +141,20 @@ in sso = lib.shb.runNixOSTest { name = "audiobookshelf-sso"; - nodes.server = { config, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript; }; diff --git a/test/services/deluge.nix b/test/services/deluge.nix index 3f506fb..19969b2 100644 --- a/test/services/deluge.nix +++ b/test/services/deluge.nix @@ -2,146 +2,169 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.deluge.ssl); - waitForServices = { ... }: [ - "nginx.service" - "deluged.service" - "delugeweb.service" - ]; - waitForPorts = { node, ... }: [ - node.config.shb.deluge.daemonPort - node.config.shb.deluge.webPort - ]; - extraScript = { node, proto_fqdn, ... }: '' - print(${node.name}.succeed('journalctl -n100 -u deluged')) - print(${node.name}.succeed('systemctl status deluged')) - print(${node.name}.succeed('systemctl status delugeweb')) - - with subtest("web connect"): - print(server.succeed("cat ${node.config.services.deluge.dataDir}/.config/deluge/auth")) - - response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """ - -H "Content-Type: application/json" - -H "Accept: application/json" - """), data = unline_with(" ", """ - {"method": "auth.login", "params": ["deluge"], "id": 1} - """)) - print(response) - if response['error']: - raise Exception(f"error is {response['error']}") - if not response['result']: - raise Exception(f"response is {response}") - - response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """ - -H "Content-Type: application/json" - -H "Accept: application/json" - """), data = unline_with(" ", """ - {"method": "web.get_hosts", "params": [], "id": 1} - """)) - print(response) - if response['error']: - raise Exception(f"error is {response['error']}") - - hostID = response['result'][0][0] - response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """ - -H "Content-Type: application/json" - -H "Accept: application/json" - """), data = unline_with(" ", f""" - {{"method": "web.connect", "params": ["{hostID}"], "id": 1}} - """)) - print(response) - if response['error']: - raise Exception(f"result had an error {response['error']}") - ''; - }; - - prometheusTestScript = { nodes, ... }: - '' - server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.deluge.port}) - with subtest("prometheus"): - response = server.succeed( - "curl -sSf " - + " http://localhost:${toString nodes.server.services.prometheus.exporters.deluge.port}/metrics" - ) - print(response) - ''; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/blocks/hardcodedsecret.nix - ../../modules/services/deluge.nix - ]; - - test = { - subdomain = "d"; - }; - - shb.deluge = { - enable = true; - inherit (config.test) domain subdomain; - - settings = { - downloadLocation = "/var/lib/deluge"; - }; - - extraUsers = { - user.password.source = pkgs.writeText "userpw" "userpw"; - }; - - localclientPassword.result = config.shb.hardcodedsecret."localclientpassword".result; - }; - shb.hardcodedsecret."localclientpassword" = { - request = config.shb.deluge.localclientPassword.request; - settings.content = "localpw"; - }; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - test = { - subdomain = "d"; - }; - - test.login = { - passwordFieldLabelRegex = "Password"; - loginButtonNameRegex = "Login"; - testLoginWith = [ - { password = "deluge"; nextPageExpect = [ - "expect(page.get_by_role('button', name='Login')).not_to_be_visible()" - "expect(page.get_by_text('Login Failed')).not_to_be_visible()" - ]; } - { password = "other"; nextPageExpect = [ - "expect(page.get_by_role('button', name='Login')).to_be_visible()" - "expect(page.get_by_text('Login Failed')).to_be_visible()" - ]; } + waitForServices = + { ... }: + [ + "nginx.service" + "deluged.service" + "delugeweb.service" ]; - }; + waitForPorts = + { node, ... }: + [ + node.config.shb.deluge.daemonPort + node.config.shb.deluge.webPort + ]; + extraScript = + { node, proto_fqdn, ... }: + '' + print(${node.name}.succeed('journalctl -n100 -u deluged')) + print(${node.name}.succeed('systemctl status deluged')) + print(${node.name}.succeed('systemctl status delugeweb')) + + with subtest("web connect"): + print(server.succeed("cat ${node.config.services.deluge.dataDir}/.config/deluge/auth")) + + response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """ + -H "Content-Type: application/json" + -H "Accept: application/json" + """), data = unline_with(" ", """ + {"method": "auth.login", "params": ["deluge"], "id": 1} + """)) + print(response) + if response['error']: + raise Exception(f"error is {response['error']}") + if not response['result']: + raise Exception(f"response is {response}") + + response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """ + -H "Content-Type: application/json" + -H "Accept: application/json" + """), data = unline_with(" ", """ + {"method": "web.get_hosts", "params": [], "id": 1} + """)) + print(response) + if response['error']: + raise Exception(f"error is {response['error']}") + + hostID = response['result'][0][0] + response = curl(client, "", "${proto_fqdn}/json", extra = unline_with(" ", """ + -H "Content-Type: application/json" + -H "Accept: application/json" + """), data = unline_with(" ", f""" + {{"method": "web.connect", "params": ["{hostID}"], "id": 1}} + """)) + print(response) + if response['error']: + raise Exception(f"result had an error {response['error']}") + ''; }; - prometheus = { config, ... }: { - shb.deluge = { - prometheusScraperPassword.result = config.shb.hardcodedsecret."scraper".result; - }; - shb.hardcodedsecret."scraper" = { - request = config.shb.deluge.prometheusScraperPassword.request; - settings.content = "scraperpw"; - }; - }; + prometheusTestScript = + { nodes, ... }: + '' + server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.deluge.port}) + with subtest("prometheus"): + response = server.succeed( + "curl -sSf " + + " http://localhost:${toString nodes.server.services.prometheus.exporters.deluge.port}/metrics" + ) + print(response) + ''; - https = { config, ...}: { - shb.deluge = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/deluge.nix + ]; - sso = { config, ... }: { - shb.deluge = { - authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + test = { + subdomain = "d"; + }; + + shb.deluge = { + enable = true; + inherit (config.test) domain subdomain; + + settings = { + downloadLocation = "/var/lib/deluge"; + }; + + extraUsers = { + user.password.source = pkgs.writeText "userpw" "userpw"; + }; + + localclientPassword.result = config.shb.hardcodedsecret."localclientpassword".result; + }; + shb.hardcodedsecret."localclientpassword" = { + request = config.shb.deluge.localclientPassword.request; + settings.content = "localpw"; + }; + }; + + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + test = { + subdomain = "d"; + }; + + test.login = { + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "Login"; + testLoginWith = [ + { + password = "deluge"; + nextPageExpect = [ + "expect(page.get_by_role('button', name='Login')).not_to_be_visible()" + "expect(page.get_by_text('Login Failed')).not_to_be_visible()" + ]; + } + { + password = "other"; + nextPageExpect = [ + "expect(page.get_by_role('button', name='Login')).to_be_visible()" + "expect(page.get_by_text('Login Failed')).to_be_visible()" + ]; + } + ]; + }; + }; + + prometheus = + { config, ... }: + { + shb.deluge = { + prometheusScraperPassword.result = config.shb.hardcodedsecret."scraper".result; + }; + shb.hardcodedsecret."scraper" = { + request = config.shb.deluge.prometheusScraperPassword.request; + settings.content = "scraperpw"; + }; + }; + + https = + { config, ... }: + { + shb.deluge = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + sso = + { config, ... }: + { + shb.deluge = { + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; }; - }; in { basic = lib.shb.runNixOSTest { @@ -164,14 +187,16 @@ in backup = lib.shb.runNixOSTest { name = "deluge_backup"; - nodes.server = { config, ... }: { - imports = [ - basic - (lib.shb.backup config.shb.deluge.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (lib.shb.backup config.shb.deluge.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -187,27 +212,29 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; sso = lib.shb.runNixOSTest { name = "deluge_sso"; - - nodes.server = { config, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ]; - }; - - nodes.client = {}; - + + nodes.server = + { config, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; + + nodes.client = { }; + testScript = commonTestScript.access.override { redirectSSO = true; }; @@ -225,10 +252,8 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; - testScript = inputs: - (commonTestScript.access inputs) - + (prometheusTestScript inputs); + testScript = inputs: (commonTestScript.access inputs) + (prometheusTestScript inputs); }; } diff --git a/test/services/forgejo.nix b/test/services/forgejo.nix index 8b4d6ad..cbfa676 100644 --- a/test/services/forgejo.nix +++ b/test/services/forgejo.nix @@ -4,150 +4,182 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.forgejo.ssl); - waitForServices = { ... }: [ - "forgejo.service" - "nginx.service" - ]; - waitForUnixSocket = { node, ... }: [ - node.config.services.forgejo.settings.server.HTTP_ADDR - ]; - extraScript = { node, ... }: '' - server.wait_for_unit("gitea-runner-local.service", timeout=10) - server.succeed("journalctl -o cat -u gitea-runner-local.service | grep -q 'Runner registered successfully'") - ''; - }; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/blocks/hardcodedsecret.nix - ../../modules/services/forgejo.nix - ]; - - test = { - subdomain = "f"; - }; - - shb.forgejo = { - enable = true; - inherit (config.test) subdomain domain; - - users = { - "theadmin" = { - isAdmin = true; - email = "theadmin@example.com"; - password.result = config.shb.hardcodedsecret.forgejoAdminPassword.result; - }; - "theuser" = { - email = "theuser@example.com"; - password.result = config.shb.hardcodedsecret.forgejoUserPassword.result; - }; - }; - databasePassword.result = config.shb.hardcodedsecret.forgejoDatabasePassword.result; - }; - - # Needed for gitea-runner-local to be able to ping forgejo. - networking.hosts = { - "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; - }; - - shb.hardcodedsecret.forgejoAdminPassword = { - request = config.shb.forgejo.users."theadmin".password.request; - settings.content = adminPassword; - }; - - shb.hardcodedsecret.forgejoUserPassword = { - request = config.shb.forgejo.users."theuser".password.request; - settings.content = "userPassword"; - }; - - shb.hardcodedsecret.forgejoDatabasePassword = { - request = config.shb.forgejo.databasePassword.request; - settings.content = "databasePassword"; - }; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - test = { - subdomain = "f"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}/user/login"; - usernameFieldLabelRegex = "Username or email address"; - passwordFieldLabelRegex = "Password"; - loginButtonNameRegex = "[sS]ign [iI]n"; - testLoginWith = [ - { username = "theadmin"; password = adminPassword + "oops"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" - ]; } - { username = "theadmin"; password = adminPassword; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page).to_have_title(re.compile('Dashboard'))" - ]; } - { username = "theuser"; password = "userPasswordOops"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" - ]; } - { username = "theuser"; password = "userPassword"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page).to_have_title(re.compile('Dashboard'))" - ]; } + waitForServices = + { ... }: + [ + "forgejo.service" + "nginx.service" ]; - }; + waitForUnixSocket = + { node, ... }: + [ + node.config.services.forgejo.settings.server.HTTP_ADDR + ]; + extraScript = + { node, ... }: + '' + server.wait_for_unit("gitea-runner-local.service", timeout=10) + server.succeed("journalctl -o cat -u gitea-runner-local.service | grep -q 'Runner registered successfully'") + ''; }; - https = { config, ... }: { - shb.forgejo = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/forgejo.nix + ]; - ldap = { config, ... }: { - shb.forgejo = { - ldap = { + test = { + subdomain = "f"; + }; + + shb.forgejo = { enable = true; - host = "127.0.0.1"; - port = config.shb.lldap.ldapPort; - dcdomain = config.shb.lldap.dcdomain; - adminPassword.result = config.shb.hardcodedsecret.forgejoLdapUserPassword.result; - waitForSystemdServices = [ "lldap.service" ]; + inherit (config.test) subdomain domain; - userGroup = "user_group"; + users = { + "theadmin" = { + isAdmin = true; + email = "theadmin@example.com"; + password.result = config.shb.hardcodedsecret.forgejoAdminPassword.result; + }; + "theuser" = { + email = "theuser@example.com"; + password.result = config.shb.hardcodedsecret.forgejoUserPassword.result; + }; + }; + databasePassword.result = config.shb.hardcodedsecret.forgejoDatabasePassword.result; + }; + + # Needed for gitea-runner-local to be able to ping forgejo. + networking.hosts = { + "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; + }; + + shb.hardcodedsecret.forgejoAdminPassword = { + request = config.shb.forgejo.users."theadmin".password.request; + settings.content = adminPassword; + }; + + shb.hardcodedsecret.forgejoUserPassword = { + request = config.shb.forgejo.users."theuser".password.request; + settings.content = "userPassword"; + }; + + shb.hardcodedsecret.forgejoDatabasePassword = { + request = config.shb.forgejo.databasePassword.request; + settings.content = "databasePassword"; }; }; - shb.hardcodedsecret.forgejoLdapUserPassword = { - request = config.shb.forgejo.ldap.adminPassword.request; - settings.content = "ldapUserPassword"; - }; - }; + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + test = { + subdomain = "f"; + }; - sso = { config, ... }: { - shb.forgejo = { - sso = { - enable = true; - endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - sharedSecret.result = config.shb.hardcodedsecret.forgejoSSOPassword.result; - sharedSecretForAuthelia.result = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.result; + test.login = { + startUrl = "http://${config.test.fqdn}/user/login"; + usernameFieldLabelRegex = "Username or email address"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "theadmin"; + password = adminPassword + "oops"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" + ]; + } + { + username = "theadmin"; + password = adminPassword; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; + } + { + username = "theuser"; + password = "userPasswordOops"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" + ]; + } + { + username = "theuser"; + password = "userPassword"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; + } + ]; }; }; - shb.hardcodedsecret.forgejoSSOPassword = { - request = config.shb.forgejo.sso.sharedSecret.request; - settings.content = "ssoPassword"; + https = + { config, ... }: + { + shb.forgejo = { + ssl = config.shb.certs.certs.selfsigned.n; + }; }; - shb.hardcodedsecret.forgejoSSOPasswordAuthelia = { - request = config.shb.forgejo.sso.sharedSecretForAuthelia.request; - settings.content = "ssoPassword"; + ldap = + { config, ... }: + { + shb.forgejo = { + ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.lldap.ldapPort; + dcdomain = config.shb.lldap.dcdomain; + adminPassword.result = config.shb.hardcodedsecret.forgejoLdapUserPassword.result; + waitForSystemdServices = [ "lldap.service" ]; + + userGroup = "user_group"; + }; + }; + + shb.hardcodedsecret.forgejoLdapUserPassword = { + request = config.shb.forgejo.ldap.adminPassword.request; + settings.content = "ldapUserPassword"; + }; + }; + + sso = + { config, ... }: + { + shb.forgejo = { + sso = { + enable = true; + endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + sharedSecret.result = config.shb.hardcodedsecret.forgejoSSOPassword.result; + sharedSecretForAuthelia.result = config.shb.hardcodedsecret.forgejoSSOPasswordAuthelia.result; + }; + }; + + shb.hardcodedsecret.forgejoSSOPassword = { + request = config.shb.forgejo.sso.sharedSecret.request; + settings.content = "ssoPassword"; + }; + + shb.hardcodedsecret.forgejoSSOPasswordAuthelia = { + request = config.shb.forgejo.sso.sharedSecretForAuthelia.request; + settings.content = "ssoPassword"; + }; }; - }; in { basic = lib.shb.runNixOSTest { @@ -170,14 +202,16 @@ in backup = lib.shb.runNixOSTest { name = "forgejo_backup"; - nodes.server = { config, ... }: { - imports = [ - basic - (lib.shb.backup config.shb.forgejo.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (lib.shb.backup config.shb.forgejo.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -193,7 +227,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; @@ -208,71 +242,100 @@ in ldap ]; }; - + nodes.client = { imports = [ - ({ config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - - test = { - subdomain = "f"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}/user/login"; - usernameFieldLabelRegex = "Username or email address"; - passwordFieldLabelRegex = "Password"; - loginButtonNameRegex = "[sS]ign [iI]n"; - testLoginWith = [ - { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" - ]; } - { username = "alice"; password = "AlicePassword"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page).to_have_title(re.compile('Dashboard'))" - ]; } - { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" - ]; } - { username = "bob"; password = "BobPassword"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page).to_have_title(re.compile('Dashboard'))" - ]; } - { username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" - ]; } - { username = "charlie"; password = "CharliePassword"; nextPageExpect = [ - "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" - ]; } + ( + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule ]; - }; - }) + + test = { + subdomain = "f"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}/user/login"; + usernameFieldLabelRegex = "Username or email address"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" + ]; + } + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; + } + { + username = "charlie"; + password = "NotCharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "CharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text('Username or password is incorrect.')).to_be_visible()" + ]; + } + ]; + }; + } + ) ]; }; - + testScript = commonTestScript.access; }; sso = lib.shb.runNixOSTest { name = "forgejo_sso"; - nodes.server = { config, pkgs, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ]; - }; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; diff --git a/test/services/grocy.nix b/test/services/grocy.nix index c2955a0..f3e417b 100644 --- a/test/services/grocy.nix +++ b/test/services/grocy.nix @@ -2,65 +2,83 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.grocy.ssl); - waitForServices = { ... }: [ - "phpfpm-grocy.service" - "nginx.service" - ]; - waitForUnixSocket = { node, ... }: [ - node.config.services.phpfpm.pools.grocy.socket - ]; - }; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/services/grocy.nix - ]; - - test = { - subdomain = "g"; - }; - - shb.grocy = { - enable = true; - inherit (config.test) subdomain domain; - }; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - virtualisation.memorySize = 4096; - - test = { - subdomain = "g"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - usernameFieldLabelRegex = "Username"; - passwordFieldLabelRegex = "Password"; - loginButtonNameRegex = "OK"; - testLoginWith = [ - { username = "admin"; password = "admin oops"; nextPageExpect = [ - "expect(page.get_by_text('Invalid credentials, please try again')).to_be_visible()" - ]; } - { username = "admin"; password = "admin"; nextPageExpect = [ - "expect(page.get_by_text('Invalid credentials, please try again')).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('OK'))).not_to_be_visible()" - "expect(page).to_have_title(re.compile('Grocy'))" - ]; } + waitForServices = + { ... }: + [ + "phpfpm-grocy.service" + "nginx.service" + ]; + waitForUnixSocket = + { node, ... }: + [ + node.config.services.phpfpm.pools.grocy.socket ]; - }; }; - https = { config, ...}: { - shb.grocy = { - ssl = config.shb.certs.certs.selfsigned.n; + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/services/grocy.nix + ]; + + test = { + subdomain = "g"; + }; + + shb.grocy = { + enable = true; + inherit (config.test) subdomain domain; + }; + }; + + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "g"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + usernameFieldLabelRegex = "Username"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "OK"; + testLoginWith = [ + { + username = "admin"; + password = "admin oops"; + nextPageExpect = [ + "expect(page.get_by_text('Invalid credentials, please try again')).to_be_visible()" + ]; + } + { + username = "admin"; + password = "admin"; + nextPageExpect = [ + "expect(page.get_by_text('Invalid credentials, please try again')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('OK'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Grocy'))" + ]; + } + ]; + }; + }; + + https = + { config, ... }: + { + shb.grocy = { + ssl = config.shb.certs.certs.selfsigned.n; + }; }; - }; in { basic = lib.shb.runNixOSTest { @@ -91,7 +109,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; diff --git a/test/services/hledger.nix b/test/services/hledger.nix index 2f36315..5dc5639 100644 --- a/test/services/hledger.nix +++ b/test/services/hledger.nix @@ -2,59 +2,71 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.hledger.ssl); - waitForServices = { ... }: [ - "hledger-web.service" - "nginx.service" - ]; - }; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/services/hledger.nix - ]; - - test = { - subdomain = "h"; - }; - - shb.hledger = { - enable = true; - inherit (config.test) subdomain domain; - }; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - - test = { - subdomain = "h"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - testLoginWith = [ - { nextPageExpect = [ - "expect(page).to_have_title('journal - hledger-web')" - ]; } + waitForServices = + { ... }: + [ + "hledger-web.service" + "nginx.service" ]; - }; }; - https = { config, ... }: { - shb.hledger = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/services/hledger.nix + ]; - sso = { config, ... }: { - shb.hledger = { - authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + test = { + subdomain = "h"; + }; + + shb.hledger = { + enable = true; + inherit (config.test) subdomain domain; + }; + }; + + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + + test = { + subdomain = "h"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + testLoginWith = [ + { + nextPageExpect = [ + "expect(page).to_have_title('journal - hledger-web')" + ]; + } + ]; + }; + }; + + https = + { config, ... }: + { + shb.hledger = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + sso = + { config, ... }: + { + shb.hledger = { + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; }; - }; in { basic = lib.shb.runNixOSTest { @@ -78,14 +90,16 @@ in backup = lib.shb.runNixOSTest { name = "hledger_backup"; - nodes.server = { config, ... }: { - imports = [ - basic - (lib.shb.backup config.shb.hledger.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (lib.shb.backup config.shb.hledger.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -101,7 +115,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; @@ -109,18 +123,20 @@ in sso = lib.shb.runNixOSTest { name = "hledger_sso"; - nodes.server = { config, pkgs, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ]; - }; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access.override { redirectSSO = true; diff --git a/test/services/home-assistant.nix b/test/services/home-assistant.nix index 84fbcf4..030ff8a 100644 --- a/test/services/home-assistant.nix +++ b/test/services/home-assistant.nix @@ -2,89 +2,103 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.home-assistant.ssl); - waitForServices = { ... }: [ - "home-assistant.service" - "nginx.service" - ]; - waitForPorts = { node, ... }: [ - 8123 - ]; - }; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/services/home-assistant.nix - ]; - - test = { - subdomain = "ha"; - }; - - shb.home-assistant = { - enable = true; - inherit (config.test) subdomain domain; - - config = { - name = "Tiserbox"; - country = "CH"; - latitude = "01.0000000000"; - longitude.source = pkgs.writeText "longitude" "01.0000000000"; - time_zone = "Europe/Zurich"; - unit_system = "metric"; - }; - }; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - virtualisation.memorySize = 4096; - - test = { - subdomain = "ha"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - testLoginWith = [ - { nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Create my smart home')).click()" - - "expect(page.get_by_text('Create user')).to_be_visible()" - "page.get_by_label(re.compile('Name')).fill('Admin')" - "page.get_by_label(re.compile('Username')).fill('admin')" - "page.get_by_label(re.compile('Password')).fill('adminpassword')" - "page.get_by_label(re.compile('Confirm password')).fill('adminpassword')" - "page.get_by_role('button', name=re.compile('Create account')).click()" - - "expect(page.get_by_text('All set!')).to_be_visible()" - "page.get_by_role('button', name=re.compile('Finish')).click()" - - "expect(page).to_have_title(re.compile('Overview'), timeout=15000)" - ]; } + waitForServices = + { ... }: + [ + "home-assistant.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + 8123 ]; - }; }; - https = { config, ...}: { - shb.home-assistant = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/services/home-assistant.nix + ]; - ldap = { config, ... }: { - shb.home-assistant = { - ldap = { + test = { + subdomain = "ha"; + }; + + shb.home-assistant = { enable = true; - host = "127.0.0.1"; - port = config.shb.lldap.webUIListenPort; - userGroup = "homeassistant_user"; + inherit (config.test) subdomain domain; + + config = { + name = "Tiserbox"; + country = "CH"; + latitude = "01.0000000000"; + longitude.source = pkgs.writeText "longitude" "01.0000000000"; + time_zone = "Europe/Zurich"; + unit_system = "metric"; + }; + }; + }; + + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "ha"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + testLoginWith = [ + { + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Create my smart home')).click()" + + "expect(page.get_by_text('Create user')).to_be_visible()" + "page.get_by_label(re.compile('Name')).fill('Admin')" + "page.get_by_label(re.compile('Username')).fill('admin')" + "page.get_by_label(re.compile('Password')).fill('adminpassword')" + "page.get_by_label(re.compile('Confirm password')).fill('adminpassword')" + "page.get_by_role('button', name=re.compile('Create account')).click()" + + "expect(page.get_by_text('All set!')).to_be_visible()" + "page.get_by_role('button', name=re.compile('Finish')).click()" + + "expect(page).to_have_title(re.compile('Overview'), timeout=15000)" + ]; + } + ]; + }; + }; + + https = + { config, ... }: + { + shb.home-assistant = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + ldap = + { config, ... }: + { + shb.home-assistant = { + ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.lldap.webUIListenPort; + userGroup = "homeassistant_user"; + }; }; }; - }; # Not yet supported # @@ -95,58 +109,60 @@ let # }; # }; - voice = { config, ... }: { - # For now, verifying the packages can build is good enough. - environment.systemPackages = [ - config.services.wyoming.piper.package - config.services.wyoming.openwakeword.package - config.services.wyoming.faster-whisper.package - ]; + voice = + { config, ... }: + { + # For now, verifying the packages can build is good enough. + environment.systemPackages = [ + config.services.wyoming.piper.package + config.services.wyoming.openwakeword.package + config.services.wyoming.faster-whisper.package + ]; - # TODO: enable this back. The issue id the services cannot talk to the internet - # to download the models so they fail to start.. - # shb.home-assistant.voice.text-to-speech = { - # "fr" = { - # enable = true; - # voice = "fr-siwis-medium"; - # uri = "tcp://0.0.0.0:10200"; - # speaker = 0; - # }; - # "en" = { - # enable = true; - # voice = "en_GB-alba-medium"; - # uri = "tcp://0.0.0.0:10201"; - # speaker = 0; - # }; - # }; - # shb.home-assistant.voice.speech-to-text = { - # "tiny-fr" = { - # enable = true; - # model = "base-int8"; - # language = "fr"; - # uri = "tcp://0.0.0.0:10300"; - # device = "cpu"; - # }; - # "tiny-en" = { - # enable = true; - # model = "base-int8"; - # language = "en"; - # uri = "tcp://0.0.0.0:10301"; - # device = "cpu"; - # }; - # }; - # shb.home-assistant.voice.wakeword = { - # enable = true; - # uri = "tcp://127.0.0.1:10400"; - # preloadModels = [ - # "alexa" - # "hey_jarvis" - # "hey_mycroft" - # "hey_rhasspy" - # "ok_nabu" - # ]; - # }; - }; + # TODO: enable this back. The issue id the services cannot talk to the internet + # to download the models so they fail to start.. + # shb.home-assistant.voice.text-to-speech = { + # "fr" = { + # enable = true; + # voice = "fr-siwis-medium"; + # uri = "tcp://0.0.0.0:10200"; + # speaker = 0; + # }; + # "en" = { + # enable = true; + # voice = "en_GB-alba-medium"; + # uri = "tcp://0.0.0.0:10201"; + # speaker = 0; + # }; + # }; + # shb.home-assistant.voice.speech-to-text = { + # "tiny-fr" = { + # enable = true; + # model = "base-int8"; + # language = "fr"; + # uri = "tcp://0.0.0.0:10300"; + # device = "cpu"; + # }; + # "tiny-en" = { + # enable = true; + # model = "base-int8"; + # language = "en"; + # uri = "tcp://0.0.0.0:10301"; + # device = "cpu"; + # }; + # }; + # shb.home-assistant.voice.wakeword = { + # enable = true; + # uri = "tcp://127.0.0.1:10400"; + # preloadModels = [ + # "alexa" + # "hey_jarvis" + # "hey_mycroft" + # "hey_rhasspy" + # "ok_nabu" + # ]; + # }; + }; in { basic = lib.shb.runNixOSTest { @@ -169,14 +185,16 @@ in backup = lib.shb.runNixOSTest { name = "homeassistant_backup"; - nodes.server = { config, ... }: { - imports = [ - basic - (lib.shb.backup config.shb.home-assistant.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (lib.shb.backup config.shb.home-assistant.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -192,24 +210,24 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; ldap = lib.shb.runNixOSTest { name = "homeassistant_ldap"; - + nodes.server = { - imports = [ + imports = [ basic lib.shb.ldap ldap ]; }; - - nodes.client = {}; - + + nodes.client = { }; + testScript = commonTestScript.access; }; @@ -218,7 +236,7 @@ in # sso = lib.shb.runNixOSTest { # name = "vaultwarden_sso"; # - # nodes.server = lib.mkMerge [ + # nodes.server = lib.mkMerge [ # basic # (lib.shb.certs domain) # https @@ -235,16 +253,16 @@ in voice = lib.shb.runNixOSTest { name = "homeassistant_voice"; - + nodes.server = { - imports = [ + imports = [ basic voice ]; }; - - nodes.client = {}; - + + nodes.client = { }; + testScript = commonTestScript.access; }; } diff --git a/test/services/immich.nix b/test/services/immich.nix index 8e60b8a..15df185 100644 --- a/test/services/immich.nix +++ b/test/services/immich.nix @@ -5,117 +5,138 @@ let commonTestScript = lib.shb.accessScript { hasSSL = { node, ... }: !(isNull node.config.shb.immich.ssl); - waitForServices = { ... }: [ "immich-server.service" "postgresql.service" "nginx.service" ]; - waitForPorts = { ... }: [ 2283 80 ]; + waitForServices = + { ... }: + [ + "immich-server.service" + "postgresql.service" + "nginx.service" + ]; + waitForPorts = + { ... }: + [ + 2283 + 80 + ]; waitForUrls = { proto_fqdn, ... }: [ "${proto_fqdn}" ]; }; - base = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/services/immich.nix - ]; + base = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/services/immich.nix + ]; - virtualisation.memorySize = 4096; - virtualisation.cores = 2; + virtualisation.memorySize = 4096; + virtualisation.cores = 2; - test = { - inherit subdomain domain; + test = { + inherit subdomain domain; + }; + + shb.immich = { + enable = true; + inherit subdomain domain; + + debug = true; + }; + + # Required for tests + environment.systemPackages = [ pkgs.curl ]; }; - shb.immich = { - enable = true; - inherit subdomain domain; + basic = + { config, ... }: + { + imports = [ base ]; - debug = true; + test.hasSSL = false; }; - # Required for tests - environment.systemPackages = [ pkgs.curl ]; - }; + https = + { config, ... }: + { + imports = [ + base + lib.shb.certs + ]; - basic = { config, ... }: { - imports = [ base ]; - - test.hasSSL = false; - }; - - https = { config, ... }: { - imports = [ - base - lib.shb.certs - ]; - - test.hasSSL = true; - shb.immich.ssl = config.shb.certs.certs.selfsigned.n; - }; - - backup = { config, ... }: { - imports = [ - https - (lib.shb.backup config.shb.immich.backup) - ]; - }; - - sso = { config, ... }: { - imports = [ - https - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - ]; - - shb.immich.sso = { - enable = true; - provider = "Authelia"; - endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - clientID = "immich"; - autoLaunch = true; - sharedSecret.result = config.shb.hardcodedsecret.immichSSOSecret.result; - sharedSecretForAuthelia.result = config.shb.hardcodedsecret.immichSSOSecretAuthelia.result; + test.hasSSL = true; + shb.immich.ssl = config.shb.certs.certs.selfsigned.n; }; - shb.hardcodedsecret.immichSSOSecret = { - request = config.shb.immich.sso.sharedSecret.request; - settings.content = "immichSSOSecret"; + backup = + { config, ... }: + { + imports = [ + https + (lib.shb.backup config.shb.immich.backup) + ]; }; - shb.hardcodedsecret.immichSSOSecretAuthelia = { - request = config.shb.immich.sso.sharedSecretForAuthelia.request; - settings.content = "immichSSOSecret"; - }; + sso = + { config, ... }: + { + imports = [ + https + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + ]; - # Configure LDAP groups for group-based access control - shb.lldap.ensureGroups.immich_user = {}; + shb.immich.sso = { + enable = true; + provider = "Authelia"; + endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + clientID = "immich"; + autoLaunch = true; + sharedSecret.result = config.shb.hardcodedsecret.immichSSOSecret.result; + sharedSecretForAuthelia.result = config.shb.hardcodedsecret.immichSSOSecretAuthelia.result; + }; - shb.lldap.ensureUsers.immich_test_user = { - email = "immich_user@example.com"; - groups = [ "immich_user" ]; - password.result = config.shb.hardcodedsecret.ldapImmichUserPassword.result; - }; + shb.hardcodedsecret.immichSSOSecret = { + request = config.shb.immich.sso.sharedSecret.request; + settings.content = "immichSSOSecret"; + }; - shb.lldap.ensureUsers.regular_test_user = { - email = "regular_user@example.com"; - groups = [ ]; - password.result = config.shb.hardcodedsecret.ldapRegularUserPassword.result; - }; + shb.hardcodedsecret.immichSSOSecretAuthelia = { + request = config.shb.immich.sso.sharedSecretForAuthelia.request; + settings.content = "immichSSOSecret"; + }; - shb.hardcodedsecret.ldapImmichUserPassword = { - request = config.shb.lldap.ensureUsers.immich_test_user.password.request; - settings.content = "immich_user_password"; - }; + # Configure LDAP groups for group-based access control + shb.lldap.ensureGroups.immich_user = { }; - shb.hardcodedsecret.ldapRegularUserPassword = { - request = config.shb.lldap.ensureUsers.regular_test_user.password.request; - settings.content = "regular_user_password"; + shb.lldap.ensureUsers.immich_test_user = { + email = "immich_user@example.com"; + groups = [ "immich_user" ]; + password.result = config.shb.hardcodedsecret.ldapImmichUserPassword.result; + }; + + shb.lldap.ensureUsers.regular_test_user = { + email = "regular_user@example.com"; + groups = [ ]; + password.result = config.shb.hardcodedsecret.ldapRegularUserPassword.result; + }; + + shb.hardcodedsecret.ldapImmichUserPassword = { + request = config.shb.lldap.ensureUsers.immich_test_user.password.request; + settings.content = "immich_user_password"; + }; + + shb.hardcodedsecret.ldapRegularUserPassword = { + request = config.shb.lldap.ensureUsers.regular_test_user.password.request; + settings.content = "regular_user_password"; + }; }; - }; in { basic = pkgs.nixosTest { name = "immich-basic"; nodes.server = basic; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript; }; @@ -124,7 +145,7 @@ in name = "immich-https"; nodes.server = https; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript; }; @@ -133,13 +154,21 @@ in name = "immich-backup"; nodes.server = backup; - nodes.client = {}; + nodes.client = { }; - testScript = (lib.shb.mkScripts { - hasSSL = args: !(isNull args.node.config.shb.immich.ssl); - waitForServices = args: [ "immich-server.service" "postgresql.service" "nginx.service" ]; - waitForPorts = args: [ 2283 80 ]; - waitForUrls = args: [ "${args.proto_fqdn}" ]; - }).backup; + testScript = + (lib.shb.mkScripts { + hasSSL = args: !(isNull args.node.config.shb.immich.ssl); + waitForServices = args: [ + "immich-server.service" + "postgresql.service" + "nginx.service" + ]; + waitForPorts = args: [ + 2283 + 80 + ]; + waitForUrls = args: [ "${args.proto_fqdn}" ]; + }).backup; }; } diff --git a/test/services/jellyfin.nix b/test/services/jellyfin.nix index 10a5052..954cec0 100644 --- a/test/services/jellyfin.nix +++ b/test/services/jellyfin.nix @@ -4,164 +4,185 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.jellyfin.ssl); - waitForServices = { ... }: [ - "jellyfin.service" - "nginx.service" - ]; - waitForPorts = { node, ... }: [ - port - ]; - waitForUrls = { proto_fqdn, ... }: [ - "${proto_fqdn}/System/Info/Public" - ]; - extraScript = { node, ... }: '' - headers = unline_with(" ", """ - -H 'Content-Type: application/json' - -H 'Authorization: MediaBrowser Client="Android TV", Device="Nvidia Shield", DeviceId="ZQ9YQHHrUzk24vV", Version="0.15.3"' - """) - with subtest("api login success"): - response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", - data="""{"Username": "jellyfin", "Pw": "admin"}""", - extra=headers) - if response['code'] != 200: - raise Exception(f"Expected success, got: {response['code']}") - - with subtest("api login failure"): - response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", - data="""{"Username": "jellyfin", "Pw": "badpassword"}""", - extra=headers) - if response['code'] != 401: - raise Exception(f"Expected failure, got: {response['code']}") - ''; - }; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/services/jellyfin.nix - ]; - test = { - subdomain = "j"; - }; - - shb.jellyfin = { - enable = true; - inherit (config.test) subdomain domain; - inherit port; - admin = { - username = "jellyfin"; - password.result = config.shb.hardcodedsecret.jellyfinAdminPassword.result; - }; - debug = true; - }; - - shb.hardcodedsecret.jellyfinAdminPassword = { - request = config.shb.jellyfin.admin.password.request; - settings.content = "admin"; - }; - - environment.systemPackages = [ - pkgs.sqlite - ]; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.clientLoginModule - ]; - virtualisation.memorySize = 4096; - - test = { - subdomain = "j"; - }; - - test.login = { - browser = "firefox"; - # I tried without the path part but it randomly selects either the wizard - # or the page that selects a server. - # startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/wizardstart.html"; - # startUrl = "${config.test.proto}://${config.test.fqdn}"; - startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/login.html"; - usernameFieldLabelRegex = "[Uu]ser"; - loginButtonNameRegex = "Sign In"; - testLoginWith = [ - # I just couldn't make this work. It's very flaky. - # Most of the time, the login jellyfin page doesn't even load - # and the playwright browser is stuck on the splash page. - # I resorted to test the API directly. - # { username = "jellyfin"; password = "badpassword"; nextPageExpect = [ - # "expect(page).to_have_title(re.compile('Jellyfin'))" - # "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=30000)" - # ]; } - # { username = "jellyfin"; password = "admin"; nextPageExpect = [ - # "expect(page).to_have_title(re.compile('Jellyfin'))" - # "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=30000)" - # "expect(page.get_by_role('label', re.compile('[Uu]ser'))).not_to_be_visible(timeout=30000)" - # "expect(page.get_by_text(re.compile('[Pp]assword'))).not_to_be_visible(timeout=30000)" - # ]; } + waitForServices = + { ... }: + [ + "jellyfin.service" + "nginx.service" ]; - }; + waitForPorts = + { node, ... }: + [ + port + ]; + waitForUrls = + { proto_fqdn, ... }: + [ + "${proto_fqdn}/System/Info/Public" + ]; + extraScript = + { node, ... }: + '' + headers = unline_with(" ", """ + -H 'Content-Type: application/json' + -H 'Authorization: MediaBrowser Client="Android TV", Device="Nvidia Shield", DeviceId="ZQ9YQHHrUzk24vV", Version="0.15.3"' + """) + with subtest("api login success"): + response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", + data="""{"Username": "jellyfin", "Pw": "admin"}""", + extra=headers) + if response['code'] != 200: + raise Exception(f"Expected success, got: {response['code']}") + + with subtest("api login failure"): + response = curl(client, """{"code":%{response_code}}""", "${node.config.test.proto_fqdn}/Users/AuthenticateByName", + data="""{"Username": "jellyfin", "Pw": "badpassword"}""", + extra=headers) + if response['code'] != 401: + raise Exception(f"Expected failure, got: {response['code']}") + ''; }; - https = { config, ... }: { - shb.jellyfin = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - test = { - hasSSL = true; - }; - }; - - ldap = { config, ... }: { - shb.jellyfin = { - ldap = { - enable = true; - host = "127.0.0.1"; - port = config.shb.lldap.ldapPort; - dcdomain = config.shb.lldap.dcdomain; - adminPassword.result = config.shb.hardcodedsecret.jellyfinLdapUserPassword.result; + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/services/jellyfin.nix + ]; + test = { + subdomain = "j"; }; - }; - shb.hardcodedsecret.jellyfinLdapUserPassword = { - request = config.shb.jellyfin.ldap.adminPassword.request; - settings.content = "ldapUserPassword"; - }; - }; - - sso = { config, ... }: { - shb.jellyfin = { - sso = { + shb.jellyfin = { enable = true; - endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - sharedSecret.result = config.shb.hardcodedsecret.jellyfinSSOPassword.result; - sharedSecretForAuthelia.result = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result; + inherit (config.test) subdomain domain; + inherit port; + admin = { + username = "jellyfin"; + password.result = config.shb.hardcodedsecret.jellyfinAdminPassword.result; + }; + debug = true; }; - }; - shb.hardcodedsecret.jellyfinSSOPassword = { - request = config.shb.jellyfin.sso.sharedSecret.request; - settings.content = "ssoPassword"; - }; + shb.hardcodedsecret.jellyfinAdminPassword = { + request = config.shb.jellyfin.admin.password.request; + settings.content = "admin"; + }; - shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = { - request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request; - settings.content = "ssoPassword"; - }; - }; - - jellyfinTest = name: { nodes, testScript }: lib.shb.runNixOSTest { - name = "jellyfin_${name}"; - - interactive.nodes.server = { environment.systemPackages = [ pkgs.sqlite ]; }; - inherit nodes; - inherit testScript; - }; + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "j"; + }; + + test.login = { + browser = "firefox"; + # I tried without the path part but it randomly selects either the wizard + # or the page that selects a server. + # startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/wizardstart.html"; + # startUrl = "${config.test.proto}://${config.test.fqdn}"; + startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/login.html"; + usernameFieldLabelRegex = "[Uu]ser"; + loginButtonNameRegex = "Sign In"; + testLoginWith = [ + # I just couldn't make this work. It's very flaky. + # Most of the time, the login jellyfin page doesn't even load + # and the playwright browser is stuck on the splash page. + # I resorted to test the API directly. + # { username = "jellyfin"; password = "badpassword"; nextPageExpect = [ + # "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=30000)" + # ]; } + # { username = "jellyfin"; password = "admin"; nextPageExpect = [ + # "expect(page).to_have_title(re.compile('Jellyfin'))" + # "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=30000)" + # "expect(page.get_by_role('label', re.compile('[Uu]ser'))).not_to_be_visible(timeout=30000)" + # "expect(page.get_by_text(re.compile('[Pp]assword'))).not_to_be_visible(timeout=30000)" + # ]; } + ]; + }; + }; + + https = + { config, ... }: + { + shb.jellyfin = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + test = { + hasSSL = true; + }; + }; + + ldap = + { config, ... }: + { + shb.jellyfin = { + ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.lldap.ldapPort; + dcdomain = config.shb.lldap.dcdomain; + adminPassword.result = config.shb.hardcodedsecret.jellyfinLdapUserPassword.result; + }; + }; + + shb.hardcodedsecret.jellyfinLdapUserPassword = { + request = config.shb.jellyfin.ldap.adminPassword.request; + settings.content = "ldapUserPassword"; + }; + }; + + sso = + { config, ... }: + { + shb.jellyfin = { + sso = { + enable = true; + endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + sharedSecret.result = config.shb.hardcodedsecret.jellyfinSSOPassword.result; + sharedSecretForAuthelia.result = config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result; + }; + }; + + shb.hardcodedsecret.jellyfinSSOPassword = { + request = config.shb.jellyfin.sso.sharedSecret.request; + settings.content = "ssoPassword"; + }; + + shb.hardcodedsecret.jellyfinSSOPasswordAuthelia = { + request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request; + settings.content = "ssoPassword"; + }; + }; + + jellyfinTest = + name: + { nodes, testScript }: + lib.shb.runNixOSTest { + name = "jellyfin_${name}"; + + interactive.nodes.server = { + environment.systemPackages = [ + pkgs.sqlite + ]; + }; + + inherit nodes; + inherit testScript; + }; in { basic = jellyfinTest "basic" { @@ -174,20 +195,22 @@ in # Client login does not work without SSL. # At least, I couldn't make it work. - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; backup = jellyfinTest "backup" { - nodes.server = { config, ... }: { - imports = [ - basic - (lib.shb.backup config.shb.jellyfin.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (lib.shb.backup config.shb.jellyfin.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -201,12 +224,14 @@ in ]; }; - nodes.client = { config, lib, ... }: { - imports = [ - lib.shb.baseModule - clientLogin - ]; - }; + nodes.client = + { config, lib, ... }: + { + imports = [ + lib.shb.baseModule + clientLogin + ]; + }; testScript = commonTestScript.access; }; @@ -219,25 +244,27 @@ in ldap ]; }; - - nodes.client = {}; - + + nodes.client = { }; + testScript = commonTestScript.access; }; sso = jellyfinTest "sso" { - nodes.server = { config, pkgs, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ]; - }; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; diff --git a/test/services/karakeep.nix b/test/services/karakeep.nix index e44f637..38b8810 100644 --- a/test/services/karakeep.nix +++ b/test/services/karakeep.nix @@ -3,144 +3,182 @@ let nextauthSecret = "nextauthSecret"; oidcSecret = "oidcSecret"; - testLib = pkgs.callPackage ../common.nix {}; + testLib = pkgs.callPackage ../common.nix { }; commonTestScript = testLib.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.karakeep.ssl); - waitForServices = { ... }: [ - "karakeep-init.service" - "karakeep-browser.service" - "karakeep-web.service" - "karakeep-workers.service" - "nginx.service" - ]; - waitForPorts = { node, ... }: [ - node.config.shb.karakeep.port - ]; - }; - - basic = { config, ... }: { - imports = [ - testLib.baseModule - ../../modules/blocks/hardcodedsecret.nix - ../../modules/blocks/lldap.nix - ../../modules/services/karakeep.nix - ]; - - test = { - subdomain = "k"; - }; - - shb.karakeep = { - enable = true; - inherit (config.test) subdomain domain; - - nextauthSecret.result = config.shb.hardcodedsecret.nextauthSecret.result; - meilisearchMasterKey.result = config.shb.hardcodedsecret.meilisearchMasterKey.result; - }; - - shb.hardcodedsecret.nextauthSecret = { - request = config.shb.karakeep.nextauthSecret.request; - settings.content = nextauthSecret; - }; - shb.hardcodedsecret.meilisearchMasterKey = { - request = config.shb.karakeep.meilisearchMasterKey.request; - settings.content = "meilisearch-master-key"; - }; - - networking.hosts = { - "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; - }; - }; - - https = { config, ... }: { - shb.karakeep = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; - - ldap = { config, ... }: { - shb.karakeep = { - ldap = { - userGroup = "user_group"; - }; - }; - }; - - clientLoginSso = { config, ... }: { - imports = [ - testLib.baseModule - testLib.clientLoginModule - ]; - test = { - subdomain = "k"; - }; - - test.login = { - startUrl = "https://${config.test.fqdn}"; - beforeHook = '' - page.get_by_role("button", name="single sign-on").click() - ''; - usernameFieldLabelRegex = "Username"; - passwordFieldLabelRegex = "Password"; - loginButtonNameRegex = "[sS]ign [iI]n"; - testLoginWith = [ - { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" - ]; } - { username = "alice"; password = "AlicePassword"; nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Accept')).click()" - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page.get_by_text('new item')).to_be_visible()" - ]; } - { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" - ]; } - { username = "bob"; password = "BobPassword"; nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Accept')).click()" - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page.get_by_text('new item')).to_be_visible()" - ]; } - { username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" - ]; } - { username = "charlie"; password = "CharliePassword"; nextPageExpect = [ - # "page.get_by_role('button', name=re.compile('Accept')).click()" # I don't understand why this is not needed. Maybe it keeps somewhere the previous token? - "expect(page.get_by_text(re.compile('login failed'))).to_be_visible(timeout=10000)" - ]; } + waitForServices = + { ... }: + [ + "karakeep-init.service" + "karakeep-browser.service" + "karakeep-web.service" + "karakeep-workers.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + node.config.shb.karakeep.port ]; - }; }; - sso = { config, ... }: { - shb.karakeep = { - sso = { - enable = true; - authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - clientID = "karakeep"; + basic = + { config, ... }: + { + imports = [ + testLib.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/blocks/lldap.nix + ../../modules/services/karakeep.nix + ]; - sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result; - sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result; + test = { + subdomain = "k"; + }; + + shb.karakeep = { + enable = true; + inherit (config.test) subdomain domain; + + nextauthSecret.result = config.shb.hardcodedsecret.nextauthSecret.result; + meilisearchMasterKey.result = config.shb.hardcodedsecret.meilisearchMasterKey.result; + }; + + shb.hardcodedsecret.nextauthSecret = { + request = config.shb.karakeep.nextauthSecret.request; + settings.content = nextauthSecret; + }; + shb.hardcodedsecret.meilisearchMasterKey = { + request = config.shb.karakeep.meilisearchMasterKey.request; + settings.content = "meilisearch-master-key"; + }; + + networking.hosts = { + "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; }; }; - shb.hardcodedsecret.oidcSecret = { - request = config.shb.karakeep.sso.sharedSecret.request; - settings.content = oidcSecret; + https = + { config, ... }: + { + shb.karakeep = { + ssl = config.shb.certs.certs.selfsigned.n; + }; }; - shb.hardcodedsecret.oidcAutheliaSecret = { - request = config.shb.karakeep.sso.sharedSecretForAuthelia.request; - settings.content = oidcSecret; + + ldap = + { config, ... }: + { + shb.karakeep = { + ldap = { + userGroup = "user_group"; + }; + }; + }; + + clientLoginSso = + { config, ... }: + { + imports = [ + testLib.baseModule + testLib.clientLoginModule + ]; + test = { + subdomain = "k"; + }; + + test.login = { + startUrl = "https://${config.test.fqdn}"; + beforeHook = '' + page.get_by_role("button", name="single sign-on").click() + ''; + usernameFieldLabelRegex = "Username"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('new item')).to_be_visible()" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('new item')).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "NotCharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "charlie"; + password = "CharliePassword"; + nextPageExpect = [ + # "page.get_by_role('button', name=re.compile('Accept')).click()" # I don't understand why this is not needed. Maybe it keeps somewhere the previous token? + "expect(page.get_by_text(re.compile('login failed'))).to_be_visible(timeout=10000)" + ]; + } + ]; + }; + }; + + sso = + { config, ... }: + { + shb.karakeep = { + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + clientID = "karakeep"; + + sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result; + sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result; + }; + }; + + shb.hardcodedsecret.oidcSecret = { + request = config.shb.karakeep.sso.sharedSecret.request; + settings.content = oidcSecret; + }; + shb.hardcodedsecret.oidcAutheliaSecret = { + request = config.shb.karakeep.sso.sharedSecretForAuthelia.request; + settings.content = oidcSecret; + }; }; - }; in { basic = pkgs.testers.runNixOSTest { name = "karakeep_basic"; - nodes.client = {}; + nodes.client = { }; nodes.server = { imports = [ basic @@ -153,14 +191,16 @@ in backup = pkgs.testers.runNixOSTest { name = "karakeep_backup"; - nodes.server = { config, ... }: { - imports = [ - basic - (testLib.backup config.shb.karakeep.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (testLib.backup config.shb.karakeep.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -168,7 +208,7 @@ in https = pkgs.testers.runNixOSTest { name = "karakeep_https"; - nodes.client = {}; + nodes.client = { }; nodes.server = { imports = [ basic @@ -191,19 +231,21 @@ in virtualisation.memorySize = 4096; }; - nodes.server = { config, pkgs, ... }: { - imports = [ - basic - testLib.certs - https - testLib.ldap - ldap - (testLib.sso config.shb.certs.certs.selfsigned.n) - sso - ]; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + testLib.certs + https + testLib.ldap + ldap + (testLib.sso config.shb.certs.certs.selfsigned.n) + sso + ]; - virtualisation.memorySize = 4096; - }; + virtualisation.memorySize = 4096; + }; testScript = commonTestScript.access; }; diff --git a/test/services/monitoring.nix b/test/services/monitoring.nix index 39e66a0..957699a 100644 --- a/test/services/monitoring.nix +++ b/test/services/monitoring.nix @@ -4,43 +4,51 @@ let commonTestScript = lib.shb.accessScript { hasSSL = { node, ... }: !(isNull node.config.shb.monitoring.ssl); - waitForServices = { ... }: [ - "grafana.service" - ]; - waitForPorts = { node, ... }: [ - node.config.shb.monitoring.grafanaPort - ]; + waitForServices = + { ... }: + [ + "grafana.service" + ]; + waitForPorts = + { node, ... }: + [ + node.config.shb.monitoring.grafanaPort + ]; }; - basic = { config, ... }: { - test = { - subdomain = "g"; + basic = + { config, ... }: + { + test = { + subdomain = "g"; + }; + + shb.monitoring = { + enable = true; + inherit (config.test) subdomain domain; + + grafanaPort = 3000; + adminPassword.result = config.shb.hardcodedsecret."admin_password".result; + secretKey.result = config.shb.hardcodedsecret."secret_key".result; + }; + + shb.hardcodedsecret."admin_password" = { + request = config.shb.monitoring.adminPassword.request; + settings.content = password; + }; + shb.hardcodedsecret."secret_key" = { + request = config.shb.monitoring.secretKey.request; + settings.content = "secret_key_pw"; + }; }; - shb.monitoring = { - enable = true; - inherit (config.test) subdomain domain; - - grafanaPort = 3000; - adminPassword.result = config.shb.hardcodedsecret."admin_password".result; - secretKey.result = config.shb.hardcodedsecret."secret_key".result; + https = + { config, ... }: + { + shb.monitoring = { + ssl = config.shb.certs.certs.selfsigned.n; + }; }; - - shb.hardcodedsecret."admin_password" = { - request = config.shb.monitoring.adminPassword.request; - settings.content = password; - }; - shb.hardcodedsecret."secret_key" = { - request = config.shb.monitoring.secretKey.request; - settings.content = "secret_key_pw"; - }; - }; - - https = { config, ...}: { - shb.monitoring = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; in { basic = lib.shb.runNixOSTest { @@ -54,7 +62,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript; }; @@ -72,7 +80,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript; }; diff --git a/test/services/nextcloud.nix b/test/services/nextcloud.nix index 0d5e4da..5343880 100644 --- a/test/services/nextcloud.nix +++ b/test/services/nextcloud.nix @@ -6,267 +6,339 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.nextcloud.ssl); - waitForServices = { ... }: [ - "phpfpm-nextcloud.service" - "nginx.service" - ]; - waitForUnixSocket = { node, ... }: [ - node.config.services.phpfpm.pools.nextcloud.socket - ]; - extraScript = { node, fqdn, proto_fqdn, ... }: '' - with subtest("fails with incorrect authentication"): - client.fail( - "curl -f -s --location -X PROPFIND" - + """ -H "Depth: 1" """ - + """ -u ${adminUser}:other """ - + " --connect-to ${fqdn}:443:server:443" - + " --connect-to ${fqdn}:80:server:80" - + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/" - ) - - client.fail( - "curl -f -s --location -X PROPFIND" - + """ -H "Depth: 1" """ - + """ -u root:rootpw """ - + " --connect-to ${fqdn}:443:server:443" - + " --connect-to ${fqdn}:80:server:80" - + " ${proto_fqdn}/remote.php/dav/files/other/" - ) - - with subtest("fails with incorrect path"): - client.fail( - "curl -f -s --location -X PROPFIND" - + """ -H "Depth: 1" """ - + """ -u ${adminUser}:${adminPass} """ - + " --connect-to ${fqdn}:443:server:443" - + " --connect-to ${fqdn}:80:server:80" - + " ${proto_fqdn}/remote.php/dav/files/other/" - ) - - with subtest("can access webdav"): - client.succeed( - "curl -f -s --location -X PROPFIND" - + """ -H "Depth: 1" """ - + """ -u ${adminUser}:${adminPass} """ - + " --connect-to ${fqdn}:443:server:443" - + " --connect-to ${fqdn}:80:server:80" - + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/" - ) - - with subtest("can create and retrieve file"): - client.fail( - "curl -f -s --location -X GET" - + """ -H "Depth: 1" """ - + """ -u ${adminUser}:${adminPass} """ - + " --connect-to ${fqdn}:443:server:443" - + " --connect-to ${fqdn}:80:server:80" - + """ -T file """ - + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/file" - ) - client.succeed("echo 'hello' > file") - client.succeed( - "curl -f -s --location -X PUT" - + """ -H "Depth: 1" """ - + """ -u ${adminUser}:${adminPass} """ - + " --connect-to ${fqdn}:443:server:443" - + " --connect-to ${fqdn}:80:server:80" - + """ -T file """ - + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/" - ) - content = client.succeed( - "curl -f -s --location -X GET" - + """ -H "Depth: 1" """ - + """ -u ${adminUser}:${adminPass} """ - + " --connect-to ${fqdn}:443:server:443" - + " --connect-to ${fqdn}:80:server:80" - + """ -T file """ - + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/file" - ) - if content != "hello\n": - raise Exception("Got incorrect content for file, expected 'hello\n' but got:\n{}".format(content)) - ''; - }; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/services/nextcloud-server.nix - ]; - - test = { - subdomain = "n"; - }; - - shb.nextcloud = { - enable = true; - inherit (config.test) subdomain domain; - - dataDir = "/var/lib/nextcloud"; - tracing = null; - defaultPhoneRegion = "US"; - - # This option is only needed because we do not access Nextcloud at the default port in the VM. - externalFqdn = "${config.test.fqdn}:8080"; - - adminUser = adminUser; - adminPass.result = config.shb.hardcodedsecret.adminPass.result; - debug = false; # Enable this if needed, but beware it is _very_ verbose. - }; - - shb.hardcodedsecret.adminPass = { - request = config.shb.nextcloud.adminPass.request; - settings.content = adminPass; - }; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - virtualisation.memorySize = 4096; - - test = { - subdomain = "n"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - usernameFieldLabelRegex = "Account name"; - passwordFieldLabelRegex = "^ *[Pp]assword"; - loginButtonNameRegex = "[Ll]og [Ii]n"; - testLoginWith = [ - { username = adminUser; password = adminPass; nextPageExpect = [ - "expect(page.get_by_text('Wrong login or password')).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" - "expect(page).to_have_title(re.compile('Dashboard'))" - ]; } - # Failure is after so we're not throttled too much. - { username = adminUser; password = adminPass + "oops"; nextPageExpect = [ - "expect(page.get_by_text('Wrong login or password')).to_be_visible()" - ]; } + waitForServices = + { ... }: + [ + "phpfpm-nextcloud.service" + "nginx.service" ]; - }; - }; - - clientLdapLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - virtualisation.memorySize = 4096; - - test = { - subdomain = "n"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - usernameFieldLabelRegex = "Account name"; - passwordFieldLabelRegex = "^ *[Pp]assword"; - loginButtonNameRegex = "[Ll]og [Ii]n"; - testLoginWith = [ - { username = "alice"; password = "AlicePassword"; nextPageExpect = [ - "expect(page.get_by_text('Wrong login or password')).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" - "expect(page).to_have_title(re.compile('Dashboard'))" - ]; } - { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ - "expect(page.get_by_text('Wrong login or password')).to_be_visible()" - ]; } - { username = "bob"; password = "BobPassword"; nextPageExpect = [ - "expect(page.get_by_text('Wrong login or password')).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" - "expect(page).to_have_title(re.compile('Dashboard'))" - ]; } - { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ - "expect(page.get_by_text('Wrong login or password')).to_be_visible()" - ]; } + waitForUnixSocket = + { node, ... }: + [ + node.config.services.phpfpm.pools.nextcloud.socket ]; - }; + extraScript = + { + node, + fqdn, + proto_fqdn, + ... + }: + '' + with subtest("fails with incorrect authentication"): + client.fail( + "curl -f -s --location -X PROPFIND" + + """ -H "Depth: 1" """ + + """ -u ${adminUser}:other """ + + " --connect-to ${fqdn}:443:server:443" + + " --connect-to ${fqdn}:80:server:80" + + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/" + ) + + client.fail( + "curl -f -s --location -X PROPFIND" + + """ -H "Depth: 1" """ + + """ -u root:rootpw """ + + " --connect-to ${fqdn}:443:server:443" + + " --connect-to ${fqdn}:80:server:80" + + " ${proto_fqdn}/remote.php/dav/files/other/" + ) + + with subtest("fails with incorrect path"): + client.fail( + "curl -f -s --location -X PROPFIND" + + """ -H "Depth: 1" """ + + """ -u ${adminUser}:${adminPass} """ + + " --connect-to ${fqdn}:443:server:443" + + " --connect-to ${fqdn}:80:server:80" + + " ${proto_fqdn}/remote.php/dav/files/other/" + ) + + with subtest("can access webdav"): + client.succeed( + "curl -f -s --location -X PROPFIND" + + """ -H "Depth: 1" """ + + """ -u ${adminUser}:${adminPass} """ + + " --connect-to ${fqdn}:443:server:443" + + " --connect-to ${fqdn}:80:server:80" + + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/" + ) + + with subtest("can create and retrieve file"): + client.fail( + "curl -f -s --location -X GET" + + """ -H "Depth: 1" """ + + """ -u ${adminUser}:${adminPass} """ + + " --connect-to ${fqdn}:443:server:443" + + " --connect-to ${fqdn}:80:server:80" + + """ -T file """ + + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/file" + ) + client.succeed("echo 'hello' > file") + client.succeed( + "curl -f -s --location -X PUT" + + """ -H "Depth: 1" """ + + """ -u ${adminUser}:${adminPass} """ + + " --connect-to ${fqdn}:443:server:443" + + " --connect-to ${fqdn}:80:server:80" + + """ -T file """ + + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/" + ) + content = client.succeed( + "curl -f -s --location -X GET" + + """ -H "Depth: 1" """ + + """ -u ${adminUser}:${adminPass} """ + + " --connect-to ${fqdn}:443:server:443" + + " --connect-to ${fqdn}:80:server:80" + + """ -T file """ + + " ${proto_fqdn}/remote.php/dav/files/${adminUser}/file" + ) + if content != "hello\n": + raise Exception("Got incorrect content for file, expected 'hello\n' but got:\n{}".format(content)) + ''; }; - clientSsoLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - virtualisation.memorySize = 4096; - - test = { - subdomain = "n"; - }; - - networking.hosts = { - "192.168.1.2" = [ "auth.example.com" ]; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - # No need since Nextcloud is auto-redirecting to the SSO sign in page. - # beforeHook = '' - # page.get_by_role("link", name="Sign in with SHB-Authelia").click() - # ''; - usernameFieldLabelRegex = "Username"; - passwordFieldSelector = "get_by_label(\"Password *\")"; - loginButtonNameRegex = "[sS]ign [iI]n"; - testLoginWith = [ - { username = "alice"; password = "AlicePassword"; nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Accept')).click()" - "expect(page).to_have_title(re.compile('Dashboard'))" - "page.goto('https://${config.test.fqdn}/settings/admin')" - "expect(page.get_by_text('Access forbidden')).to_be_visible()" - ]; } - { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ - "expect(page.get_by_text('Incorrect username or password')).to_be_visible()" - ]; } - { username = "bob"; password = "BobPassword"; nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Accept')).click()" - "expect(page).to_have_title(re.compile('Dashboard'))" - "page.goto('https://${config.test.fqdn}/settings/admin')" - "expect(page.get_by_text('Access forbidden')).not_to_be_visible()" - ]; } - { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ - "expect(page.get_by_text('Incorrect username or password')).to_be_visible()" - ]; } - { username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [ - "expect(page.get_by_text('Incorrect username or password')).to_be_visible()" - ]; } - { username = "charlie"; password = "CharliePassword"; nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Accept')).click()" - "expect(page.get_by_text('not member of the allowed groups')).to_be_visible()" - ]; } + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/services/nextcloud-server.nix ]; - }; - }; - https = { config, ...}: { - shb.nextcloud = { - ssl = config.shb.certs.certs.selfsigned.n; + test = { + subdomain = "n"; + }; - externalFqdn = lib.mkForce null; - }; - }; - - ldap = { config, ... }: { - shb.nextcloud = { - apps.ldap = { + shb.nextcloud = { enable = true; - host = "127.0.0.1"; - port = config.shb.lldap.ldapPort; - dcdomain = config.shb.lldap.dcdomain; - adminName = "admin"; - adminPassword.result = config.shb.hardcodedsecret.nextcloudLdapUserPassword.result; - userGroup = "user_group"; + inherit (config.test) subdomain domain; + + dataDir = "/var/lib/nextcloud"; + tracing = null; + defaultPhoneRegion = "US"; + + # This option is only needed because we do not access Nextcloud at the default port in the VM. + externalFqdn = "${config.test.fqdn}:8080"; + + adminUser = adminUser; + adminPass.result = config.shb.hardcodedsecret.adminPass.result; + debug = false; # Enable this if needed, but beware it is _very_ verbose. + }; + + shb.hardcodedsecret.adminPass = { + request = config.shb.nextcloud.adminPass.request; + settings.content = adminPass; }; }; - shb.hardcodedsecret.nextcloudLdapUserPassword = { - request = config.shb.nextcloud.apps.ldap.adminPassword.request; - settings = config.shb.hardcodedsecret.ldapUserPassword.settings; - }; - }; - sso = { config, ... }: + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "n"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + usernameFieldLabelRegex = "Account name"; + passwordFieldLabelRegex = "^ *[Pp]assword"; + loginButtonNameRegex = "[Ll]og [Ii]n"; + testLoginWith = [ + { + username = adminUser; + password = adminPass; + nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; + } + # Failure is after so we're not throttled too much. + { + username = adminUser; + password = adminPass + "oops"; + nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).to_be_visible()" + ]; + } + ]; + }; + }; + + clientLdapLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "n"; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + usernameFieldLabelRegex = "Account name"; + passwordFieldLabelRegex = "^ *[Pp]assword"; + loginButtonNameRegex = "[Ll]og [Ii]n"; + testLoginWith = [ + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; + } + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).to_be_visible()" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()" + "expect(page).to_have_title(re.compile('Dashboard'))" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page.get_by_text('Wrong login or password')).to_be_visible()" + ]; + } + ]; + }; + }; + + clientSsoLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "n"; + }; + + networking.hosts = { + "192.168.1.2" = [ "auth.example.com" ]; + }; + + test.login = { + startUrl = "http://${config.test.fqdn}"; + # No need since Nextcloud is auto-redirecting to the SSO sign in page. + # beforeHook = '' + # page.get_by_role("link", name="Sign in with SHB-Authelia").click() + # ''; + usernameFieldLabelRegex = "Username"; + passwordFieldSelector = "get_by_label(\"Password *\")"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page).to_have_title(re.compile('Dashboard'))" + "page.goto('https://${config.test.fqdn}/settings/admin')" + "expect(page.get_by_text('Access forbidden')).to_be_visible()" + ]; + } + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text('Incorrect username or password')).to_be_visible()" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page).to_have_title(re.compile('Dashboard'))" + "page.goto('https://${config.test.fqdn}/settings/admin')" + "expect(page.get_by_text('Access forbidden')).not_to_be_visible()" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page.get_by_text('Incorrect username or password')).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "NotCharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text('Incorrect username or password')).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "CharliePassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page.get_by_text('not member of the allowed groups')).to_be_visible()" + ]; + } + ]; + }; + }; + + https = + { config, ... }: + { + shb.nextcloud = { + ssl = config.shb.certs.certs.selfsigned.n; + + externalFqdn = lib.mkForce null; + }; + }; + + ldap = + { config, ... }: + { + shb.nextcloud = { + apps.ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.lldap.ldapPort; + dcdomain = config.shb.lldap.dcdomain; + adminName = "admin"; + adminPassword.result = config.shb.hardcodedsecret.nextcloudLdapUserPassword.result; + userGroup = "user_group"; + }; + }; + shb.hardcodedsecret.nextcloudLdapUserPassword = { + request = config.shb.nextcloud.apps.ldap.adminPassword.request; + settings = config.shb.hardcodedsecret.ldapUserPassword.settings; + }; + }; + + sso = + { config, ... }: { shb.nextcloud = { apps.ldap = { @@ -297,17 +369,19 @@ let request = config.shb.nextcloud.apps.sso.secretForAuthelia.request; settings.content = oidcSecret; }; - }; - - previewgenerator = { config, ...}: { - systemd.tmpfiles.rules = [ - "d '/srv/nextcloud' 0750 nextcloud nextcloud - -" - ]; - - shb.nextcloud = { - apps.previewgenerator.enable = true; }; - }; + + previewgenerator = + { config, ... }: + { + systemd.tmpfiles.rules = [ + "d '/srv/nextcloud' 0750 nextcloud nextcloud - -" + ]; + + shb.nextcloud = { + apps.previewgenerator.enable = true; + }; + }; externalstorage = { systemd.tmpfiles.rules = [ @@ -323,43 +397,50 @@ let }; }; - memories = { config, ...}: { - systemd.tmpfiles.rules = [ - "d '/srv/nextcloud' 0750 nextcloud nextcloud - -" - ]; + memories = + { config, ... }: + { + systemd.tmpfiles.rules = [ + "d '/srv/nextcloud' 0750 nextcloud nextcloud - -" + ]; - shb.nextcloud = { - apps.memories.enable = true; - apps.memories.vaapi = true; + shb.nextcloud = { + apps.memories.enable = true; + apps.memories.vaapi = true; + }; }; - }; - recognize = { config, ...}: { - systemd.tmpfiles.rules = [ - "d '/srv/nextcloud' 0750 nextcloud nextcloud - -" - ]; + recognize = + { config, ... }: + { + systemd.tmpfiles.rules = [ + "d '/srv/nextcloud' 0750 nextcloud nextcloud - -" + ]; - shb.nextcloud = { - apps.recognize.enable = true; + shb.nextcloud = { + apps.recognize.enable = true; + }; }; - }; - prometheus = { config, ... }: { - shb.nextcloud = { - phpFpmPrometheusExporter.enable = true; + prometheus = + { config, ... }: + { + shb.nextcloud = { + phpFpmPrometheusExporter.enable = true; + }; }; - }; - prometheusTestScript = { nodes, ... }: + prometheusTestScript = + { nodes, ... }: '' - server.wait_for_open_unix_socket("${nodes.server.services.phpfpm.pools.nextcloud.socket}") - server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.php-fpm.port}) - with subtest("prometheus"): - response = server.succeed( - "curl -sSf " - + " http://localhost:${toString nodes.server.services.prometheus.exporters.php-fpm.port}/metrics" - ) - print(response) + server.wait_for_open_unix_socket("${nodes.server.services.phpfpm.pools.nextcloud.socket}") + server.wait_for_open_port(${toString nodes.server.services.prometheus.exporters.php-fpm.port}) + with subtest("prometheus"): + response = server.succeed( + "curl -sSf " + + " http://localhost:${toString nodes.server.services.prometheus.exporters.php-fpm.port}/metrics" + ) + print(response) ''; in { @@ -389,44 +470,53 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access.override { - extraScript = { node, fqdn, proto_fqdn, ... }: '' - import time + extraScript = + { + node, + fqdn, + proto_fqdn, + ... + }: + '' + import time - def find_in_logs(unit, text): - return server.systemctl("status {}".format(unit))[1].find(text) != -1 + def find_in_logs(unit, text): + return server.systemctl("status {}".format(unit))[1].find(text) != -1 - with subtest("cron job succeeds"): - # This call does not block until the service is done. - server.succeed("systemctl start nextcloud-cron.service&") + with subtest("cron job succeeds"): + # This call does not block until the service is done. + server.succeed("systemctl start nextcloud-cron.service&") - # If the service failed, then we're not happy. - status = "active" - while status == "active": - status = server.get_unit_info("nextcloud-cron")["ActiveState"] - time.sleep(5) - if status != "inactive": - raise Exception("Cron job did not finish correctly") + # If the service failed, then we're not happy. + status = "active" + while status == "active": + status = server.get_unit_info("nextcloud-cron")["ActiveState"] + time.sleep(5) + if status != "inactive": + raise Exception("Cron job did not finish correctly") - if not find_in_logs("nextcloud-cron", "nextcloud-cron.service: Deactivated successfully."): - raise Exception("Nextcloud cron job did not finish successfully.") - ''; + if not find_in_logs("nextcloud-cron", "nextcloud-cron.service: Deactivated successfully."): + raise Exception("Nextcloud cron job did not finish successfully.") + ''; }; }; backup = lib.shb.runNixOSTest { name = "nextcloud_backup"; - nodes.server = { config, ... }: { - imports = [ - basic - (lib.shb.backup config.shb.nextcloud.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (lib.shb.backup config.shb.nextcloud.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -442,7 +532,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; # TODO: Test login testScript = commonTestScript.access; @@ -460,7 +550,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; @@ -477,7 +567,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; @@ -514,77 +604,89 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; ldap = lib.shb.runNixOSTest { name = "nextcloud_ldap"; - - nodes.server = { config, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - ldap - ]; - }; - + + nodes.server = + { config, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + ldap + ]; + }; + nodes.client = { imports = [ clientLdapLogin ]; }; - + testScript = commonTestScript.access; }; sso = lib.shb.runNixOSTest { name = "nextcloud_sso"; - nodes.server = { config, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ({ config, ... }: { - networking.hosts = { - "127.0.0.1" = [ config.test.fqdn ]; - }; - }) - ]; - }; - + nodes.server = + { config, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ( + { config, ... }: + { + networking.hosts = { + "127.0.0.1" = [ config.test.fqdn ]; + }; + } + ) + ]; + }; + nodes.client = { imports = [ clientSsoLogin - ({ config, ... }: { - networking.hosts = { - "192.168.1.2" = [ config.test.fqdn ]; - }; - }) + ( + { config, ... }: + { + networking.hosts = { + "192.168.1.2" = [ config.test.fqdn ]; + }; + } + ) ]; }; - + testScript = commonTestScript.access; }; prometheus = lib.shb.runNixOSTest { name = "nextcloud_prometheus"; - nodes.server = { config, ... }: { - imports = [ - basic - prometheus - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + prometheus + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = prometheusTestScript; }; diff --git a/test/services/open-webui.nix b/test/services/open-webui.nix index 07f3fa0..f40dd4e 100644 --- a/test/services/open-webui.nix +++ b/test/services/open-webui.nix @@ -4,131 +4,169 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.open-webui.ssl); - waitForServices = { ... }: [ - "open-webui.service" - "nginx.service" - ]; - waitForPorts = { node, ... }: [ - node.config.shb.open-webui.port - ]; - }; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/blocks/hardcodedsecret.nix - ../../modules/services/open-webui.nix - ]; - - test = { - subdomain = "o"; - }; - - shb.open-webui = { - enable = true; - inherit (config.test) subdomain domain; - }; - - networking.hosts = { - "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; - }; - }; - - https = { config, ... }: { - shb.open-webui = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - - systemd.services.open-webui.environment = { - # Needed for open-webui to be able to talk to auth server. - SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt"; - }; - }; - - ldap = { config, ... }: { - shb.open-webui = { - ldap = { - userGroup = "user_group"; - adminGroup = "admin_group"; - }; - }; - }; - - clientLoginSso = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - virtualisation.memorySize = 4096; - test = { - subdomain = "o"; - }; - - test.login = { - startUrl = "https://${config.test.fqdn}/auth"; - beforeHook = '' - page.get_by_role("button", name="continue").click() - ''; - usernameFieldLabelRegex = "Username"; - passwordFieldLabelRegex = "Password"; - loginButtonNameRegex = "[sS]ign [iI]n"; - testLoginWith = [ - { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" - ]; } - { username = "alice"; password = "AlicePassword"; nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Accept')).click()" - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page.get_by_text('logged in')).to_be_visible()" - ]; } - { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" - ]; } - { username = "bob"; password = "BobPassword"; nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Accept')).click()" - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page.get_by_text('logged in')).to_be_visible()" - ]; } - { username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" - ]; } - { username = "charlie"; password = "CharliePassword"; nextPageExpect = [ - "page.get_by_role('button', name=re.compile('Accept')).click()" - "expect(page.get_by_text('unauthorized')).to_be_visible()" - ]; } + waitForServices = + { ... }: + [ + "open-webui.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + node.config.shb.open-webui.port ]; - }; }; - sso = { config, ... }: { - shb.open-webui = { - sso = { - enable = true; - authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - clientID = "open-webui"; + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/open-webui.nix + ]; - sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result; - sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result; + test = { + subdomain = "o"; + }; + + shb.open-webui = { + enable = true; + inherit (config.test) subdomain domain; + }; + + networking.hosts = { + "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; }; }; - shb.hardcodedsecret.oidcSecret = { - request = config.shb.open-webui.sso.sharedSecret.request; - settings.content = oidcSecret; + https = + { config, ... }: + { + shb.open-webui = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + + systemd.services.open-webui.environment = { + # Needed for open-webui to be able to talk to auth server. + SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt"; + }; }; - shb.hardcodedsecret.oidcAutheliaSecret = { - request = config.shb.open-webui.sso.sharedSecretForAuthelia.request; - settings.content = oidcSecret; + + ldap = + { config, ... }: + { + shb.open-webui = { + ldap = { + userGroup = "user_group"; + adminGroup = "admin_group"; + }; + }; + }; + + clientLoginSso = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + virtualisation.memorySize = 4096; + test = { + subdomain = "o"; + }; + + test.login = { + startUrl = "https://${config.test.fqdn}/auth"; + beforeHook = '' + page.get_by_role("button", name="continue").click() + ''; + usernameFieldLabelRegex = "Username"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('logged in')).to_be_visible()" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('logged in')).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "NotCharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "CharliePassword"; + nextPageExpect = [ + "page.get_by_role('button', name=re.compile('Accept')).click()" + "expect(page.get_by_text('unauthorized')).to_be_visible()" + ]; + } + ]; + }; + }; + + sso = + { config, ... }: + { + shb.open-webui = { + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + clientID = "open-webui"; + + sharedSecret.result = config.shb.hardcodedsecret.oidcSecret.result; + sharedSecretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result; + }; + }; + + shb.hardcodedsecret.oidcSecret = { + request = config.shb.open-webui.sso.sharedSecret.request; + settings.content = oidcSecret; + }; + shb.hardcodedsecret.oidcAutheliaSecret = { + request = config.shb.open-webui.sso.sharedSecretForAuthelia.request; + settings.content = oidcSecret; + }; }; - }; in { basic = lib.shb.runNixOSTest { name = "open-webui_basic"; - nodes.client = {}; + nodes.client = { }; nodes.server = { imports = [ basic @@ -141,14 +179,16 @@ in backup = lib.shb.runNixOSTest { name = "open-webui_backup"; - nodes.server = { config, ... }: { - imports = [ - basic - (lib.shb.backup config.shb.open-webui.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (lib.shb.backup config.shb.open-webui.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -156,7 +196,7 @@ in https = lib.shb.runNixOSTest { name = "open-webui_https"; - nodes.client = {}; + nodes.client = { }; nodes.server = { imports = [ basic @@ -176,17 +216,19 @@ in clientLoginSso ]; }; - nodes.server = { config, pkgs, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ]; - }; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; testScript = commonTestScript.access; }; diff --git a/test/services/pinchflat.nix b/test/services/pinchflat.nix index 5f3ea46..2aeb9c0 100644 --- a/test/services/pinchflat.nix +++ b/test/services/pinchflat.nix @@ -2,134 +2,178 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.pinchflat.ssl); - waitForServices = { ... }: [ - "pinchflat.service" - "nginx.service" - ]; - waitForPorts = { node, ... }: [ - node.config.shb.pinchflat.port - ]; - }; - - basic = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/blocks/hardcodedsecret.nix - ../../modules/services/pinchflat.nix - ]; - - test = { - subdomain = "p"; - }; - - shb.pinchflat = { - enable = true; - inherit (config.test) subdomain domain; - mediaDir = "/src/pinchflat"; - timeZone = "America/Los_Angeles"; - secretKeyBase.result = config.shb.hardcodedsecret.secretKeyBase.result; - }; - - systemd.tmpfiles.rules = [ - "d '/src/pinchflat' 0750 pinchflat pinchflat - -" - ]; - - # Needed for gitea-runner-local to be able to ping pinchflat. - networking.hosts = { - "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; - }; - - shb.hardcodedsecret.secretKeyBase = { - request = config.shb.pinchflat.secretKeyBase.request; - settings.content = pkgs.lib.strings.replicate 64 "Z"; - }; - }; - - clientLogin = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - test = { - subdomain = "p"; - }; - - test.login = { - startUrl = "http://${config.test.fqdn}"; - # There is no login without SSO integration. - testLoginWith = [ - { username = null; password = null; nextPageExpect = [ - "expect(page.get_by_text('Create a media profile')).to_be_visible()" - ]; } + waitForServices = + { ... }: + [ + "pinchflat.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + node.config.shb.pinchflat.port ]; - }; }; - https = { config, ... }: { - shb.pinchflat = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; + basic = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/pinchflat.nix + ]; - ldap = { config, ... }: { - shb.pinchflat = { - ldap = { + test = { + subdomain = "p"; + }; + + shb.pinchflat = { enable = true; + inherit (config.test) subdomain domain; + mediaDir = "/src/pinchflat"; + timeZone = "America/Los_Angeles"; + secretKeyBase.result = config.shb.hardcodedsecret.secretKeyBase.result; + }; - userGroup = "user_group"; + systemd.tmpfiles.rules = [ + "d '/src/pinchflat' 0750 pinchflat pinchflat - -" + ]; + + # Needed for gitea-runner-local to be able to ping pinchflat. + networking.hosts = { + "127.0.0.1" = [ "${config.test.subdomain}.${config.test.domain}" ]; + }; + + shb.hardcodedsecret.secretKeyBase = { + request = config.shb.pinchflat.secretKeyBase.request; + settings.content = pkgs.lib.strings.replicate 64 "Z"; }; }; - }; - clientLoginSso = { config, ... }: { - imports = [ - lib.shb.baseModule - lib.shb.clientLoginModule - ]; - test = { - subdomain = "p"; - }; - - test.login = { - startUrl = "https://${config.test.fqdn}"; - usernameFieldLabelRegex = "Username"; - passwordFieldLabelRegex = "Password"; - loginButtonNameRegex = "[sS]ign [iI]n"; - testLoginWith = [ - { username = "alice"; password = "NotAlicePassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" - ]; } - { username = "alice"; password = "AlicePassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page.get_by_text('Create a media profile')).to_be_visible()" - ]; } - { username = "bob"; password = "NotBobPassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" - ]; } - { username = "bob"; password = "BobPassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" - "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" - "expect(page.get_by_text('Create a media profile')).to_be_visible()" - ]; } - { username = "charlie"; password = "NotCharliePassword"; nextPageExpect = [ - "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" - ]; } - { username = "charlie"; password = "CharliePassword"; nextPageExpect = [ - "expect(page).to_have_url(re.compile('.*/authenticated'))" - ]; } + clientLogin = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule ]; - }; - }; + test = { + subdomain = "p"; + }; - sso = { config, ... }: { - shb.pinchflat = { - sso = { - enable = true; - authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + test.login = { + startUrl = "http://${config.test.fqdn}"; + # There is no login without SSO integration. + testLoginWith = [ + { + username = null; + password = null; + nextPageExpect = [ + "expect(page.get_by_text('Create a media profile')).to_be_visible()" + ]; + } + ]; + }; + }; + + https = + { config, ... }: + { + shb.pinchflat = { + ssl = config.shb.certs.certs.selfsigned.n; + }; + }; + + ldap = + { config, ... }: + { + shb.pinchflat = { + ldap = { + enable = true; + + userGroup = "user_group"; + }; + }; + }; + + clientLoginSso = + { config, ... }: + { + imports = [ + lib.shb.baseModule + lib.shb.clientLoginModule + ]; + test = { + subdomain = "p"; + }; + + test.login = { + startUrl = "https://${config.test.fqdn}"; + usernameFieldLabelRegex = "Username"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[sS]ign [iI]n"; + testLoginWith = [ + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('Create a media profile')).to_be_visible()" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).not_to_be_visible()" + "expect(page.get_by_role('button', name=re.compile('Sign In'))).not_to_be_visible()" + "expect(page.get_by_text('Create a media profile')).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "NotCharliePassword"; + nextPageExpect = [ + "expect(page.get_by_text(re.compile('[Ii]ncorrect'))).to_be_visible()" + ]; + } + { + username = "charlie"; + password = "CharliePassword"; + nextPageExpect = [ + "expect(page).to_have_url(re.compile('.*/authenticated'))" + ]; + } + ]; + }; + }; + + sso = + { config, ... }: + { + shb.pinchflat = { + sso = { + enable = true; + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; }; }; - }; in { basic = lib.shb.runNixOSTest { @@ -152,14 +196,16 @@ in backup = lib.shb.runNixOSTest { name = "pinchflat_backup"; - nodes.server = { config, ... }: { - imports = [ - basic - (lib.shb.backup config.shb.pinchflat.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + basic + (lib.shb.backup config.shb.pinchflat.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; }; @@ -191,17 +237,19 @@ in clientLoginSso ]; }; - nodes.server = { config, pkgs, ... }: { - imports = [ - basic - lib.shb.certs - https - lib.shb.ldap - ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ]; - }; + nodes.server = + { config, pkgs, ... }: + { + imports = [ + basic + lib.shb.certs + https + lib.shb.ldap + ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; testScript = commonTestScript.access.override { redirectSSO = true; diff --git a/test/services/vaultwarden.nix b/test/services/vaultwarden.nix index 759715d..4dc37a2 100644 --- a/test/services/vaultwarden.nix +++ b/test/services/vaultwarden.nix @@ -2,14 +2,18 @@ let commonTestScript = lib.shb.mkScripts { hasSSL = { node, ... }: !(isNull node.config.shb.vaultwarden.ssl); - waitForServices = { ... }: [ - "vaultwarden.service" - "nginx.service" - ]; - waitForPorts = { node, ... }: [ - 8222 - 5432 - ]; + waitForServices = + { ... }: + [ + "vaultwarden.service" + "nginx.service" + ]; + waitForPorts = + { node, ... }: + [ + 8222 + 5432 + ]; # to get the get token test to succeed we need: # 1. add group Vaultwarden_admin to LLDAP # 2. add an Authelia user with to that group @@ -17,58 +21,64 @@ let # 4. go to the Vaultwarden /admin endpoint # 5. create a Vaultwarden user # 6. now login with that new user to Vaultwarden - extraScript = { node, proto_fqdn, ... }: '' - with subtest("prelogin"): - response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """ - {"email": "me@example.com"} - """)) - print(response) - if 'kdf' not in response: - raise Exception("Unrecognized response: {}".format(response)) + extraScript = + { node, proto_fqdn, ... }: + '' + with subtest("prelogin"): + response = curl(client, "", "${proto_fqdn}/identity/accounts/prelogin", data=unline_with("", """ + {"email": "me@example.com"} + """)) + print(response) + if 'kdf' not in response: + raise Exception("Unrecognized response: {}".format(response)) - with subtest("get token"): - response = curl(client, "", "${proto_fqdn}/identity/connect/token", data=unline_with("", """ - scope=api%20offline_access - &client_id=web - &deviceType=10 - &deviceIdentifier=a60323bf-4686-4b4d-96e0-3c241fa5581c - &deviceName=firefox - &grant_type=password&username=me - &password=mypassword - """)) - print(response) - if response["message"] != "Username or password is incorrect. Try again": - raise Exception("Unrecognized response: {}".format(response)) - ''; + with subtest("get token"): + response = curl(client, "", "${proto_fqdn}/identity/connect/token", data=unline_with("", """ + scope=api%20offline_access + &client_id=web + &deviceType=10 + &deviceIdentifier=a60323bf-4686-4b4d-96e0-3c241fa5581c + &deviceName=firefox + &grant_type=password&username=me + &password=mypassword + """)) + print(response) + if response["message"] != "Username or password is incorrect. Try again": + raise Exception("Unrecognized response: {}".format(response)) + ''; }; - basic = { config, ... }: { - test = { - subdomain = "v"; + basic = + { config, ... }: + { + test = { + subdomain = "v"; + }; + + shb.vaultwarden = { + enable = true; + inherit (config.test) subdomain domain; + + port = 8222; + databasePassword.result = config.shb.hardcodedsecret.passphrase.result; + }; + shb.hardcodedsecret.passphrase = { + request = config.shb.vaultwarden.databasePassword.request; + settings.content = "PassPhrase"; + }; + + # networking.hosts = { + # "127.0.0.1" = [ fqdn ]; + # }; }; - shb.vaultwarden = { - enable = true; - inherit (config.test) subdomain domain; - - port = 8222; - databasePassword.result = config.shb.hardcodedsecret.passphrase.result; + https = + { config, ... }: + { + shb.vaultwarden = { + ssl = config.shb.certs.certs.selfsigned.n; + }; }; - shb.hardcodedsecret.passphrase = { - request = config.shb.vaultwarden.databasePassword.request; - settings.content = "PassPhrase"; - }; - - # networking.hosts = { - # "127.0.0.1" = [ fqdn ]; - # }; - }; - - https = { config, ... }: { - shb.vaultwarden = { - ssl = config.shb.certs.certs.selfsigned.n; - }; - }; # Not yet supported # ldap = { config, ... }: { @@ -78,11 +88,13 @@ let # # }; # }; - sso = { config, ... }: { - shb.vaultwarden = { - authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + sso = + { config, ... }: + { + shb.vaultwarden = { + authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + }; }; - }; in { basic = lib.shb.runNixOSTest { @@ -97,7 +109,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; @@ -116,7 +128,7 @@ in ]; }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access; }; @@ -126,7 +138,7 @@ in # ldap = lib.shb.runNixOSTest { # name = "vaultwarden_ldap"; # - # nodes.server = lib.mkMerge [ + # nodes.server = lib.mkMerge [ # lib.shb.baseModule # ../../modules/blocks/hardcodedsecret.nix # ../../modules/services/vaultwarden.nix @@ -142,56 +154,64 @@ in sso = lib.shb.runNixOSTest { name = "vaultwarden_sso"; - nodes.server = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/blocks/hardcodedsecret.nix - ../../modules/services/vaultwarden.nix - lib.shb.certs - basic - https - lib.shb.ldap - (lib.shb.sso config.shb.certs.certs.selfsigned.n) - sso - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/vaultwarden.nix + lib.shb.certs + basic + https + lib.shb.ldap + (lib.shb.sso config.shb.certs.certs.selfsigned.n) + sso + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.access.override { - waitForPorts = { node, ... }: [ - 8222 - 5432 - 9091 - ]; - extraScript = { node, proto_fqdn, ... }: '' - with subtest("unauthenticated access is not granted to /admin"): - response = curl(client, """{"code":%{response_code},"auth_host":"%{urle.host}","auth_query":"%{urle.query}","all":%{json}}""", "${proto_fqdn}/admin") + waitForPorts = + { node, ... }: + [ + 8222 + 5432 + 9091 + ]; + extraScript = + { node, proto_fqdn, ... }: + '' + with subtest("unauthenticated access is not granted to /admin"): + response = curl(client, """{"code":%{response_code},"auth_host":"%{urle.host}","auth_query":"%{urle.query}","all":%{json}}""", "${proto_fqdn}/admin") - if response['code'] != 200: - raise Exception(f"Code is {response['code']}") - if response['auth_host'] != "auth.${node.config.test.domain}": - raise Exception(f"auth host should be auth.${node.config.test.domain} but is {response['auth_host']}") - if response['auth_query'] != "rd=${proto_fqdn}/admin": - raise Exception(f"auth query should be rd=${proto_fqdn}/admin but is {response['auth_query']}") - ''; + if response['code'] != 200: + raise Exception(f"Code is {response['code']}") + if response['auth_host'] != "auth.${node.config.test.domain}": + raise Exception(f"auth host should be auth.${node.config.test.domain} but is {response['auth_host']}") + if response['auth_query'] != "rd=${proto_fqdn}/admin": + raise Exception(f"auth query should be rd=${proto_fqdn}/admin but is {response['auth_query']}") + ''; }; }; backup = lib.shb.runNixOSTest { name = "vaultwarden_backup"; - nodes.server = { config, ... }: { - imports = [ - lib.shb.baseModule - ../../modules/blocks/hardcodedsecret.nix - ../../modules/services/vaultwarden.nix - basic - (lib.shb.backup config.shb.vaultwarden.backup) - ]; - }; + nodes.server = + { config, ... }: + { + imports = [ + lib.shb.baseModule + ../../modules/blocks/hardcodedsecret.nix + ../../modules/services/vaultwarden.nix + basic + (lib.shb.backup config.shb.vaultwarden.backup) + ]; + }; - nodes.client = {}; + nodes.client = { }; testScript = commonTestScript.backup; };