nixos/dokuwiki: Overhaul for structured settings

Added the RFC42-style added the posibility to use
`services.dokuwiki.sites.<name>.settings' instead of passing a plain
string to `<name>.extraConfig`. ´<name>.pluginsConfig` now also accepts
structured configuration.
This commit is contained in:
Moritz 'e1mo' Fromm 2023-01-06 02:36:30 +01:00
parent 108627107d
commit 236d90fde0
No known key found for this signature in database
GPG Key ID: 1D5D79A439E787F1
4 changed files with 260 additions and 75 deletions

View File

@ -307,6 +307,20 @@
deprecated when NixOS 22.11 reaches end of life. deprecated when NixOS 22.11 reaches end of life.
</para> </para>
</listitem> </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> <listitem>
<para> <para>
To reduce closure size in To reduce closure size in

View File

@ -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 `services.dnsmasq.extraConfig` will be deprecated when NixOS 22.11 reaches
end of life. 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. - 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. - The minimal ISO image now uses the `nixos/modules/profiles/minimal.nix` profile.

View File

@ -15,30 +15,64 @@ let
extraConfig = mkPhpIni cfg.phpOptions; 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 # acl.auth.php
# <?php exit()?> # <?php exit()?>
# #
# Access Control Lists # Access Control Lists
# #
${toString cfg.acl} ${if isString acl then acl else acl_gen acl}
''; '';
dokuwikiLocalConfig = hostName: cfg: pkgs.writeText "local-${hostName}.php" '' mergeConfig = cfg: {
<?php useacl = false; # Dokuwiki default
$conf['savedir'] = '${cfg.stateDir}'; savedir = cfg.stateDir;
$conf['superuser'] = '${toString cfg.superUser}'; } // cfg.settings;
$conf['useacl'] = '${toString cfg.aclUse}';
$conf['disableactions'] = '${cfg.disableActions}'; 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} ${toString cfg.extraConfig}
''; '';
dokuwikiPluginsLocalConfig = hostName: cfg: pkgs.writeText "plugins.local-${hostName}.php" '' dokuwikiPluginsLocalConfig = hostName: cfg: let
<?php pc = cfg.pluginsConfig;
${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 { pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
pname = "dokuwiki-${hostName}"; pname = "dokuwiki-${hostName}";
version = src.version; version = src.version;
@ -49,22 +83,82 @@ let
cp -r * $out/ cp -r * $out/
# symlink the dokuwiki config # 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 # 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 # symlink acl (if needed)
ln -s ${dokuwikiAclAuthConfig hostName cfg} $out/share/dokuwiki/acl.auth.php ${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) # symlink additional plugin(s) and templates(s)
${concatMapStringsSep "\n" (template: "ln -s ${template} $out/share/dokuwiki/lib/tpl/${template.name}") cfg.templates} ${concatMapStringsSep "\n" (template: "ln -sf ${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" (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, ... }: 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 = { options = {
enable = mkEnableOption (lib.mdDoc "DokuWiki web application."); enable = mkEnableOption (lib.mdDoc "DokuWiki web application.");
@ -82,9 +176,22 @@ let
}; };
acl = mkOption { acl = mkOption {
type = types.nullOr types.lines; type = with types; nullOr (oneOf [ lines (listOf (submodule aclOpts)) ]);
default = null; default = null;
example = "* @ALL 8"; example = literalExpression ''
[
{
page = "start";
actor = "@external";
level = "read";
}
{
page = "*";
actor = "@users";
level = "upload";
}
]
'';
description = lib.mdDoc '' description = lib.mdDoc ''
Access Control Lists: see <https://www.dokuwiki.org/acl> Access Control Lists: see <https://www.dokuwiki.org/acl>
Mutually exclusive with services.dokuwiki.aclFile Mutually exclusive with services.dokuwiki.aclFile
@ -97,7 +204,7 @@ let
aclFile = mkOption { aclFile = mkOption {
type = with types; nullOr str; 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 '' description = lib.mdDoc ''
Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
Mutually exclusive with services.dokuwiki.acl which is preferred. Mutually exclusive with services.dokuwiki.acl which is preferred.
@ -107,42 +214,22 @@ let
example = "/var/lib/dokuwiki/${name}/acl.auth.php"; 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 { pluginsConfig = mkOption {
type = types.lines; type = with types; oneOf [lines (attrsOf bool)];
default = '' default = {
$plugins['authad'] = 0; authad = false;
$plugins['authldap'] = 0; authldap = false;
$plugins['authmysql'] = 0; authmysql = false;
$plugins['authpgsql'] = 0; authpgsql = false;
''; };
description = lib.mdDoc '' description = lib.mdDoc ''
List of the dokuwiki (un)loaded plugins. List of the dokuwiki (un)loaded plugins.
''; '';
}; };
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.
'';
};
usersFile = mkOption { usersFile = mkOption {
type = with types; nullOr str; 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 '' description = lib.mdDoc ''
Location of the dokuwiki users file. List of users. Format: Location of the dokuwiki users file. List of users. Format:
@ -157,17 +244,6 @@ let
example = "/var/lib/dokuwiki/${name}/users.auth.php"; 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 { plugins = mkOption {
type = types.listOf types.path; type = types.listOf types.path;
default = []; 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 { 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; type = types.nullOr types.lines;
default = null; default = null;
example = '' example = ''
@ -277,15 +396,26 @@ let
DokuWiki configuration. Refer to DokuWiki configuration. Refer to
<https://www.dokuwiki.org/config> <https://www.dokuwiki.org/config>
for details on supported values. 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 in
{ {
# interface
options = { options = {
services.dokuwiki = { services.dokuwiki = {
@ -315,14 +445,16 @@ in
# implementation # implementation
config = mkIf (eachSite != {}) (mkMerge [{ config = mkIf (eachSite != {}) (mkMerge [{
warnings = flatten (mapAttrsToList (_: cfg: cfg.warnings) eachSite);
assertions = flatten (mapAttrsToList (hostName: cfg: assertions = flatten (mapAttrsToList (hostName: cfg:
[{ [{
assertion = cfg.aclUse -> (cfg.acl != null || cfg.aclFile != null); 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 aclUse true"; 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; assertion = cfg.usersFile != null -> cfg.mergedConfig.useacl != false;
message = "services.dokuwiki.sites.${hostName}.aclUse must must be true if usersFile is not null"; message = "services.dokuwiki.sites.${hostName}.settings.useacl must must be true if usersFile is not null";
} }
]) eachSite); ]) eachSite);
@ -332,12 +464,9 @@ in
group = webserver.group; group = webserver.group;
phpPackage = mkPhpPackage cfg; phpPackage = mkPhpPackage cfg;
phpEnv = { phpEnv = optionalAttrs (cfg.usersFile != null) {
DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
} // optionalAttrs (cfg.usersFile != null) {
DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}"; 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}"; DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}";
}; };

View File

@ -41,15 +41,39 @@ let
sites = { sites = {
"site1.local" = { "site1.local" = {
aclUse = false; templates = [ template-bootstrap3 ];
superUser = "admin"; settings = {
useacl = false;
userewrite = true;
template = "bootstrap3";
};
}; };
"site2.local" = { "site2.local" = {
package = dwWithAcronyms; package = dwWithAcronyms;
usersFile = "/var/lib/dokuwiki/site2.local/users.auth.php"; usersFile = "/var/lib/dokuwiki/site2.local/users.auth.php";
superUser = "admin";
templates = [ template-bootstrap3 ];
plugins = [ plugin-icalevents ]; 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" ]; networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
}; };
titleFile = pkgs.writeText "dokuwiki-title" "DokuWiki on site2";
in { in {
name = "dokuwiki"; name = "dokuwiki";
meta = with pkgs.lib; { meta = with pkgs.lib; {
@ -88,7 +113,7 @@ in {
machine.succeed("curl -sSfL http://site1.local/ | grep 'DokuWiki'") machine.succeed("curl -sSfL http://site1.local/ | grep 'DokuWiki'")
machine.fail("curl -sSfL 'http://site1.local/doku.php?do=login' | grep 'Login'") 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'") machine.succeed("curl -sSfL 'http://site2.local/doku.php?do=login' | grep 'Login'")
with subtest("ACL Operations"): 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>'", "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"): with subtest("Customizing Dokuwiki"):
machine.succeed( machine.succeed(
"echo 'r13y is awesome!' >> /var/lib/dokuwiki/site2.local/data/pages/acronyms-test.txt", "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>'", "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 # Just to ensure both Webserver configurations are consistent in allowing that
with subtest("Rewriting"): with subtest("Rewriting"):
machine.succeed( machine.succeed(