Commit 42b6d626 authored by ibizaman's avatar ibizaman Committed by Herwig Hochleitner
Browse files

lldap: add options to set important secrets

parent 1bda23e1
Loading
Loading
Loading
Loading
+71 −0
Original line number Diff line number Diff line
@@ -102,12 +102,83 @@ in
            default = "sqlite://./users.db?mode=rwc";
            example = "postgres://postgres-user:password@postgres-server/my-database";
          };

          ldap_user_pass_file = mkOption {
            type = types.nullOr types.str;
            default = null;
            description = ''
              Path to a file containing the default admin password.

              If you want to update the default admin password through this setting,
              you must set `force_ldap_user_pass_reset` to `true`.
              Otherwise changing this setting will have no effect
              unless this is the very first time LLDAP is started and its database is still empty.
            '';
          };

          force_ldap_user_pass_reset = mkOption {
            type = types.oneOf [
              types.bool
              (types.enum [ "always" ])
            ];
            default = false;
            description = ''
              Force reset of the admin password.

              Set this setting to `"always"` to update the admin password when `ldap_user_pass_file` changes.
              Setting to `"always"` also means any password update in the UI will be overwritten next time the service restarts.

              The difference between `true` and `"always"` is the former is intended for a one time fix
              while the latter is intended for a declarative workflow. In practice, the result
              is the same: the password gets reset. The only practical difference is the former
              outputs a warning message while the latter outputs an info message.
            '';
          };

          jwt_secret_file = mkOption {
            type = types.nullOr types.str;
            default = null;
            description = ''
              Path to a file containing the JWT secret.
            '';
          };
        };
      };

      # TOML does not allow null values, so we use null to omit those fields
      apply = lib.filterAttrsRecursive (_: v: v != null);
    };

    silenceForceUserPassResetWarning = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Disable warning when the admin password is set declaratively with the `ldap_user_pass_file` setting
        but the `force_ldap_user_pass_reset` is set to `false`.

        This can lead to the admin password to drift from the one given declaratively.
        If that is okay for you and you want to silence the warning, set this option to `true`.
      '';
    };
  };

  config = lib.mkIf cfg.enable {
    warnings =
      lib.optionals
        (
          (cfg.settings.ldap_user_pass_file or null) != null
          && cfg.settings.force_ldap_user_pass_reset == false
          && cfg.silenceForceUserPassResetWarning == false
        )
        [
          ''
            lldap: The default admin password is declared with the setting `ldap_user_pass_file`, but `force_ldap_user_pass_reset` is set to `false`.
            This means the admin password can be changed through the UI and will drift from the one defined in your nix config.
            It also means changing the setting `ldap_user_pass_file` will have no effect on the admin password.
            Either set `force_ldap_user_pass_reset` to `"always"` or silence this warning by setting the option `services.lldap.silenceForceUserPassResetWarning` to `true`.
          ''
        ];

    systemd.services.lldap = {
      description = "Lightweight LDAP server (lldap)";
      wants = [ "network-online.target" ];
+63 −9
Original line number Diff line number Diff line
{ ... }:
let
  adminPassword = "mySecretPassword";
in
{
  name = "lldap";

@@ -7,23 +10,74 @@
    {
      services.lldap = {
        enable = true;

        settings = {
          verbose = true;
          ldap_base_dn = "dc=example,dc=com";
        };
      };
      environment.systemPackages = [ pkgs.openldap ];

      specialisation = {
        differentAdminPassword.configuration =
          { ... }:
          {
            services.lldap.settings = {
              ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" adminPassword);
              force_ldap_user_pass_reset = "always";
            };
          };

        changeAdminPassword.configuration =
          { ... }:
          {
            services.lldap.settings = {
              ldap_user_pass_file = toString (pkgs.writeText "adminPasswordFile" "password");
              force_ldap_user_pass_reset = false;
            };
          };
      };
    };

  testScript = ''
  testScript =
    { nodes, ... }:
    let
      specializations = "${nodes.machine.system.build.toplevel}/specialisation";
    in
    ''
      machine.wait_for_unit("lldap.service")
      machine.wait_for_open_port(3890)
      machine.wait_for_open_port(17170)

      machine.succeed("curl --location --fail http://localhost:17170/")

    print(
      machine.succeed('ldapsearch -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w password')
    )
      adminPassword="${adminPassword}"

      def try_login(user, password, expect_success=True):
          cmd = f'ldapsearch -H ldap://localhost:3890 -D uid={user},ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w {password}'
          code, response = machine.execute(cmd)
          print(cmd)
          print(response)
          if expect_success:
              if code != 0:
                  raise Exception(f"Expected success, had failure {code}")
          else:
              if code == 0:
                  raise Exception("Expected failure, had success")
          return response

      with subtest("default admin password"):
          try_login("admin", "password",    expect_success=True)
          try_login("admin", adminPassword, expect_success=False)

      with subtest("different admin password"):
          machine.succeed('${specializations}/differentAdminPassword/bin/switch-to-configuration test')
          try_login("admin", "password",    expect_success=False)
          try_login("admin", adminPassword, expect_success=True)

      with subtest("change admin password has no effect"):
          machine.succeed('${specializations}/differentAdminPassword/bin/switch-to-configuration test')
          try_login("admin", "password",    expect_success=False)
          try_login("admin", adminPassword, expect_success=True)
    '';
}