From 8184187c03a94f12f664b4e7e0e4b28f6b380c9c Mon Sep 17 00:00:00 2001
From: ibizaman <ibizapeanut@gmail.com>
Date: Fri, 24 May 2024 15:02:38 -0700
Subject: [PATCH] add vm test for vaultwarden

---
 flake.nix                        |   1 +
 modules/services/vaultwarden.nix |  23 ++--
 test/vm/vaultwarden.nix          | 207 +++++++++++++++++++++++++++++++
 3 files changed, 218 insertions(+), 13 deletions(-)
 create mode 100644 test/vm/vaultwarden.nix

diff --git a/flake.nix b/flake.nix
index cdb503b..7b43844 100644
--- a/flake.nix
+++ b/flake.nix
@@ -118,6 +118,7 @@
           // (vm_test "nextcloud" ./test/vm/nextcloud.nix)
           // (vm_test "postgresql" ./test/vm/postgresql.nix)
           // (vm_test "ssl" ./test/vm/ssl.nix)
+          // (vm_test "vaultwarden" ./test/vm/vaultwarden.nix)
           );
       }
   );
diff --git a/modules/services/vaultwarden.nix b/modules/services/vaultwarden.nix
index 8d0fab2..d8bfa09 100644
--- a/modules/services/vaultwarden.nix
+++ b/modules/services/vaultwarden.nix
@@ -36,26 +36,22 @@ in
       default = 8222;
     };
 
-    ldapEndpoint = lib.mkOption {
-      type = lib.types.str;
-      description = "Endpoint for LDAP authentication backend.";
-      example = "ldap.example.com";
-    };
-
     authEndpoint = lib.mkOption {
-      type = lib.types.str;
+      type = lib.types.nullOr lib.types.str;
       description = "OIDC endpoint for SSO";
+      default = null;
       example = "https://authelia.example.com";
     };
 
     databasePasswordFile = lib.mkOption {
-      type = lib.types.str;
+      type = lib.types.path;
       description = "File containing the password to connect to the postgresql database.";
     };
 
     smtp = lib.mkOption {
       description = "SMTP options.";
-      type = lib.types.submodule {
+      default = null;
+      type = lib.types.nullOr (lib.types.submodule {
         options = {
           from_address = lib.mkOption {
             type = lib.types.str;
@@ -95,7 +91,7 @@ in
             description = "File containing the password to connect to the SMTP host.";
           };
         };
-      };
+      });
     };
 
     backupConfig = lib.mkOption {
@@ -130,7 +126,7 @@ 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) {
         SMTP_FROM = cfg.smtp.from_address;
         SMTP_FROM_NAME = cfg.smtp.from_name;
         SMTP_HOST = cfg.smtp.host;
@@ -152,10 +148,11 @@ in
         userConfig = {
           DATABASE_URL.source = cfg.databasePasswordFile;
           DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden";
+        } // lib.optionalAttrs (cfg.smtp != null) {
           SMTP_PASSWORD.source = cfg.smtp.passwordFile;
         };
         resultPath = "/var/lib/bitwarden_rs/vaultwarden.env";
-        generator = name: v: lib.generators.toINIWithGlobalSection {} { globalSection = v; };
+        generator = name: v: pkgs.writeText "template" (lib.generators.toINIWithGlobalSection {} { globalSection = v; });
       };
 
     shb.nginx.vhosts = [
@@ -186,7 +183,7 @@ in
       {
         username = "vaultwarden";
         database = "vaultwarden";
-        passwordFile = cfg.databasePasswordFile;
+        passwordFile = builtins.toString cfg.databasePasswordFile;
       }
     ];
 
diff --git a/test/vm/vaultwarden.nix b/test/vm/vaultwarden.nix
new file mode 100644
index 0000000..bbc9f87
--- /dev/null
+++ b/test/vm/vaultwarden.nix
@@ -0,0 +1,207 @@
+{ pkgs, lib, ... }:
+let
+  pkgs' = pkgs;
+
+  subdomain = "v";
+  domain = "example.com";
+  fqdn = "${subdomain}.${domain}";
+
+  # TODO: Test login
+  commonTestScript = { nodes, ... }:
+    let
+      hasSSL = !(isNull nodes.server.shb.vaultwarden.ssl);
+      proto_fqdn = if hasSSL then "https://${fqdn}" else "http://${fqdn}";
+    in
+    ''
+    import json
+    import os
+    import pathlib
+
+    start_all()
+    server.wait_for_unit("vaultwarden.service")
+    server.wait_for_unit("nginx.service")
+    server.wait_for_open_port(8222)
+    server.wait_for_open_port(5432)
+
+    if ${if hasSSL 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")
+
+    def curl(target, format, endpoint, succeed=True):
+        return json.loads(target.succeed(
+            "curl --fail-with-body --silent --show-error --output /dev/null --location"
+            + " --connect-to ${fqdn}:443:server:443"
+            + " --connect-to ${fqdn}:80:server:80"
+            + f" --write-out '{format}'"
+            + " " + endpoint
+        ))
+
+    with subtest("access"):
+        response = curl(client, """{"code":%{response_code}}""", "${proto_fqdn}")
+
+        if response['code'] != 200:
+            raise Exception(f"Code is {response['code']}")
+    '';
+
+  base = { config, ... }: {
+    imports = [
+      (pkgs'.path + "/nixos/modules/profiles/headless.nix")
+      (pkgs'.path + "/nixos/modules/profiles/qemu-guest.nix")
+      {
+        options = {
+          shb.backup = lib.mkOption { type = lib.types.anything; };
+        };
+      }
+      ../../modules/blocks/nginx.nix
+      ../../modules/blocks/postgresql.nix
+      ../../modules/blocks/ssl.nix
+      ../../modules/services/vaultwarden.nix
+    ];
+
+    # Nginx port.
+    networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+    shb.certs = {
+      cas.selfsigned.myca = {
+        name = "My CA";
+      };
+      certs.selfsigned = {
+        n = {
+          ca = config.shb.certs.cas.selfsigned.myca;
+          domain = "*.${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 ];
+  };
+
+  basic = { config, ... }: {
+    shb.vaultwarden = {
+      enable = true;
+      inherit subdomain domain;
+      ssl = config.shb.certs.certs.selfsigned.n;
+      port = 8222;
+      databasePasswordFile = pkgs.writeText "pwfile" "DBPASSWORDFILE";
+    };
+
+    networking.hosts = {
+      "127.0.0.1" = [ fqdn ];
+    };
+  };
+
+  ldap = { config, ... }: {
+    imports = [
+      ../../modules/blocks/ldap.nix
+    ];
+
+    shb.ldap = {
+      enable = true;
+      inherit domain;
+      subdomain = "ldap";
+      ldapPort = 3890;
+      webUIListenPort = 17170;
+      dcdomain = "dc=example,dc=com";
+      ldapUserPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+      jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
+    };
+
+    networking.hosts = {
+      "127.0.0.1" = [ "${config.shb.ldap.subdomain}.${domain}" ];
+    };
+
+    # Not yet supported
+    # shb.vaultwarden = {
+    #   ldapEndpoint = "http://127.0.0.1:${builtins.toString config.shb.ldap.webUIListenPort}";
+    # };
+  };
+
+  sso = { config, ... }: {
+    imports = [
+      ../../modules/blocks/authelia.nix
+    ];
+
+    shb.authelia = {
+      enable = true;
+      inherit domain;
+      subdomain = "auth";
+      ssl = config.shb.certs.certs.selfsigned.n;
+
+      ldapEndpoint = "ldap://127.0.0.1:${builtins.toString config.shb.ldap.ldapPort}";
+      dcdomain = config.shb.ldap.dcdomain;
+
+      secrets = {
+        jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
+        ldapAdminPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
+        sessionSecretFile = pkgs.writeText "sessionSecret" "sessionSecret";
+        storageEncryptionKeyFile = pkgs.writeText "storageEncryptionKey" "storageEncryptionKey";
+        identityProvidersOIDCHMACSecretFile = pkgs.writeText "identityProvidersOIDCHMACSecret" "identityProvidersOIDCHMACSecret";
+        identityProvidersOIDCIssuerPrivateKeyFile = (pkgs.runCommand "gen-private-key" {} ''
+          mkdir $out
+          ${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
+        '') + "/private.pem";
+      };
+    };
+
+    networking.hosts = {
+      "127.0.0.1" = [ "${config.shb.authelia.subdomain}.${domain}" ];
+    };
+
+    shb.vaultwarden = {
+      authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
+    };
+  };
+in
+{
+  basic = pkgs.testers.runNixOSTest {
+    name = "vaultwarden_basic";
+
+    nodes.server = lib.mkMerge [
+      base
+      basic
+      {
+        options = {
+          shb.authelia = lib.mkOption { type = lib.types.anything; };
+        };
+      }
+    ];
+
+    nodes.client = {};
+
+    testScript = commonTestScript;
+  };
+
+  # Not yet supported
+  #
+  # ldap = pkgs.testers.runNixOSTest {
+  #   name = "vaultwarden_ldap";
+  #
+  #   nodes.server = lib.mkMerge [ 
+  #     base
+  #     basic
+  #     ldap
+  #   ];
+  #
+  #   nodes.client = {};
+  #
+  #   testScript = commonTestScript;
+  # };
+
+  sso = pkgs.testers.runNixOSTest {
+    name = "vaultwarden_sso";
+
+    nodes.server = lib.mkMerge [ 
+      base
+      basic
+      ldap
+      sso
+    ];
+
+    nodes.client = {};
+
+    testScript = commonTestScript;
+  };
+}