Unverified Commit f26f92db authored by Philip Taron's avatar Philip Taron Committed by GitHub
Browse files

nixos/services.mosquitto: remove `with lib;` (#338022)

parents 03f36dc3 481df957
Loading
Loading
Loading
Loading
+91 −94
Original line number Diff line number Diff line
{ config, lib, pkgs, ...}:

with lib;

let
  cfg = config.services.mosquitto;

  # note that mosquitto config parsing is very simplistic as of may 2021.
  # often times they'll e.g. strtok() a line, check the first two tokens, and ignore the rest.
  # there's no escaping available either, so we have to prevent any being necessary.
  str = types.strMatching "[^\r\n]*" // {
  str = lib.types.strMatching "[^\r\n]*" // {
    description = "single-line string";
  };
  path = types.addCheck types.path (p: str.check "${p}");
  configKey = types.strMatching "[^\r\n\t ]+";
  optionType = with types; oneOf [ str path bool int ] // {
  path = lib.types.addCheck lib.types.path (p: str.check "${p}");
  configKey = lib.types.strMatching "[^\r\n\t ]+";
  optionType = with lib.types; oneOf [ str path bool int ] // {
    description = "string, path, bool, or integer";
  };
  optionToString = v:
    if isBool v then boolToString v
    if lib.isBool v then lib.boolToString v
    else if path.check v then "${v}"
    else toString v;

  assertKeysValid = prefix: valid: config:
    mapAttrsToList
    lib.mapAttrsToList
      (n: _: {
        assertion = valid ? ${n};
        message = "Invalid config key ${prefix}.${n}.";
      })
      config;

  formatFreeform = { prefix ? "" }: mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");
  formatFreeform = { prefix ? "" }: lib.mapAttrsToList (n: v: "${prefix}${n} ${optionToString v}");

  userOptions = with types; submodule {
  userOptions = with lib.types; submodule {
    options = {
      password = mkOption {
      password = lib.mkOption {
        type = uniq (nullOr str);
        default = null;
        description = ''
@@ -41,7 +38,7 @@ let
        '';
      };

      passwordFile = mkOption {
      passwordFile = lib.mkOption {
        type = uniq (nullOr path);
        example = "/path/to/file";
        default = null;
@@ -54,7 +51,7 @@ let
        '';
      };

      hashedPassword = mkOption {
      hashedPassword = lib.mkOption {
        type = uniq (nullOr str);
        default = null;
        description = ''
@@ -66,7 +63,7 @@ let
        '';
      };

      hashedPasswordFile = mkOption {
      hashedPasswordFile = lib.mkOption {
        type = uniq (nullOr path);
        example = "/path/to/file";
        default = null;
@@ -82,7 +79,7 @@ let
        '';
      };

      acl = mkOption {
      acl = lib.mkOption {
        type = listOf str;
        example = [ "read A/B" "readwrite A/#" ];
        default = [];
@@ -94,15 +91,15 @@ let
  };

  userAsserts = prefix: users:
    mapAttrsToList
    lib.mapAttrsToList
      (n: _: {
        assertion = builtins.match "[^:\r\n]+" n != null;
        message = "Invalid user name ${n} in ${prefix}";
      })
      users
    ++ mapAttrsToList
    ++ lib.mapAttrsToList
      (n: u: {
        assertion = count (s: s != null) [
        assertion = lib.count (s: s != null) [
          u.password u.passwordFile u.hashedPassword u.hashedPasswordFile
        ] <= 1;
        message = "Cannot set more than one password option for user ${n} in ${prefix}";
@@ -112,26 +109,26 @@ let
  userScope = prefix: index: "${prefix}-user-${toString index}";
  credentialID = prefix: credential: "${prefix}-${credential}";

  toScopedUsers = listenerScope: users: pipe users [
    attrNames
    (imap0 (index: user: nameValuePair user
  toScopedUsers = listenerScope: users: lib.pipe users [
    lib.attrNames
    (lib.imap0 (index: user: lib.nameValuePair user
      (users.${user} // { scope = userScope listenerScope index; })
    ))
    listToAttrs
    lib.listToAttrs
  ];

  userCredentials = user: credentials: pipe credentials [
    (filter (credential: user.${credential} != null))
  userCredentials = user: credentials: lib.pipe credentials [
    (lib.filter (credential: user.${credential} != null))
    (map (credential: "${credentialID user.scope credential}:${user.${credential}}"))
  ];
  usersCredentials = listenerScope: users: credentials: pipe users [
  usersCredentials = listenerScope: users: credentials: lib.pipe users [
    (toScopedUsers listenerScope)
    (mapAttrsToList (_: user: userCredentials user credentials))
    concatLists
    (lib.mapAttrsToList (_: user: userCredentials user credentials))
    lib.concatLists
  ];
  systemdCredentials = listeners: listenerCredentials: pipe listeners [
    (imap0 (index: listener: listenerCredentials (listenerScope index) listener))
    concatLists
  systemdCredentials = listeners: listenerCredentials: lib.pipe listeners [
    (lib.imap0 (index: listener: listenerCredentials (listenerScope index) listener))
    lib.concatLists
  ];

  makePasswordFile = listenerScope: users: path:
@@ -139,12 +136,12 @@ let
      makeLines = store: file: let
        scopedUsers = toScopedUsers listenerScope users;
      in
        mapAttrsToList
          (name: user: ''addLine ${escapeShellArg name} "''$(systemd-creds cat ${credentialID user.scope store})"'')
          (filterAttrs (_: user: user.${store} != null) scopedUsers)
        ++ mapAttrsToList
          (name: user: ''addFile ${escapeShellArg name} "''${CREDENTIALS_DIRECTORY}/${credentialID user.scope file}"'')
          (filterAttrs (_: user: user.${file} != null) scopedUsers);
        lib.mapAttrsToList
          (name: user: ''addLine ${lib.escapeShellArg name} "''$(systemd-creds cat ${credentialID user.scope store})"'')
          (lib.filterAttrs (_: user: user.${store} != null) scopedUsers)
        ++ lib.mapAttrsToList
          (name: user: ''addFile ${lib.escapeShellArg name} "''${CREDENTIALS_DIRECTORY}/${credentialID user.scope file}"'')
          (lib.filterAttrs (_: user: user.${file} != null) scopedUsers);
      plainLines = makeLines "password" "passwordFile";
      hashedLines = makeLines "hashedPassword" "hashedPasswordFile";
    in
@@ -154,7 +151,7 @@ let

          set -eu

          file=${escapeShellArg path}
          file=${lib.escapeShellArg path}

          rm -f "$file"
          touch "$file"
@@ -170,23 +167,23 @@ let
            echo "$1:$(cat "$2")" >> "$file"
          }
        ''
        + concatStringsSep "\n"
        + lib.concatStringsSep "\n"
          (plainLines
           ++ optional (plainLines != []) ''
           ++ lib.optional (plainLines != []) ''
             ${cfg.package}/bin/mosquitto_passwd -U "$file"
           ''
           ++ hashedLines));

  authPluginOptions = with types; submodule {
  authPluginOptions = with lib.types; submodule {
    options = {
      plugin = mkOption {
      plugin = lib.mkOption {
        type = path;
        description = ''
          Plugin path to load, should be a `.so` file.
        '';
      };

      denySpecialChars = mkOption {
      denySpecialChars = lib.mkOption {
        type = bool;
        description = ''
          Automatically disallow all clients using `#`
@@ -195,7 +192,7 @@ let
        default = true;
      };

      options = mkOption {
      options = lib.mkOption {
        type = attrsOf optionType;
        description = ''
          Options for the auth plugin. Each key turns into a `auth_opt_*`
@@ -207,7 +204,7 @@ let
  };

  authAsserts = prefix: auth:
    mapAttrsToList
    lib.mapAttrsToList
      (n: _: {
        assertion = configKey.check n;
        message = "Invalid auth plugin key ${prefix}.${n}";
@@ -253,9 +250,9 @@ let
    use_username_as_clientid = 1;
  };

  listenerOptions = with types; submodule {
  listenerOptions = with lib.types; submodule {
    options = {
      port = mkOption {
      port = lib.mkOption {
        type = port;
        description = ''
          Port to listen on. Must be set to 0 to listen on a unix domain socket.
@@ -263,7 +260,7 @@ let
        default = 1883;
      };

      address = mkOption {
      address = lib.mkOption {
        type = nullOr str;
        description = ''
          Address to listen on. Listen on `0.0.0.0`/`::`
@@ -272,7 +269,7 @@ let
        default = null;
      };

      authPlugins = mkOption {
      authPlugins = lib.mkOption {
        type = listOf authPluginOptions;
        description = ''
          Authentication plugin to attach to this listener.
@@ -282,7 +279,7 @@ let
        default = [];
      };

      users = mkOption {
      users = lib.mkOption {
        type = attrsOf userOptions;
        example = { john = { password = "123456"; acl = [ "readwrite john/#" ]; }; };
        description = ''
@@ -291,7 +288,7 @@ let
        default = {};
      };

      omitPasswordAuth = mkOption {
      omitPasswordAuth = lib.mkOption {
        type = bool;
        description = ''
          Omits password checking, allowing anyone to log in with any user name unless
@@ -300,7 +297,7 @@ let
        default = false;
      };

      acl = mkOption {
      acl = lib.mkOption {
        type = listOf str;
        description = ''
          Additional ACL items to prepend to the generated ACL file.
@@ -309,7 +306,7 @@ let
        default = [];
      };

      settings = mkOption {
      settings = lib.mkOption {
        type = submodule {
          freeformType = attrsOf optionType;
        };
@@ -324,7 +321,7 @@ let
  listenerAsserts = prefix: listener:
    assertKeysValid "${prefix}.settings" freeformListenerKeys listener.settings
    ++ userAsserts prefix listener.users
    ++ imap0
    ++ lib.imap0
      (i: v: authAsserts "${prefix}.authPlugins.${toString i}" v)
      listener.authPlugins;

@@ -333,9 +330,9 @@ let
      "listener ${toString listener.port} ${toString listener.address}"
      "acl_file /etc/mosquitto/acl-${toString idx}.conf"
    ]
    ++ optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
    ++ lib.optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}"
    ++ formatFreeform {} listener.settings
    ++ concatMap formatAuthPlugin listener.authPlugins;
    ++ lib.concatMap formatAuthPlugin listener.authPlugins;

  freeformBridgeKeys = {
    bridge_alpn = 1;
@@ -373,19 +370,19 @@ let
    try_private = 1;
  };

  bridgeOptions = with types; submodule {
  bridgeOptions = with lib.types; submodule {
    options = {
      addresses = mkOption {
      addresses = lib.mkOption {
        type = listOf (submodule {
          options = {
            address = mkOption {
            address = lib.mkOption {
              type = str;
              description = ''
                Address of the remote MQTT broker.
              '';
            };

            port = mkOption {
            port = lib.mkOption {
              type = port;
              description = ''
                Port of the remote MQTT broker.
@@ -400,7 +397,7 @@ let
        '';
      };

      topics = mkOption {
      topics = lib.mkOption {
        type = listOf str;
        description = ''
          Topic patterns to be shared between the two brokers.
@@ -411,7 +408,7 @@ let
        example = [ "# both 2 local/topic/ remote/topic/" ];
      };

      settings = mkOption {
      settings = lib.mkOption {
        type = submodule {
          freeformType = attrsOf optionType;
        };
@@ -426,14 +423,14 @@ let
  bridgeAsserts = prefix: bridge:
    assertKeysValid "${prefix}.settings" freeformBridgeKeys bridge.settings
    ++ [ {
      assertion = length bridge.addresses > 0;
      assertion = lib.length bridge.addresses > 0;
      message = "Bridge ${prefix} needs remote broker addresses";
    } ];

  formatBridge = name: bridge:
    [
      "connection ${name}"
      "addresses ${concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
      "addresses ${lib.concatMapStringsSep " " (a: "${a.address}:${toString a.port}") bridge.addresses}"
    ]
    ++ map (t: "topic ${t}") bridge.topics
    ++ formatFreeform {} bridge.settings;
@@ -468,12 +465,12 @@ let
    websockets_log_level = 1;
  };

  globalOptions = with types; {
    enable = mkEnableOption "the MQTT Mosquitto broker";
  globalOptions = with lib.types; {
    enable = lib.mkEnableOption "the MQTT Mosquitto broker";

    package = mkPackageOption pkgs "mosquitto" { };
    package = lib.mkPackageOption pkgs "mosquitto" { };

    bridges = mkOption {
    bridges = lib.mkOption {
      type = attrsOf bridgeOptions;
      default = {};
      description = ''
@@ -481,7 +478,7 @@ let
      '';
    };

    listeners = mkOption {
    listeners = lib.mkOption {
      type = listOf listenerOptions;
      default = [];
      description = ''
@@ -489,7 +486,7 @@ let
      '';
    };

    includeDirs = mkOption {
    includeDirs = lib.mkOption {
      type = listOf path;
      description = ''
        Directories to be scanned for further config files to include.
@@ -500,7 +497,7 @@ let
      default = [];
    };

    logDest = mkOption {
    logDest = lib.mkOption {
      type = listOf (either path (enum [ "stdout" "stderr" "syslog" "topic" "dlt" ]));
      description = ''
        Destinations to send log messages to.
@@ -508,7 +505,7 @@ let
      default = [ "stderr" ];
    };

    logType = mkOption {
    logType = lib.mkOption {
      type = listOf (enum [ "debug" "error" "warning" "notice" "information"
                            "subscribe" "unsubscribe" "websockets" "none" "all" ]);
      description = ''
@@ -517,7 +514,7 @@ let
      default = [];
    };

    persistence = mkOption {
    persistence = lib.mkOption {
      type = bool;
      description = ''
        Enable persistent storage of subscriptions and messages.
@@ -525,15 +522,15 @@ let
      default = true;
    };

    dataDir = mkOption {
    dataDir = lib.mkOption {
      default = "/var/lib/mosquitto";
      type = types.path;
      type = lib.types.path;
      description = ''
        The data directory.
      '';
    };

    settings = mkOption {
    settings = lib.mkOption {
      type = submodule {
        freeformType = attrsOf optionType;
      };
@@ -545,10 +542,10 @@ let
  };

  globalAsserts = prefix: cfg:
    flatten [
    lib.flatten [
      (assertKeysValid "${prefix}.settings" freeformGlobalKeys cfg.settings)
      (imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
      (mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
      (lib.imap0 (n: l: listenerAsserts "${prefix}.listener.${toString n}" l) cfg.listeners)
      (lib.mapAttrsToList (n: b: bridgeAsserts "${prefix}.bridge.${n}" b) cfg.bridges)
    ];

  formatGlobal = cfg:
@@ -561,12 +558,12 @@ let
      cfg.logDest
    ++ map (t: "log_type ${t}") cfg.logType
    ++ formatFreeform {} cfg.settings
    ++ concatLists (imap0 formatListener cfg.listeners)
    ++ concatLists (mapAttrsToList formatBridge cfg.bridges)
    ++ lib.concatLists (lib.imap0 formatListener cfg.listeners)
    ++ lib.concatLists (lib.mapAttrsToList formatBridge cfg.bridges)
    ++ map (d: "include_dir ${d}") cfg.includeDirs;

  configFile = pkgs.writeText "mosquitto.conf"
    (concatStringsSep "\n" (formatGlobal cfg));
    (lib.concatStringsSep "\n" (formatGlobal cfg));

in

@@ -578,7 +575,7 @@ in

  ###### Implementation

  config = mkIf cfg.enable {
  config = lib.mkIf cfg.enable {

    assertions = globalAsserts "services.mosquitto" cfg;

@@ -633,13 +630,13 @@ in
        ReadWritePaths = [
          cfg.dataDir
          "/tmp"  # mosquitto_passwd creates files in /tmp before moving them
        ] ++ filter path.check cfg.logDest;
        ] ++ lib.filter path.check cfg.logDest;
        ReadOnlyPaths =
          map (p: "${p}")
            (cfg.includeDirs
             ++ filter
             ++ lib.filter
               (v: v != null)
               (flatten [
               (lib.flatten [
                 (map
                   (l: [
                     (l.settings.psk_file or null)
@@ -652,7 +649,7 @@ in
                     (l.settings.keyfile or null)
                   ])
                   cfg.listeners)
                 (mapAttrsToList
                 (lib.mapAttrsToList
                   (_: b: [
                     (b.settings.bridge_cafile or null)
                     (b.settings.bridge_capath or null)
@@ -680,26 +677,26 @@ in
        UMask = "0077";
      };
      preStart =
        concatStringsSep
        lib.concatStringsSep
          "\n"
          (imap0
          (lib.imap0
            (idx: listener: makePasswordFile (listenerScope idx) listener.users "${cfg.dataDir}/passwd-${toString idx}")
            cfg.listeners);
    };

    environment.etc = listToAttrs (
      imap0
    environment.etc = lib.listToAttrs (
      lib.imap0
        (idx: listener: {
          name = "mosquitto/acl-${toString idx}.conf";
          value = {
            user = config.users.users.mosquitto.name;
            group = config.users.users.mosquitto.group;
            mode = "0400";
            text = (concatStringsSep
            text = (lib.concatStringsSep
              "\n"
              (flatten [
              (lib.flatten [
                listener.acl
                (mapAttrsToList
                (lib.mapAttrsToList
                  (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl)
                  listener.users)
              ]));