]>
Commit | Line | Data |
---|---|---|
ab8f306d | 1 | { lib, pkgs, config, ... }: |
22149d17 | 2 | let |
8d213e2b | 3 | cfg = config.myServices.tasks; |
2977fd8f | 4 | server_vardir = config.services.taskserver.dataDir; |
22149d17 IB |
5 | fqdn = "task.immae.eu"; |
6 | user = config.services.taskserver.user; | |
ab8f306d | 7 | env = config.myEnv.tools.task; |
22149d17 | 8 | group = config.services.taskserver.group; |
99b0b74a IB |
9 | taskserver-user-certs = pkgs.runCommand "taskserver-user-certs" {} '' |
10 | mkdir -p $out/bin | |
11 | cat > $out/bin/taskserver-user-certs <<"EOF" | |
12 | #!/usr/bin/env bash | |
13 | ||
14 | user=$1 | |
15 | ||
16 | silent_certtool() { | |
17 | if ! output="$("${pkgs.gnutls.bin}/bin/certtool" "$@" 2>&1)"; then | |
18 | echo "GNUTLS certtool invocation failed with output:" >&2 | |
19 | echo "$output" >&2 | |
20 | fi | |
21 | } | |
22 | ||
23 | silent_certtool -p \ | |
24 | --bits 4096 \ | |
2977fd8f IB |
25 | --outfile "${server_vardir}/userkeys/$user.key.pem" |
26 | ${pkgs.gnused}/bin/sed -i -n -e '/^-----BEGIN RSA PRIVATE KEY-----$/,$p' "${server_vardir}/userkeys/$user.key.pem" | |
99b0b74a IB |
27 | |
28 | silent_certtool -c \ | |
29 | --template "${pkgs.writeText "taskserver-ca.template" '' | |
30 | tls_www_client | |
31 | encryption_key | |
32 | signing_key | |
33 | expiration_days = 3650 | |
34 | ''}" \ | |
2977fd8f IB |
35 | --load-ca-certificate "${server_vardir}/keys/ca.cert" \ |
36 | --load-ca-privkey "${server_vardir}/keys/ca.key" \ | |
37 | --load-privkey "${server_vardir}/userkeys/$user.key.pem" \ | |
38 | --outfile "${server_vardir}/userkeys/$user.cert.pem" | |
99b0b74a IB |
39 | EOF |
40 | chmod a+x $out/bin/taskserver-user-certs | |
41 | patchShebangs $out/bin/taskserver-user-certs | |
42 | ''; | |
2977fd8f IB |
43 | taskwarrior-web = pkgs.webapps.taskwarrior-web; |
44 | socketsDir = "/run/taskwarrior-web"; | |
45 | varDir = "/var/lib/taskwarrior-web"; | |
99b0b74a IB |
46 | taskwebPages = let |
47 | uidPages = lib.attrsets.zipAttrs ( | |
48 | lib.lists.flatten | |
49 | (lib.attrsets.mapAttrsToList (k: c: map (v: { "${v}" = k; }) c.uid) env.taskwarrior-web) | |
50 | ); | |
51 | pages = lib.attrsets.mapAttrs (uid: items: | |
52 | if lib.lists.length items == 1 then | |
53 | '' | |
54 | <html> | |
55 | <head> | |
56 | <meta http-equiv="refresh" content="0; url=/taskweb/${lib.lists.head items}/" /> | |
57 | </head> | |
58 | <body></body> | |
59 | </html> | |
60 | '' | |
61 | else | |
62 | '' | |
63 | <html> | |
64 | <head> | |
65 | <title>To-do list disponibles</title> | |
66 | <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
67 | <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
68 | </head> | |
69 | <body> | |
70 | <ul> | |
71 | ${builtins.concatStringsSep "\n" (map (item: "<li><a href='/taskweb/${item}'>${item}</a></li>") items)} | |
72 | </ul> | |
73 | </body> | |
74 | </html> | |
75 | '' | |
76 | ) uidPages; | |
77 | in | |
78 | pkgs.runCommand "taskwerver-pages" {} '' | |
79 | mkdir -p $out/ | |
80 | ${builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (k: v: "cp ${pkgs.writeText k v} $out/${k}.html") pages)} | |
81 | echo "Please login" > $out/index.html | |
82 | ''; | |
22149d17 | 83 | in { |
8d213e2b | 84 | options.myServices.tasks = { |
22149d17 IB |
85 | enable = lib.mkEnableOption "my tasks service"; |
86 | }; | |
87 | ||
88 | config = lib.mkIf cfg.enable { | |
120bcf4d IB |
89 | myServices.chatonsProperties.services.taskwarrior = { |
90 | file.datetime = "2022-08-22T00:00:00"; | |
91 | service = { | |
92 | name = "Taskwarrior"; | |
93 | description = "Taskwarrior is Free and Open Source Software that manages your TODO list from the command line. Web interface and synchronization server"; | |
94 | website = "https://task.immae.eu/"; | |
95 | logo = "https://taskwarrior.org/favicon.ico"; | |
96 | status.level = "OK"; | |
97 | status.description = "OK"; | |
98 | registration."" = ["MEMBER" "CLIENT"]; | |
99 | registration.load = "OPEN"; | |
100 | install.type = "PACKAGE"; | |
101 | }; | |
102 | software = { | |
103 | name = "Taskwarrior"; | |
104 | website = "https://taskwarrior.org/"; | |
105 | license.url = "https://github.com/GothenburgBitFactory/taskwarrior/blob/develop/LICENSE"; | |
106 | license.name = "MIT License"; | |
107 | version = pkgs.webapps.taskwarrior-web.version; | |
108 | source.url = "https://taskwarrior.org/download/"; | |
109 | }; | |
110 | }; | |
4c4652aa IB |
111 | secrets.keys = { |
112 | "webapps/tools-taskwarrior-web" = { | |
afde6c32 IB |
113 | user = "wwwrun"; |
114 | group = "wwwrun"; | |
115 | permissions = "0400"; | |
116 | text = '' | |
117 | SetEnv TASKD_HOST "${fqdn}:${toString config.services.taskserver.listenPort}" | |
118 | SetEnv TASKD_VARDIR "${server_vardir}" | |
119 | SetEnv TASKD_LDAP_HOST "ldaps://${env.ldap.host}" | |
120 | SetEnv TASKD_LDAP_DN "${env.ldap.dn}" | |
121 | SetEnv TASKD_LDAP_PASSWORD "${env.ldap.password}" | |
122 | SetEnv TASKD_LDAP_BASE "${env.ldap.base}" | |
123 | SetEnv TASKD_LDAP_FILTER "${env.ldap.filter}" | |
124 | ''; | |
4c4652aa IB |
125 | }; |
126 | } // (lib.mapAttrs' (name: userConfig: lib.nameValuePair "webapps/tools-taskwarrior/${name}-taskrc" { | |
afde6c32 | 127 | inherit user group; |
cd85801d | 128 | permissions = "0400"; |
afde6c32 IB |
129 | text = let |
130 | credentials = "${userConfig.org}/${name}/${userConfig.key}"; | |
131 | dateFormat = userConfig.date; | |
132 | in '' | |
133 | data.location=${varDir}/${name} | |
134 | taskd.certificate=${server_vardir}/userkeys/taskwarrior-web.cert.pem | |
135 | taskd.key=${server_vardir}/userkeys/taskwarrior-web.key.pem | |
136 | # IdenTrust DST Root CA X3 | |
137 | # obtained here: https://letsencrypt.org/fr/certificates/ | |
138 | taskd.ca=${pkgs.writeText "ca.cert" '' | |
139 | -----BEGIN CERTIFICATE----- | |
619c894a IB |
140 | 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 | |
afde6c32 IB |
169 | -----END CERTIFICATE-----''} |
170 | taskd.server=${fqdn}:${toString config.services.taskserver.listenPort} | |
171 | taskd.credentials=${credentials} | |
172 | dateformat=${dateFormat} | |
173 | ''; | |
174 | }) env.taskwarrior-web); | |
da30ae4f | 175 | services.websites.env.tools.watchPaths = [ config.secrets.fullPaths."webapps/tools-taskwarrior-web" ]; |
29f8cb85 IB |
176 | services.websites.env.tools.modules = [ "proxy_fcgi" "sed" ]; |
177 | services.websites.env.tools.vhostConfs.task = { | |
22149d17 | 178 | certName = "eldiron"; |
7df420c2 | 179 | addToCerts = true; |
22149d17 | 180 | hosts = [ "task.immae.eu" ]; |
750fe5a4 | 181 | root = ./www; |
22149d17 | 182 | extraConfig = [ '' |
750fe5a4 | 183 | <Directory ${./www}> |
22149d17 IB |
184 | DirectoryIndex index.php |
185 | Use LDAPConnect | |
186 | Require ldap-group cn=users,cn=taskwarrior,ou=services,dc=immae,dc=eu | |
187 | <FilesMatch "\.php$"> | |
5400b9b6 | 188 | SetHandler "proxy:unix:${config.services.phpfpm.pools.tasks.socket}|fcgi://localhost" |
22149d17 | 189 | </FilesMatch> |
da30ae4f | 190 | Include ${config.secrets.fullPaths."webapps/tools-taskwarrior-web"} |
22149d17 | 191 | </Directory> |
99b0b74a IB |
192 | '' |
193 | '' | |
194 | <Macro Taskwarrior %{folderName}> | |
2977fd8f IB |
195 | ProxyPass "unix://${socketsDir}/%{folderName}.sock|http://localhost-%{folderName}/" |
196 | ProxyPassReverse "unix://${socketsDir}/%{folderName}.sock|http://localhost-%{folderName}/" | |
99b0b74a IB |
197 | ProxyPassReverse http://${fqdn}/ |
198 | ||
199 | SetOutputFilter Sed | |
200 | OutputSed "s|/ajax|/taskweb/%{folderName}/ajax|g" | |
201 | OutputSed "s|\([^x]\)/tasks|\1/taskweb/%{folderName}/tasks|g" | |
202 | OutputSed "s|\([^x]\)/projects|\1/taskweb/%{folderName}/projects|g" | |
203 | OutputSed "s|http://${fqdn}/|/taskweb/%{folderName}/|g" | |
204 | OutputSed "s|/img/relax.jpg|/taskweb/%{folderName}/img/relax.jpg|g" | |
205 | </Macro> | |
206 | '' | |
207 | '' | |
208 | Alias /taskweb ${taskwebPages} | |
209 | <Directory "${taskwebPages}"> | |
210 | DirectoryIndex index.html | |
211 | Require all granted | |
212 | </Directory> | |
213 | ||
214 | RewriteEngine on | |
215 | RewriteRule ^/taskweb$ /taskweb/ [R=301,L] | |
216 | RedirectMatch permanent ^/taskweb/([^/]+)$ /taskweb/$1/ | |
217 | ||
218 | RewriteCond %{LA-U:REMOTE_USER} !="" | |
219 | RewriteCond ${taskwebPages}/%{LA-U:REMOTE_USER}.html -f | |
220 | RewriteRule ^/taskweb/?$ ${taskwebPages}/%{LA-U:REMOTE_USER}.html [L] | |
221 | ||
222 | <Location /taskweb/> | |
223 | Use LDAPConnect | |
224 | Require ldap-group cn=users,cn=taskwarrior,ou=services,dc=immae,dc=eu | |
225 | </Location> | |
226 | '' | |
227 | ] ++ (lib.attrsets.mapAttrsToList (k: v: '' | |
228 | <Location /taskweb/${k}/> | |
229 | ${builtins.concatStringsSep "\n" (map (uid: "Require ldap-attribute uid=${uid}") v.uid)} | |
230 | ||
231 | Use Taskwarrior ${k} | |
232 | </Location> | |
233 | '') env.taskwarrior-web); | |
22149d17 | 234 | }; |
441da8aa IB |
235 | services.phpfpm.pools = { |
236 | tasks = { | |
5400b9b6 IB |
237 | user = user; |
238 | group = group; | |
239 | settings = { | |
240 | "listen.owner" = "wwwrun"; | |
241 | "listen.group" = "wwwrun"; | |
242 | "pm" = "dynamic"; | |
243 | "pm.max_children" = "60"; | |
244 | "pm.start_servers" = "2"; | |
245 | "pm.min_spare_servers" = "1"; | |
246 | "pm.max_spare_servers" = "10"; | |
247 | ||
248 | # Needed to avoid clashes in browser cookies (same domain) | |
249 | "php_value[session.name]" = "TaskPHPSESSID"; | |
bbea22c0 IB |
250 | "php_admin_value[session.save_handler]" = "redis"; |
251 | "php_admin_value[session.save_path]" = "'unix:///run/redis-php-sessions/redis.sock?persistent=1&prefix=Tools:Task:'"; | |
5400b9b6 IB |
252 | "php_admin_value[open_basedir]" = "${./www}:/tmp:${server_vardir}:/etc/profiles/per-user/${user}/bin/"; |
253 | }; | |
254 | phpEnv = { | |
255 | PATH = "/etc/profiles/per-user/${user}/bin"; | |
256 | }; | |
bbea22c0 | 257 | phpPackage = pkgs.php72.withExtensions({ enabled, all }: enabled ++ [ all.redis ]); |
441da8aa | 258 | }; |
22149d17 IB |
259 | }; |
260 | ||
5400b9b6 | 261 | security.acme.certs."task" = config.myServices.certificates.certConfig // { |
e34b3079 | 262 | inherit group; |
22149d17 IB |
263 | domain = fqdn; |
264 | postRun = '' | |
265 | systemctl restart taskserver.service | |
266 | ''; | |
267 | }; | |
268 | ||
afde6c32 IB |
269 | users.users.${user} = { |
270 | extraGroups = [ "keys" ]; | |
271 | packages = [ taskserver-user-certs ]; | |
272 | }; | |
22149d17 IB |
273 | |
274 | system.activationScripts.taskserver = { | |
275 | deps = [ "users" ]; | |
276 | text = '' | |
2977fd8f IB |
277 | install -m 0750 -o ${user} -g ${group} -d ${server_vardir} |
278 | install -m 0750 -o ${user} -g ${group} -d ${server_vardir}/userkeys | |
279 | install -m 0750 -o ${user} -g ${group} -d ${server_vardir}/keys | |
c92933bf | 280 | |
2977fd8f | 281 | if [ ! -e "${server_vardir}/keys/ca.key" ]; then |
c92933bf IB |
282 | silent_certtool() { |
283 | if ! output="$("${pkgs.gnutls.bin}/bin/certtool" "$@" 2>&1)"; then | |
284 | echo "GNUTLS certtool invocation failed with output:" >&2 | |
285 | echo "$output" >&2 | |
286 | fi | |
287 | } | |
288 | ||
289 | silent_certtool -p \ | |
290 | --bits 4096 \ | |
2977fd8f | 291 | --outfile "${server_vardir}/keys/ca.key" |
c92933bf IB |
292 | |
293 | silent_certtool -s \ | |
294 | --template "${pkgs.writeText "taskserver-ca.template" '' | |
295 | cn = ${fqdn} | |
296 | expiration_days = -1 | |
297 | cert_signing_key | |
298 | ca | |
299 | ''}" \ | |
2977fd8f IB |
300 | --load-privkey "${server_vardir}/keys/ca.key" \ |
301 | --outfile "${server_vardir}/keys/ca.cert" | |
c92933bf | 302 | |
2977fd8f IB |
303 | chown :${group} "${server_vardir}/keys/ca.key" |
304 | chmod g+r "${server_vardir}/keys/ca.key" | |
c92933bf | 305 | fi |
22149d17 IB |
306 | ''; |
307 | }; | |
308 | ||
309 | services.taskserver = { | |
310 | enable = true; | |
311 | allowedClientIDs = [ "^task [2-9]" "^Mirakel [1-9]" ]; | |
312 | inherit fqdn; | |
313 | listenHost = "::"; | |
2977fd8f | 314 | pki.manual.ca.cert = "${server_vardir}/keys/ca.cert"; |
5400b9b6 IB |
315 | pki.manual.server.cert = "${config.security.acme.certs.task.directory}/fullchain.pem"; |
316 | pki.manual.server.crl = "${config.security.acme.certs.task.directory}/invalid.crl"; | |
317 | pki.manual.server.key = "${config.security.acme.certs.task.directory}/key.pem"; | |
22149d17 IB |
318 | requestLimit = 104857600; |
319 | }; | |
99b0b74a IB |
320 | |
321 | system.activationScripts.taskwarrior-web = { | |
322 | deps = [ "users" ]; | |
323 | text = '' | |
2977fd8f | 324 | if [ ! -f ${server_vardir}/userkeys/taskwarrior-web.cert.pem ]; then |
99b0b74a | 325 | ${taskserver-user-certs}/bin/taskserver-user-certs taskwarrior-web |
2977fd8f | 326 | chown taskd:taskd ${server_vardir}/userkeys/taskwarrior-web.cert.pem ${server_vardir}/userkeys/taskwarrior-web.key.pem |
99b0b74a IB |
327 | fi |
328 | ''; | |
329 | }; | |
330 | ||
850adcf4 IB |
331 | systemd.slices.taskwarrior = { |
332 | description = "Taskwarrior slice"; | |
333 | }; | |
334 | ||
99b0b74a | 335 | systemd.services = (lib.attrsets.mapAttrs' (name: userConfig: |
afde6c32 | 336 | lib.attrsets.nameValuePair "taskwarrior-web-${name}" { |
99b0b74a IB |
337 | description = "Taskwarrior webapp for ${name}"; |
338 | wantedBy = [ "multi-user.target" ]; | |
339 | after = [ "network.target" ]; | |
340 | path = [ pkgs.taskwarrior ]; | |
341 | ||
da30ae4f | 342 | environment.TASKRC = config.secrets.fullPaths."webapps/tools-taskwarrior/${name}-taskrc"; |
450e8ce0 | 343 | environment.BUNDLE_PATH = "${taskwarrior-web.gems}/${taskwarrior-web.gems.ruby.gemPath}"; |
99b0b74a IB |
344 | environment.BUNDLE_GEMFILE = "${taskwarrior-web.gems.confFiles}/Gemfile"; |
345 | environment.LC_ALL = "fr_FR.UTF-8"; | |
346 | ||
347 | script = '' | |
2977fd8f | 348 | exec ${taskwarrior-web.gems}/${taskwarrior-web.gems.ruby.gemPath}/bin/bundle exec thin start -R config.ru -S ${socketsDir}/${name}.sock |
99b0b74a IB |
349 | ''; |
350 | ||
351 | serviceConfig = { | |
850adcf4 | 352 | Slice = "taskwarrior.slice"; |
99b0b74a IB |
353 | User = user; |
354 | PrivateTmp = true; | |
355 | Restart = "always"; | |
356 | TimeoutSec = 60; | |
357 | Type = "simple"; | |
2977fd8f | 358 | WorkingDirectory = taskwarrior-web; |
81b9ff89 IB |
359 | StateDirectoryMode = 0750; |
360 | StateDirectory = assert lib.strings.hasPrefix "/var/lib/" varDir; | |
361 | (lib.strings.removePrefix "/var/lib/" varDir + "/${name}"); | |
362 | RuntimeDirectoryPreserve = "yes"; | |
363 | RuntimeDirectory = assert lib.strings.hasPrefix "/run/" socketsDir; | |
364 | lib.strings.removePrefix "/run/" socketsDir; | |
99b0b74a IB |
365 | }; |
366 | ||
2977fd8f | 367 | unitConfig.RequiresMountsFor = varDir; |
99b0b74a IB |
368 | }) env.taskwarrior-web) // { |
369 | taskserver-ca.postStart = '' | |
2977fd8f IB |
370 | chown :${group} "${server_vardir}/keys/ca.key" |
371 | chmod g+r "${server_vardir}/keys/ca.key" | |
99b0b74a | 372 | ''; |
850adcf4 IB |
373 | taskserver-ca.serviceConfig.Slice = "taskwarrior.slice"; |
374 | taskserver-init.serviceConfig.Slice = "taskwarrior.slice"; | |
375 | taskserver.serviceConfig.Slice = "taskwarrior.slice"; | |
99b0b74a IB |
376 | }; |
377 | ||
22149d17 IB |
378 | }; |
379 | } |