Unverified Commit cff8fd9c authored by Jonas Heinrich's avatar Jonas Heinrich Committed by GitHub
Browse files

Merge pull request #208299 from e1mo/dokuwkiki-makeover

nixos/dokuwiki: Overhaul for structured settings
parents 2f84082d 236d90fd
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -307,6 +307,20 @@
          deprecated when NixOS 22.11 reaches end of life.
        </para>
      </listitem>
      <listitem>
        <para>
          The <literal>dokuwiki</literal> service now takes
          configuration via the
          <literal>services.dokuwiki.sites.&lt;name&gt;.settings</literal>
          attribute set, <literal>extraConfig</literal> is deprecated
          and will be removed. The
          <literal>{aclUse,superUser,disableActions}</literal>
          attributes have been renamed, <literal>pluginsConfig</literal>
          now also accepts an attribute set of booleans, passing plain
          PHP is deprecated. Same applies to <literal>acl</literal>
          which now also accepts structured settings.
        </para>
      </listitem>
      <listitem>
        <para>
          To reduce closure size in
+4 −0
Original line number Diff line number Diff line
@@ -84,6 +84,10 @@ In addition to numerous new and upgraded packages, this release has the followin
  `services.dnsmasq.extraConfig` will be deprecated when NixOS 22.11 reaches
  end of life.

- The `dokuwiki` service now takes configuration via the `services.dokuwiki.sites.<name>.settings` attribute set, `extraConfig` is deprecated and will be removed.
  The `{aclUse,superUser,disableActions}` attributes have been renamed, `pluginsConfig` now also accepts an attribute set of booleans, passing plain PHP is deprecated.
  Same applies to `acl` which now also accepts structured settings.

- To reduce closure size in `nixos/modules/profiles/minimal.nix` profile disabled installation documentations and manuals. Also disabled `logrotate` and `udisks2` services.

- The minimal ISO image now uses the `nixos/modules/profiles/minimal.nix` profile.
+199 −70
Original line number Diff line number Diff line
@@ -15,30 +15,64 @@ let
    extraConfig = mkPhpIni cfg.phpOptions;
  };

  dokuwikiAclAuthConfig = hostName: cfg: pkgs.writeText "acl.auth-${hostName}.php" ''
  dokuwikiAclAuthConfig = hostName: cfg: let
    inherit (cfg) acl;
    acl_gen = concatMapStringsSep "\n" (l: "${l.page} \t ${l.actor} \t ${toString l.level}");
  in pkgs.writeText "acl.auth-${hostName}.php" ''
    # acl.auth.php
    # <?php exit()?>
    #
    # Access Control Lists
    #
    ${toString cfg.acl}
    ${if isString acl then acl else acl_gen acl}
  '';

  dokuwikiLocalConfig = hostName: cfg: pkgs.writeText "local-${hostName}.php" ''
    <?php
    $conf['savedir'] = '${cfg.stateDir}';
    $conf['superuser'] = '${toString cfg.superUser}';
    $conf['useacl'] = '${toString cfg.aclUse}';
    $conf['disableactions'] = '${cfg.disableActions}';
  mergeConfig = cfg: {
    useacl = false; # Dokuwiki default
    savedir = cfg.stateDir;
  } // cfg.settings;

  writePhpFile = name: text: pkgs.writeTextFile {
    inherit name;
    text = "<?php\n${text}";
    checkPhase = "${pkgs.php81}/bin/php --syntax-check $target";
  };

  mkPhpValue = v: let
    isHasAttr = s: isAttrs v && hasAttr s v;
  in
    if isString v then escapeShellArg v
    # NOTE: If any value contains a , (comma) this will not get escaped
    else if isList v && any lib.strings.isCoercibleToString v then escapeShellArg (concatMapStringsSep "," toString v)
    else if isInt v then toString v
    else if isBool v then toString (if v then 1 else 0)
    else if isHasAttr "_file" then "trim(file_get_contents(${lib.escapeShellArg v._file}))"
    else if isHasAttr "_raw" then v._raw
    else abort "The dokuwiki localConf value ${lib.generators.toPretty {} v} can not be encoded."
  ;

  mkPhpAttrVals = v: flatten (mapAttrsToList mkPhpKeyVal v);
  mkPhpKeyVal = k: v: let
    values = if (isAttrs v && (hasAttr "_file" v || hasAttr "_raw" v )) || !isAttrs v then
      [" = ${mkPhpValue v};"]
    else
      mkPhpAttrVals v;
  in map (e: "[${escapeShellArg k}]${e}") (flatten values);

  dokuwikiLocalConfig = hostName: cfg: let
    conf_gen = c: map (v: "$conf${v}") (mkPhpAttrVals c);
  in writePhpFile "local-${hostName}.php" ''
    ${concatStringsSep "\n" (conf_gen cfg.mergedConfig)}
    ${toString cfg.extraConfig}
  '';

  dokuwikiPluginsLocalConfig = hostName: cfg: pkgs.writeText "plugins.local-${hostName}.php" ''
    <?php
    ${cfg.pluginsConfig}
  dokuwikiPluginsLocalConfig = hostName: cfg: let
    pc = cfg.pluginsConfig;
    pc_gen = pc: concatStringsSep "\n" (mapAttrsToList (n: v: "$plugins['${n}'] = ${boolToString v};") pc);
  in writePhpFile "plugins.local-${hostName}.php" ''
    ${if isString pc then pc else pc_gen pc}
  '';


  pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
    pname = "dokuwiki-${hostName}";
    version = src.version;
@@ -49,22 +83,82 @@ let
      cp -r * $out/

      # symlink the dokuwiki config
      ln -s ${dokuwikiLocalConfig hostName cfg} $out/share/dokuwiki/local.php
      ln -sf ${dokuwikiLocalConfig hostName cfg} $out/share/dokuwiki/conf/local.php

      # symlink plugins config
      ln -s ${dokuwikiPluginsLocalConfig hostName cfg} $out/share/dokuwiki/plugins.local.php
      ln -sf ${dokuwikiPluginsLocalConfig hostName cfg} $out/share/dokuwiki/conf/plugins.local.php

      # symlink acl
      ln -s ${dokuwikiAclAuthConfig hostName cfg} $out/share/dokuwiki/acl.auth.php
      # symlink acl (if needed)
      ${optionalString (cfg.mergedConfig.useacl && cfg.acl != null) "ln -sf ${dokuwikiAclAuthConfig hostName cfg} $out/share/dokuwiki/acl.auth.php"}

      # symlink additional plugin(s) and templates(s)
      ${concatMapStringsSep "\n" (template: "ln -s ${template} $out/share/dokuwiki/lib/tpl/${template.name}") cfg.templates}
      ${concatMapStringsSep "\n" (plugin: "ln -s ${plugin} $out/share/dokuwiki/lib/plugins/${plugin.name}") cfg.plugins}
      ${concatMapStringsSep "\n" (template: "ln -sf ${template} $out/share/dokuwiki/lib/tpl/${template.name}") cfg.templates}
      ${concatMapStringsSep "\n" (plugin: "ln -sf ${plugin} $out/share/dokuwiki/lib/plugins/${plugin.name}") cfg.plugins}
    '';
  };

  aclOpts = { ... }: {
    options = {

      page = mkOption {
        type = types.str;
        description = "Page or namespace to restrict";
        example = "start";
      };

      actor = mkOption {
        type = types.str;
        description = "User or group to restrict";
        example = "@external";
      };

      level = let
        available = {
          "none" = 0;
          "read" = 1;
          "edit" = 2;
          "create" = 4;
          "upload" = 8;
          "delete" = 16;
        };
      in mkOption {
        type = types.enum ((attrValues available) ++ (attrNames available));
        apply = x: if isInt x then x else available.${x};
        description = ''
          Permission level to restrict the actor(s) to.
          See <https://www.dokuwiki.org/acl#background_info> for explanation
        '';
        example = "read";
      };

    };
  };

  siteOpts = { config, lib, name, ... }:
    {
      imports = [
        # NOTE: These will sadly not print the absolute argument path but only the name. Related to #96006
        (mkRenamedOptionModule [ "aclUse" ] [ "settings" "useacl" ] )
        (mkRenamedOptionModule [ "superUser" ] [ "settings" "superuser" ] )
        (mkRenamedOptionModule [ "disableActions" ] [ "settings" "disableactions" ] )
        ({ config, options, name, ...}: {
          config.warnings =
            (optional (isString config.pluginsConfig) ''
              Passing plain strings to services.dokuwiki.sites.${name}.pluginsConfig has been deprecated and will not be continue to be supported in the future.
              Please pass structured settings instead.
            '')
            ++ (optional (isString config.acl) ''
              Passing a plain string to services.dokuwiki.sites.${name}.acl has been deprecated and will not continue to be supported in the future.
              Please pass structured settings instead.
            '')
            ++ (optional (config.extraConfig != null) ''
              services.dokuwiki.sites.${name}.extraConfig is deprecated and will be removed in the future.
              Please pass structured settings to services.dokuwiki.sites.${name}.settings instead.
            '')
          ;
        })
      ];

      options = {
        enable = mkEnableOption (lib.mdDoc "DokuWiki web application.");

@@ -82,9 +176,22 @@ let
        };

        acl = mkOption {
          type = types.nullOr types.lines;
          type = with types; nullOr (oneOf [ lines (listOf (submodule aclOpts)) ]);
          default = null;
          example = "*               @ALL               8";
          example = literalExpression ''
            [
              {
                page = "start";
                actor = "@external";
                level = "read";
              }
              {
                page = "*";
                actor = "@users";
                level = "upload";
              }
            ]
          '';
          description = lib.mdDoc ''
            Access Control Lists: see <https://www.dokuwiki.org/acl>
            Mutually exclusive with services.dokuwiki.aclFile
@@ -97,7 +204,7 @@ let

        aclFile = mkOption {
          type = with types; nullOr str;
          default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
          default = if (config.mergedConfig.useacl && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
          description = lib.mdDoc ''
            Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
            Mutually exclusive with services.dokuwiki.acl which is preferred.
@@ -107,42 +214,22 @@ let
          example = "/var/lib/dokuwiki/${name}/acl.auth.php";
        };

        aclUse = mkOption {
          type = types.bool;
          default = true;
          description = lib.mdDoc ''
            Necessary for users to log in into the system.
            Also limits anonymous users. When disabled,
            everyone is able to create and edit content.
          '';
        };

        pluginsConfig = mkOption {
          type = types.lines;
          default = ''
            $plugins['authad'] = 0;
            $plugins['authldap'] = 0;
            $plugins['authmysql'] = 0;
            $plugins['authpgsql'] = 0;
          '';
          description = lib.mdDoc ''
            List of the dokuwiki (un)loaded plugins.
          '';
          type = with types; oneOf [lines (attrsOf bool)];
          default = {
            authad = false;
            authldap = false;
            authmysql = false;
            authpgsql = false;
          };

        superUser = mkOption {
          type = types.nullOr types.str;
          default = "@admin";
          description = lib.mdDoc ''
            You can set either a username, a list of usernames (“admin1,admin2”),
            or the name of a group by prepending an @ char to the groupname
            Consult documentation <https://www.dokuwiki.org/config:superuser> for further instructions.
            List of the dokuwiki (un)loaded plugins.
          '';
        };

        usersFile = mkOption {
          type = with types; nullOr str;
          default = if config.aclUse then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
          default = if config.mergedConfig.useacl then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
          description = lib.mdDoc ''
            Location of the dokuwiki users file. List of users. Format:

@@ -157,17 +244,6 @@ let
          example = "/var/lib/dokuwiki/${name}/users.auth.php";
        };

        disableActions = mkOption {
          type = types.nullOr types.str;
          default = "";
          example = "search,register";
          description = lib.mdDoc ''
            Disable individual action modes. Refer to
            <https://www.dokuwiki.org/config:action_modes>
            for details on supported values.
          '';
        };

        plugins = mkOption {
          type = types.listOf types.path;
          default = [];
@@ -266,7 +342,50 @@ let
          '';
        };

        settings = mkOption {
          type = types.attrsOf types.anything;
          default = {
            useacl = true;
            superuser = "admin";
          };
          description = lib.mdDoc ''
            Structural DokuWiki configuration.
            Refer to <https://www.dokuwiki.org/config>
            for details and supported values.
            Settings can either be directly set from nix,
            loaded from a file using `._file` or obtained from any
            PHP function calls using `._raw`.
          '';
          example = literalExpression ''
            {
              title = "My Wiki";
              userewrite = 1;
              disableactions = [ "register" ]; # Will be concatenated with commas
              plugin.smtp = {
                smtp_pass._file = "/var/run/secrets/dokuwiki/smtp_pass";
                smtp_user._raw = "getenv('DOKUWIKI_SMTP_USER')";
              };
            }
          '';
        };

        mergedConfig = mkOption {
          readOnly = true;
          default = mergeConfig config;
          defaultText = literalExpression ''
            {
              useacl = true;
            }
          '';
          description = lib.mdDoc ''
            Read only representation of the final configuration.
          '';
        };

        extraConfig = mkOption {
          # This Option is deprecated and only kept until sometime before 23.05 for compatibility reasons
          # FIXME (@e1mo): Actually remember removing this before 23.05.
          visible = false;
          type = types.nullOr types.lines;
          default = null;
          example = ''
@@ -277,15 +396,26 @@ let
            DokuWiki configuration. Refer to
            <https://www.dokuwiki.org/config>
            for details on supported values.

            **Note**: Please pass Structured settings via
            `services.dokuwiki.sites.${name}.settings` instead.
          '';
        };

      # Required for the mkRenamedOptionModule
      # TODO: Remove me once https://github.com/NixOS/nixpkgs/issues/96006 is fixed
      # or the aclUse, ... options are removed.
      warnings = mkOption {
        type = types.listOf types.unspecified;
        default = [ ];
        visible = false;
        internal = true;
      };

    };
  };
in
{
  # interface
  options = {
    services.dokuwiki = {

@@ -315,14 +445,16 @@ in
  # implementation
  config = mkIf (eachSite != {}) (mkMerge [{

    warnings = flatten (mapAttrsToList (_: cfg: cfg.warnings) eachSite);

    assertions = flatten (mapAttrsToList (hostName: cfg:
    [{
      assertion = cfg.aclUse -> (cfg.acl != null || cfg.aclFile != null);
      message = "Either services.dokuwiki.sites.${hostName}.acl or services.dokuwiki.sites.${hostName}.aclFile is mandatory if aclUse true";
      assertion = cfg.mergedConfig.useacl -> (cfg.acl != null || cfg.aclFile != null);
      message = "Either services.dokuwiki.sites.${hostName}.acl or services.dokuwiki.sites.${hostName}.aclFile is mandatory if settings.useacl is true";
    }
    {
      assertion = cfg.usersFile != null -> cfg.aclUse != false;
      message = "services.dokuwiki.sites.${hostName}.aclUse must must be true if usersFile is not null";
      assertion = cfg.usersFile != null -> cfg.mergedConfig.useacl != false;
      message = "services.dokuwiki.sites.${hostName}.settings.useacl must must be true if usersFile is not null";
    }
    ]) eachSite);

@@ -332,12 +464,9 @@ in
        group = webserver.group;

        phpPackage = mkPhpPackage cfg;
        phpEnv = {
          DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
          DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
        } // optionalAttrs (cfg.usersFile != null) {
        phpEnv = optionalAttrs (cfg.usersFile != null) {
          DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
        } //optionalAttrs (cfg.aclUse) {
        } // optionalAttrs (cfg.mergedConfig.useacl) {
          DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}";
        };

+43 −5
Original line number Diff line number Diff line
@@ -41,15 +41,39 @@ let

      sites = {
        "site1.local" = {
          aclUse = false;
          superUser = "admin";
          templates = [ template-bootstrap3 ];
          settings = {
            useacl = false;
            userewrite = true;
            template = "bootstrap3";
          };
        };
        "site2.local" = {
          package = dwWithAcronyms;
          usersFile = "/var/lib/dokuwiki/site2.local/users.auth.php";
          superUser = "admin";
          templates = [ template-bootstrap3 ];
          plugins = [ plugin-icalevents ];
          settings = {
            useacl = true;
            superuser = "admin";
            title._file = titleFile;
            plugin.dummy.empty = "This is just for testing purposes";
          };
          acl = [
            { page = "*";
              actor = "@ALL";
              level = "read"; }
            { page = "acl-test";
              actor = "@ALL";
              level = "none"; }
          ];
          pluginsConfig = {
            authad = false;
            authldap = false;
            authmysql = false;
            authpgsql = false;
            tag = false;
            icalevents = true;
          };
        };
      };
    };
@@ -58,6 +82,7 @@ let
    networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
  };

  titleFile = pkgs.writeText "dokuwiki-title" "DokuWiki on site2";
in {
  name = "dokuwiki";
  meta = with pkgs.lib; {
@@ -88,7 +113,7 @@ in {
        machine.succeed("curl -sSfL http://site1.local/ | grep 'DokuWiki'")
        machine.fail("curl -sSfL 'http://site1.local/doku.php?do=login' | grep 'Login'")

        machine.succeed("curl -sSfL http://site2.local/ | grep 'DokuWiki'")
        machine.succeed("curl -sSfL http://site2.local/ | grep 'DokuWiki on site2'")
        machine.succeed("curl -sSfL 'http://site2.local/doku.php?do=login' | grep 'Login'")

        with subtest("ACL Operations"):
@@ -98,12 +123,25 @@ in {
            "curl -sSfL --cookie cjar --cookie-jar cjar 'http://site2.local/doku.php?do=login' | grep 'Logged in as: <bdi>Admin</bdi>'",
          )

          # Ensure the generated ACL is valid
          machine.succeed(
            "echo 'No Hello World! for @ALL here' >> /var/lib/dokuwiki/site2.local/data/pages/acl-test.txt",
            "curl -sSL 'http://site2.local/doku.php?id=acl-test' | grep 'Permission Denied'"
          )

        with subtest("Customizing Dokuwiki"):
          machine.succeed(
            "echo 'r13y is awesome!' >> /var/lib/dokuwiki/site2.local/data/pages/acronyms-test.txt",
            "curl -sSfL 'http://site2.local/doku.php?id=acronyms-test' | grep '<abbr title=\"reproducibility\">r13y</abbr>'",
          )

          # Testing if plugins (a) be correctly loaded and (b) configuration to enable them works
          machine.succeed(
              "echo '~~INFO:syntaxplugins~~' >> /var/lib/dokuwiki/site2.local/data/pages/plugin-list.txt",
              "curl -sSfL 'http://site2.local/doku.php?id=plugin-list' | grep 'plugin:icalevents'",
              "curl -sSfL 'http://site2.local/doku.php?id=plugin-list' | (! grep 'plugin:tag')",
          )

        # Just to ensure both Webserver configurations are consistent in allowing that
        with subtest("Rewriting"):
          machine.succeed(