Unverified Commit aa6cbcbf authored by oddlama's avatar oddlama
Browse files

nixos/kanidm: run nixfmt-rfc-style

parent 391d05ce
Loading
Loading
Loading
Loading
+429 −309
Original line number Diff line number Diff line
{ config, lib, options, pkgs, ... }:
{
  config,
  lib,
  options,
  pkgs,
  ...
}:
let
  cfg = config.services.kanidm;
  settingsFormat = pkgs.formats.toml { };
@@ -7,18 +13,29 @@ let
  serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings);
  clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings);
  unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings);
  certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ];
  certPaths = builtins.map builtins.dirOf [
    cfg.serverSettings.tls_chain
    cfg.serverSettings.tls_key
  ];

  # Merge bind mount paths and remove paths where a prefix is already mounted.
  # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is already in the mount
  # paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
  hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
  mergePaths = lib.foldl' (merged: newPath: let
  hasPrefixInList =
    list: newPath:
    lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
  mergePaths = lib.foldl' (
    merged: newPath:
    let
      # If the new path is a prefix to some existing path, we need to filter it out
      filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
      filteredPaths = lib.filter (
        p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)
      ) merged;
      # If a prefix of the new path is already in the list, do not add it
      filteredNew = lib.optional (!hasPrefixInList filteredPaths newPath) newPath;
    in filteredPaths ++ filteredNew) [];
    in
    filteredPaths ++ filteredNew
  ) [ ];

  defaultServiceConfig = {
    BindReadOnlyPaths = [
@@ -57,12 +74,16 @@ let
    RestrictRealtime = true;
    RestrictSUIDSGID = true;
    SystemCallArchitectures = "native";
    SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
    SystemCallFilter = [
      "@system-service"
      "~@privileged @resources @setuid @keyring"
    ];
    # Does not work well with the temporary root
    #UMask = "0066";
  };

  mkPresentOption = what:
  mkPresentOption =
    what:
    lib.mkOption {
      description = "Whether to ensure that this ${what} is present or absent.";
      type = lib.types.bool;
@@ -71,9 +92,9 @@ let

  filterPresent = lib.filterAttrs (_: v: v.present);

  provisionStateJson = pkgs.writeText "provision-state.json" (builtins.toJSON {
    inherit (cfg.provision) groups persons systems;
  });
  provisionStateJson = pkgs.writeText "provision-state.json" (
    builtins.toJSON { inherit (cfg.provision) groups persons systems; }
  );

  # Only recover the admin account if a password should explicitly be provisioned
  # for the account. Otherwise it is not needed for provisioning.
@@ -89,8 +110,9 @@ let
  # Recover the idm_admin account. If a password should explicitly be provisioned
  # for the account we set it, otherwise we generate a new one because it is required
  # for provisioning.
  recoverIdmAdmin = if cfg.provision.idmAdminPasswordFile != null
    then ''
  recoverIdmAdmin =
    if cfg.provision.idmAdminPasswordFile != null then
      ''
        KANIDM_IDM_ADMIN_PASSWORD=$(< ${cfg.provision.idmAdminPasswordFile})
        # We always reset the idm_admin account password if a desired password was specified.
        if ! KANIDM_RECOVER_ACCOUNT_PASSWORD=$KANIDM_IDM_ADMIN_PASSWORD ${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin --from-environment >/dev/null; then
@@ -98,7 +120,8 @@ let
          exit 1
        fi
      ''
    else ''
    else
      ''
        # Recover idm_admin account
        if ! recover_out=$(${cfg.package}/bin/kanidmd recover-account -c ${serverConfigFile} idm_admin -o json); then
          echo "$recover_out" >&2
@@ -142,14 +165,15 @@ let

  serverPort =
    # ipv6:
    if lib.hasInfix "]:" cfg.serverSettings.bindaddress
    then lib.last (lib.splitString "]:" cfg.serverSettings.bindaddress)
    if lib.hasInfix "]:" cfg.serverSettings.bindaddress then
      lib.last (lib.splitString "]:" cfg.serverSettings.bindaddress)
    else
    # ipv4:
      if lib.hasInfix "." cfg.serverSettings.bindaddress
      then lib.last (lib.splitString ":" cfg.serverSettings.bindaddress)
    if lib.hasInfix "." cfg.serverSettings.bindaddress then
      lib.last (lib.splitString ":" cfg.serverSettings.bindaddress)
    # default is 8443
      else "8443";
    else
      "8443";
in
{
  options.services.kanidm = {
@@ -213,12 +237,20 @@ in
          log_level = lib.mkOption {
            description = "Log level of the server.";
            default = "info";
            type = lib.types.enum [ "info" "debug" "trace" ];
            type = lib.types.enum [
              "info"
              "debug"
              "trace"
            ];
          };
          role = lib.mkOption {
            description = "The role of this server. This affects the replication relationship and thereby available features.";
            default = "WriteReplica";
            type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ];
            type = lib.types.enum [
              "WriteReplica"
              "WriteReplicaNoUI"
              "ReadOnlyReplica"
            ];
          };
          online_backup = {
            path = lib.mkOption {
@@ -348,7 +380,8 @@ in
      groups = lib.mkOption {
        description = "Provisioning of kanidm groups";
        default = { };
        type = lib.types.attrsOf (lib.types.submodule (groupSubmod: {
        type = lib.types.attrsOf (
          lib.types.submodule (groupSubmod: {
            options = {
              present = mkPresentOption "group";

@@ -359,16 +392,23 @@ in
                default = [ ];
              };
            };
          config.members = lib.concatLists (lib.flip lib.mapAttrsToList cfg.provision.persons (person: personCfg:
            lib.optional (personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups) person
          ));
        }));
            config.members = lib.concatLists (
              lib.flip lib.mapAttrsToList cfg.provision.persons (
                person: personCfg:
                lib.optional (
                  personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups
                ) person
              )
            );
          })
        );
      };

      persons = lib.mkOption {
        description = "Provisioning of kanidm persons";
        default = { };
        type = lib.types.attrsOf (lib.types.submodule {
        type = lib.types.attrsOf (
          lib.types.submodule {
            options = {
              present = mkPresentOption "person";

@@ -399,13 +439,15 @@ in
                default = [ ];
              };
            };
        });
          }
        );
      };

      systems.oauth2 = lib.mkOption {
        description = "Provisioning of oauth2 resource servers";
        default = { };
        type = lib.types.attrsOf (lib.types.submodule {
        type = lib.types.attrsOf (
          lib.types.submodule {
            options = {
              present = mkPresentOption "oauth2 resource server";

@@ -423,7 +465,8 @@ in

              originUrl = lib.mkOption {
                description = "The origin URL of the service. OAuth2 redirects will only be allowed to sites under this origin. Must end with a slash.";
              type = let
                type =
                  let
                    originStrType = lib.types.strMatching ".*://.*/$";
                  in
                  lib.types.either originStrType (lib.types.nonEmptyListOf originStrType);
@@ -504,14 +547,19 @@ in
                  See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
                '';
                default = { };
              type = lib.types.attrsOf (lib.types.submodule {
                type = lib.types.attrsOf (
                  lib.types.submodule {
                    options = {
                      joinType = lib.mkOption {
                        description = ''
                          Determines how multiple values are joined to create the claim value.
                          See [Claim Maps](https://kanidm.github.io/kanidm/master/integrations/oauth2.html#custom-claim-maps) for more information.
                        '';
                    type = lib.types.enum ["array" "csv" "ssv"];
                        type = lib.types.enum [
                          "array"
                          "csv"
                          "ssv"
                        ];
                        default = "array";
                      };

@@ -521,47 +569,58 @@ in
                        type = lib.types.attrsOf (lib.types.listOf lib.types.str);
                      };
                    };
              });
                  }
                );
              };
            };
        });
          }
        );
      };
    };
  };

  config = lib.mkIf (cfg.enableClient || cfg.enableServer || cfg.enablePam) {
    assertions = let
      entityList = type: attrs: lib.flip lib.mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; });
    assertions =
      let
        entityList =
          type: attrs: lib.flip lib.mapAttrsToList (filterPresent attrs) (name: _: { inherit type name; });
        entities =
          entityList "group" cfg.provision.groups
          ++ entityList "person" cfg.provision.persons
          ++ entityList "oauth2" cfg.provision.systems.oauth2;

        # Accumulate entities by name. Track corresponding entity types for later duplicate check.
      entitiesByName = lib.foldl' (acc: { type, name }:
        acc // {
          ${name} = (acc.${name} or []) ++ [type];
        }
        entitiesByName = lib.foldl' (
          acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; }
        ) { } entities;

      assertGroupsKnown = opt: groups: let
        assertGroupsKnown =
          opt: groups:
          let
            knownGroups = lib.attrNames (filterPresent cfg.provision.groups);
            unknownGroups = lib.subtractLists knownGroups groups;
      in {
          in
          {
            assertion = (cfg.enableServer && cfg.provision.enable) -> unknownGroups == [ ];
            message = "${opt} refers to unknown groups: ${toString unknownGroups}";
          };

      assertEntitiesKnown = opt: entities: let
        assertEntitiesKnown =
          opt: entities:
          let
            unknownEntities = lib.subtractLists (lib.attrNames entitiesByName) entities;
      in {
          in
          {
            assertion = (cfg.enableServer && cfg.provision.enable) -> unknownEntities == [ ];
            message = "${opt} refers to unknown entities: ${toString unknownEntities}";
          };
      in
      [
        {
          assertion = !cfg.enableServer || ((cfg.serverSettings.tls_chain or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_chain);
          assertion =
            !cfg.enableServer
            || ((cfg.serverSettings.tls_chain or null) == null)
            || (!lib.isStorePath cfg.serverSettings.tls_chain);
          message = ''
            <option>services.kanidm.serverSettings.tls_chain</option> points to
            a file in the Nix store. You should use a quoted absolute path to
@@ -569,7 +628,10 @@ in
          '';
        }
        {
          assertion = !cfg.enableServer || ((cfg.serverSettings.tls_key or null) == null) || (!lib.isStorePath cfg.serverSettings.tls_key);
          assertion =
            !cfg.enableServer
            || ((cfg.serverSettings.tls_key or null) == null)
            || (!lib.isStorePath cfg.serverSettings.tls_key);
          message = ''
            <option>services.kanidm.serverSettings.tls_key</option> points to
            a file in the Nix store. You should use a quoted absolute path to
@@ -591,8 +653,12 @@ in
          '';
        }
        {
          assertion = !cfg.enableServer || (cfg.serverSettings.domain == null
            -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI");
          assertion =
            !cfg.enableServer
            || (
              cfg.serverSettings.domain == null
              -> cfg.serverSettings.role == "WriteReplica" || cfg.serverSettings.role == "WriteReplicaNoUI"
            );
          message = ''
            <option>services.kanidm.serverSettings.domain</option> can only be set if this instance
            is not a ReadOnlyReplica. Otherwise the db would inherit it from
@@ -605,63 +671,96 @@ in
        }
        # If any secret is provisioned, the kanidm package must have some required patches applied to it
        {
          assertion = (cfg.provision.enable &&
            (cfg.provision.adminPasswordFile != null
          assertion =
            (
              cfg.provision.enable
              && (
                cfg.provision.adminPasswordFile != null
                || cfg.provision.idmAdminPasswordFile != null
              || lib.any (x: x.basicSecretFile != null) (lib.attrValues (filterPresent cfg.provision.systems.oauth2))
            )) -> cfg.package.enableSecretProvisioning;
                || lib.any (x: x.basicSecretFile != null) (
                  lib.attrValues (filterPresent cfg.provision.systems.oauth2)
                )
              )
            )
            -> cfg.package.enableSecretProvisioning;
          message = ''
            Specifying an admin account password or oauth2 basicSecretFile requires kanidm to be built with the secret provisioning patches.
            You may want to set `services.kanidm.package = pkgs.kanidm.withSecretProvisioning;`.
          '';
        }
        # Entity names must be globally unique:
        (let
        (
          let
            # Filter all names that occurred in more than one entity type.
            duplicateNames = lib.filterAttrs (_: v: builtins.length v > 1) entitiesByName;
        in {
          in
          {
            assertion = cfg.provision.enable -> duplicateNames == { };
            message = ''
              services.kanidm.provision requires all entity names (group, person, oauth2, ...) to be unique!
            ${lib.concatLines (lib.mapAttrsToList (name: xs: "  - '${name}' used as: ${toString xs}") duplicateNames)}'';
        })
              ${lib.concatLines (
                lib.mapAttrsToList (name: xs: "  - '${name}' used as: ${toString xs}") duplicateNames
              )}'';
          }
        )
      ]
      ++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.persons) (person: personCfg:
      ++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.persons) (
        person: personCfg:
        assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups
      )
      ++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.groups) (group: groupCfg:
      ++ lib.flip lib.mapAttrsToList (filterPresent cfg.provision.groups) (
        group: groupCfg:
        assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members
      )
      ++ lib.concatLists (lib.flip lib.mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
      ++ lib.concatLists (
        lib.flip lib.mapAttrsToList (filterPresent cfg.provision.systems.oauth2) (
          oauth2: oauth2Cfg:
          [
            (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (lib.attrNames oauth2Cfg.scopeMaps))
            (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (lib.attrNames oauth2Cfg.supplementaryScopeMaps))
            (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" (
              lib.attrNames oauth2Cfg.scopeMaps
            ))
            (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" (
              lib.attrNames oauth2Cfg.supplementaryScopeMaps
            ))
          ]
          ++ lib.concatLists (lib.flip lib.mapAttrsToList oauth2Cfg.claimMaps (claim: claimCfg: [
            (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (lib.attrNames claimCfg.valuesByGroup))
          ++ lib.concatLists (
            lib.flip lib.mapAttrsToList oauth2Cfg.claimMaps (
              claim: claimCfg: [
                (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" (
                  lib.attrNames claimCfg.valuesByGroup
                ))
                # At least one group must map to a value in each claim map
                {
              assertion = (cfg.provision.enable && cfg.enableServer) -> lib.any (xs: xs != []) (lib.attrValues claimCfg.valuesByGroup);
                  assertion =
                    (cfg.provision.enable && cfg.enableServer)
                    -> lib.any (xs: xs != [ ]) (lib.attrValues claimCfg.valuesByGroup);
                  message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group";
                }
                # Public clients cannot define a basic secret
                {
              assertion = (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
                  assertion =
                    (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null;
                  message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret";
                }
                # Public clients cannot disable PKCE
                {
              assertion = (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> !oauth2Cfg.allowInsecureClientDisablePkce;
                  assertion =
                    (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public)
                    -> !oauth2Cfg.allowInsecureClientDisablePkce;
                  message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE";
                }
                # Non-public clients cannot enable localhost redirects
                {
              assertion = (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) -> !oauth2Cfg.enableLocalhostRedirects;
                  assertion =
                    (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public)
                    -> !oauth2Cfg.enableLocalhostRedirects;
                  message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects";
                }
          ]))
      ));
              ]
            )
          )
        )
      );

    environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ];

@@ -679,9 +778,12 @@ in
      after = [ "network.target" ];
      serviceConfig = lib.mkMerge [
        # Merge paths and ignore existing prefixes needs to sidestep mkMerge
        (defaultServiceConfig // {
        (
          defaultServiceConfig
          // {
            BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
        })
          }
        )
        {
          StateDirectory = "kanidm";
          StateDirectoryMode = "0700";
@@ -704,7 +806,11 @@ in
          PrivateUsers = lib.mkForce false;
          # Port needs to be exposed to the host network
          PrivateNetwork = lib.mkForce false;
          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
          RestrictAddressFamilies = [
            "AF_INET"
            "AF_INET6"
            "AF_UNIX"
          ];
          TemporaryFileSystem = "/:ro";
        }
      ];
@@ -715,7 +821,10 @@ in
      description = "Kanidm PAM daemon";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];
      restartTriggers = [ unixConfigFile clientConfigFile ];
      restartTriggers = [
        unixConfigFile
        clientConfigFile
      ];
      serviceConfig = lib.mkMerge [
        defaultServiceConfig
        {
@@ -740,7 +849,11 @@ in
          ];
          # Needs to connect to kanidmd
          PrivateNetwork = lib.mkForce false;
          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
          RestrictAddressFamilies = [
            "AF_INET"
            "AF_INET6"
            "AF_UNIX"
          ];
          TemporaryFileSystem = "/:ro";
        }
      ];
@@ -750,9 +863,15 @@ in
    systemd.services.kanidm-unixd-tasks = lib.mkIf cfg.enablePam {
      description = "Kanidm PAM home management daemon";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" "kanidm-unixd.service" ];
      after = [
        "network.target"
        "kanidm-unixd.service"
      ];
      partOf = [ "kanidm-unixd.service" ];
      restartTriggers = [ unixConfigFile clientConfigFile ];
      restartTriggers = [
        unixConfigFile
        clientConfigFile
      ];
      serviceConfig = {
        ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks";

@@ -772,7 +891,12 @@ in
          "/run/kanidm-unixd:/var/run/kanidm-unixd"
        ];
        # CAP_DAC_OVERRIDE is needed to ignore ownership of unixd socket
        CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_FOWNER" "CAP_DAC_OVERRIDE" "CAP_DAC_READ_SEARCH" ];
        CapabilityBoundingSet = [
          "CAP_CHOWN"
          "CAP_FOWNER"
          "CAP_DAC_OVERRIDE"
          "CAP_DAC_READ_SEARCH"
        ];
        IPAddressDeny = "any";
        # Need access to users
        PrivateUsers = false;
@@ -787,15 +911,11 @@ in

    # These paths are hardcoded
    environment.etc = lib.mkMerge [
      (lib.mkIf cfg.enableServer {
        "kanidm/server.toml".source = serverConfigFile;
      })
      (lib.mkIf cfg.enableServer { "kanidm/server.toml".source = serverConfigFile; })
      (lib.mkIf options.services.kanidm.clientSettings.isDefined {
        "kanidm/config".source = clientConfigFile;
      })
      (lib.mkIf cfg.enablePam {
        "kanidm/unixd".source = unixConfigFile;
      })
      (lib.mkIf cfg.enablePam { "kanidm/unixd".source = unixConfigFile; })
    ];

    system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ];
@@ -804,12 +924,8 @@ in
    system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm";

    users.groups = lib.mkMerge [
      (lib.mkIf cfg.enableServer {
        kanidm = { };
      })
      (lib.mkIf cfg.enablePam {
        kanidm-unixd = { };
      })
      (lib.mkIf cfg.enableServer { kanidm = { }; })
      (lib.mkIf cfg.enablePam { kanidm-unixd = { }; })
    ];
    users.users = lib.mkMerge [
      (lib.mkIf cfg.enableServer {
@@ -830,6 +946,10 @@ in
    ];
  };

  meta.maintainers = with lib.maintainers; [ erictapen Flakebi oddlama ];
  meta.maintainers = with lib.maintainers; [
    erictapen
    Flakebi
    oddlama
  ];
  meta.buildDocsInSandbox = false;
}
+1 −1

File changed.

Contains only whitespace changes.