1{ config, lib, pkgs, ... }:
2with lib;
3
4let
5 cfg = config.services.moinmoin;
6 python = pkgs.python27;
7 pkg = python.pkgs.moinmoin;
8 dataDir = "/var/lib/moin";
9 usingGunicorn = cfg.webServer == "nginx-gunicorn" || cfg.webServer == "gunicorn";
10 usingNginx = cfg.webServer == "nginx-gunicorn";
11 user = "moin";
12 group = "moin";
13
14 uLit = s: ''u"${s}"'';
15 indentLines = n: str: concatMapStrings (line: "${fixedWidthString n " " " "}${line}\n") (splitString "\n" str);
16
17 moinCliWrapper = wikiIdent: pkgs.writeShellScriptBin "moin-${wikiIdent}" ''
18 ${pkgs.su}/bin/su -s ${pkgs.runtimeShell} -c "${pkg}/bin/moin --config-dir=/var/lib/moin/${wikiIdent}/config $*" ${user}
19 '';
20
21 wikiConfig = wikiIdent: w: ''
22 # -*- coding: utf-8 -*-
23
24 from MoinMoin.config import multiconfig, url_prefix_static
25
26 class Config(multiconfig.DefaultConfig):
27 ${optionalString (w.webLocation != "/") ''
28 url_prefix_static = '${w.webLocation}' + url_prefix_static
29 ''}
30
31 sitename = u'${w.siteName}'
32 page_front_page = u'${w.frontPage}'
33
34 data_dir = '${dataDir}/${wikiIdent}/data'
35 data_underlay_dir = '${dataDir}/${wikiIdent}/underlay'
36
37 language_default = u'${w.languageDefault}'
38 ${optionalString (w.superUsers != []) ''
39 superuser = [${concatMapStringsSep ", " uLit w.superUsers}]
40 ''}
41
42 ${indentLines 4 w.extraConfig}
43 '';
44 wikiConfigFile = name: wiki: pkgs.writeText "${name}.py" (wikiConfig name wiki);
45
46in
47{
48 options.services.moinmoin = with types; {
49 enable = mkEnableOption "MoinMoin Wiki Engine";
50
51 webServer = mkOption {
52 type = enum [ "nginx-gunicorn" "gunicorn" "none" ];
53 default = "nginx-gunicorn";
54 example = "none";
55 description = ''
56 Which web server to use to serve the wiki.
57 Use <literal>none</literal> if you want to configure this yourself.
58 '';
59 };
60
61 gunicorn.workers = mkOption {
62 type = ints.positive;
63 default = 3;
64 example = 10;
65 description = ''
66 The number of worker processes for handling requests.
67 '';
68 };
69
70 wikis = mkOption {
71 type = attrsOf (submodule ({ name, ... }: {
72 options = {
73 siteName = mkOption {
74 type = str;
75 default = "Untitled Wiki";
76 example = "ExampleWiki";
77 description = ''
78 Short description of your wiki site, displayed below the logo on each page, and
79 used in RSS documents as the channel title.
80 '';
81 };
82
83 webHost = mkOption {
84 type = str;
85 description = "Host part of the wiki URL. If undefined, the name of the attribute set will be used.";
86 example = "wiki.example.org";
87 };
88
89 webLocation = mkOption {
90 type = str;
91 default = "/";
92 example = "/moin";
93 description = "Location part of the wiki URL.";
94 };
95
96 frontPage = mkOption {
97 type = str;
98 default = "LanguageSetup";
99 example = "FrontPage";
100 description = ''
101 Front page name. Set this to something like <literal>FrontPage</literal> once languages are
102 configured.
103 '';
104 };
105
106 superUsers = mkOption {
107 type = listOf str;
108 default = [];
109 example = [ "elvis" ];
110 description = ''
111 List of trusted user names with wiki system administration super powers.
112
113 Please note that accounts for these users need to be created using the <command>moin</command> command-line utility, e.g.:
114 <command>moin-<replaceable>WIKINAME</replaceable> account create --name=<replaceable>NAME</replaceable> --email=<replaceable>EMAIL</replaceable> --password=<replaceable>PASSWORD</replaceable></command>.
115 '';
116 };
117
118 languageDefault = mkOption {
119 type = str;
120 default = "en";
121 example = "de";
122 description = "The ISO-639-1 name of the main wiki language. Languages that MoinMoin does not support are ignored.";
123 };
124
125 extraConfig = mkOption {
126 type = lines;
127 default = "";
128 example = ''
129 show_hosts = True
130 search_results_per_page = 100
131 acl_rights_default = u"Known:read,write,delete,revert All:read"
132 logo_string = u"<h2>\U0001f639</h2>"
133 theme_default = u"modernized"
134
135 user_checkbox_defaults = {'show_page_trail': 0, 'edit_on_doubleclick': 0}
136 navi_bar = [u'SomePage'] + multiconfig.DefaultConfig.navi_bar
137 actions_excluded = multiconfig.DefaultConfig.actions_excluded + ['newaccount']
138
139 mail_smarthost = "mail.example.org"
140 mail_from = u"Example.Org Wiki <wiki@example.org>"
141 '';
142 description = ''
143 Additional configuration to be appended verbatim to this wiki's config.
144
145 See <link xlink:href='http://moinmo.in/HelpOnConfiguration' /> for documentation.
146 '';
147 };
148
149 };
150 config = {
151 webHost = mkDefault name;
152 };
153 }));
154 example = literalExample ''
155 {
156 "mywiki" = {
157 siteName = "Example Wiki";
158 webHost = "wiki.example.org";
159 superUsers = [ "admin" ];
160 frontPage = "Index";
161 extraConfig = "page_category_regex = ur'(?P<all>(Category|Kategorie)(?P<key>(?!Template)\S+))'"
162 };
163 }
164 '';
165 description = ''
166 Configurations of the individual wikis. Attribute names must be valid Python
167 identifiers of the form <literal>[A-Za-z_][A-Za-z0-9_]*</literal>.
168
169 For every attribute <replaceable>WIKINAME</replaceable>, a helper script
170 moin-<replaceable>WIKINAME</replaceable> is created which runs the
171 <command>moin</command> command under the <literal>moin</literal> user (to avoid
172 file ownership issues) and with the right configuration directory passed to it.
173 '';
174 };
175 };
176
177 config = mkIf cfg.enable {
178 assertions = forEach (attrNames cfg.wikis) (wname:
179 { assertion = builtins.match "[A-Za-z_][A-Za-z0-9_]*" wname != null;
180 message = "${wname} is not valid Python identifier";
181 }
182 );
183
184 users.users = {
185 moin = {
186 description = "MoinMoin wiki";
187 home = dataDir;
188 group = group;
189 isSystemUser = true;
190 };
191 };
192
193 users.groups = {
194 moin = {
195 members = mkIf usingNginx [ config.services.nginx.user ];
196 };
197 };
198
199 environment.systemPackages = [ pkg ] ++ map moinCliWrapper (attrNames cfg.wikis);
200
201 systemd.services = mkIf usingGunicorn
202 (flip mapAttrs' cfg.wikis (wikiIdent: wiki:
203 nameValuePair "moin-${wikiIdent}"
204 {
205 description = "MoinMoin wiki ${wikiIdent} - gunicorn process";
206 wantedBy = [ "multi-user.target" ];
207 after = [ "network.target" ];
208 restartIfChanged = true;
209 restartTriggers = [ (wikiConfigFile wikiIdent wiki) ];
210
211 environment = let
212 penv = python.buildEnv.override {
213 # setuptools: https://github.com/benoitc/gunicorn/issues/1716
214 extraLibs = [ python.pkgs.eventlet python.pkgs.setuptools pkg ];
215 };
216 in {
217 PYTHONPATH = "${dataDir}/${wikiIdent}/config:${penv}/${python.sitePackages}";
218 };
219
220 preStart = ''
221 umask 0007
222 rm -rf ${dataDir}/${wikiIdent}/underlay
223 cp -r ${pkg}/share/moin/underlay ${dataDir}/${wikiIdent}/
224 chmod -R u+w ${dataDir}/${wikiIdent}/underlay
225 '';
226
227 startLimitIntervalSec = 30;
228
229 serviceConfig = {
230 User = user;
231 Group = group;
232 WorkingDirectory = "${dataDir}/${wikiIdent}";
233 ExecStart = ''${python.pkgs.gunicorn}/bin/gunicorn moin_wsgi \
234 --name gunicorn-${wikiIdent} \
235 --workers ${toString cfg.gunicorn.workers} \
236 --worker-class eventlet \
237 --bind unix:/run/moin/${wikiIdent}/gunicorn.sock
238 '';
239
240 Restart = "on-failure";
241 RestartSec = "2s";
242
243 StateDirectory = "moin/${wikiIdent}";
244 StateDirectoryMode = "0750";
245 RuntimeDirectory = "moin/${wikiIdent}";
246 RuntimeDirectoryMode = "0750";
247
248 NoNewPrivileges = true;
249 ProtectSystem = "strict";
250 ProtectHome = true;
251 PrivateTmp = true;
252 PrivateDevices = true;
253 PrivateNetwork = true;
254 ProtectKernelTunables = true;
255 ProtectKernelModules = true;
256 ProtectControlGroups = true;
257 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
258 RestrictNamespaces = true;
259 LockPersonality = true;
260 MemoryDenyWriteExecute = true;
261 RestrictRealtime = true;
262 };
263 }
264 ));
265
266 services.nginx = mkIf usingNginx {
267 enable = true;
268 virtualHosts = flip mapAttrs' cfg.wikis (name: w: nameValuePair w.webHost {
269 forceSSL = mkDefault true;
270 enableACME = mkDefault true;
271 locations."${w.webLocation}" = {
272 extraConfig = ''
273 proxy_set_header Host $host;
274 proxy_set_header X-Real-IP $remote_addr;
275 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
276 proxy_set_header X-Forwarded-Proto $scheme;
277 proxy_set_header X-Forwarded-Host $host;
278 proxy_set_header X-Forwarded-Server $host;
279
280 proxy_pass http://unix:/run/moin/${name}/gunicorn.sock;
281 '';
282 };
283 });
284 };
285
286 systemd.tmpfiles.rules = [
287 "d /run/moin 0750 ${user} ${group} - -"
288 "d ${dataDir} 0550 ${user} ${group} - -"
289 ]
290 ++ (concatLists (flip mapAttrsToList cfg.wikis (wikiIdent: wiki: [
291 "d ${dataDir}/${wikiIdent} 0750 ${user} ${group} - -"
292 "d ${dataDir}/${wikiIdent}/config 0550 ${user} ${group} - -"
293 "L+ ${dataDir}/${wikiIdent}/config/wikiconfig.py - - - - ${wikiConfigFile wikiIdent wiki}"
294 # needed in order to pass module name to gunicorn
295 "L+ ${dataDir}/${wikiIdent}/config/moin_wsgi.py - - - - ${pkg}/share/moin/server/moin.wsgi"
296 # seed data files
297 "C ${dataDir}/${wikiIdent}/data 0770 ${user} ${group} - ${pkg}/share/moin/data"
298 # fix nix store permissions
299 "Z ${dataDir}/${wikiIdent}/data 0770 ${user} ${group} - -"
300 ])));
301 };
302
303 meta.maintainers = with lib.maintainers; [ mmilata ];
304}