1import ./make-test-python.nix (
2 { pkgs, ... }:
3 let
4 snakeOil =
5 pkgs.runCommand "snakeoil-certs"
6 {
7 outputs = [
8 "out"
9 "cacert"
10 "cert"
11 "key"
12 "crl"
13 ];
14 buildInputs = [ pkgs.gnutls.bin ];
15 caTemplate = pkgs.writeText "snakeoil-ca.template" ''
16 cn = server
17 expiration_days = -1
18 cert_signing_key
19 ca
20 '';
21 certTemplate = pkgs.writeText "snakeoil-cert.template" ''
22 cn = server
23 expiration_days = -1
24 tls_www_server
25 encryption_key
26 signing_key
27 '';
28 crlTemplate = pkgs.writeText "snakeoil-crl.template" ''
29 expiration_days = -1
30 '';
31 userCertTemplate = pkgs.writeText "snakeoil-user-cert.template" ''
32 organization = snakeoil
33 cn = server
34 expiration_days = -1
35 tls_www_client
36 encryption_key
37 signing_key
38 '';
39 }
40 ''
41 certtool -p --bits 4096 --outfile ca.key
42 certtool -s --template "$caTemplate" --load-privkey ca.key \
43 --outfile "$cacert"
44 certtool -p --bits 4096 --outfile "$key"
45 certtool -c --template "$certTemplate" \
46 --load-ca-privkey ca.key \
47 --load-ca-certificate "$cacert" \
48 --load-privkey "$key" \
49 --outfile "$cert"
50 certtool --generate-crl --template "$crlTemplate" \
51 --load-ca-privkey ca.key \
52 --load-ca-certificate "$cacert" \
53 --outfile "$crl"
54
55 mkdir "$out"
56
57 # Stripping key information before the actual PEM-encoded values is solely
58 # to make test output a bit less verbose when copying the client key to the
59 # actual client.
60 certtool -p --bits 4096 | sed -n \
61 -e '/^----* *BEGIN/,/^----* *END/p' > "$out/alice.key"
62
63 certtool -c --template "$userCertTemplate" \
64 --load-privkey "$out/alice.key" \
65 --load-ca-privkey ca.key \
66 --load-ca-certificate "$cacert" \
67 --outfile "$out/alice.cert"
68 '';
69
70 in
71 {
72 name = "taskserver";
73
74 nodes = rec {
75 server = {
76 services.taskserver.enable = true;
77 services.taskserver.listenHost = "::";
78 services.taskserver.openFirewall = true;
79 services.taskserver.fqdn = "server";
80 services.taskserver.organisations = {
81 testOrganisation.users = [
82 "alice"
83 "foo"
84 ];
85 anotherOrganisation.users = [ "bob" ];
86 };
87
88 specialisation.manual_config.configuration = {
89 services.taskserver.pki.manual = {
90 ca.cert = snakeOil.cacert;
91 server.cert = snakeOil.cert;
92 server.key = snakeOil.key;
93 server.crl = snakeOil.crl;
94 };
95 };
96 };
97
98 client1 =
99 { pkgs, ... }:
100 {
101 environment.systemPackages = [
102 pkgs.taskwarrior2
103 pkgs.gnutls
104 ];
105 users.users.alice.isNormalUser = true;
106 users.users.bob.isNormalUser = true;
107 users.users.foo.isNormalUser = true;
108 users.users.bar.isNormalUser = true;
109 };
110
111 client2 = client1;
112 };
113
114 testScript =
115 { nodes, ... }:
116 let
117 cfg = nodes.server.services.taskserver;
118 portStr = toString cfg.listenPort;
119 specialisations = "${nodes.server.system.build.toplevel}/specialisation";
120 newServerSystem = "${specialisations}/manual_config";
121 switchToNewServer = "${newServerSystem}/bin/switch-to-configuration test";
122 in
123 ''
124 from shlex import quote
125
126
127 def su(user, cmd):
128 return f"su - {user} -c {quote(cmd)}"
129
130
131 def no_extra_init(client, org, user):
132 pass
133
134
135 def setup_clients_for(org, user, extra_init=no_extra_init):
136 for client in [client1, client2]:
137 with client.nested(f"initialize client for user {user}"):
138 client.succeed(
139 su(user, f"rm -rf /home/{user}/.task"),
140 su(user, "task rc.confirmation=no config confirmation no"),
141 )
142
143 exportinfo = server.succeed(f"nixos-taskserver user export {org} {user}")
144
145 with client.nested("importing taskwarrior configuration"):
146 client.succeed(su(user, f"eval {quote(exportinfo)} >&2"))
147
148 extra_init(client, org, user)
149
150 client.succeed(su(user, "task config taskd.server server:${portStr} >&2"))
151
152 client.succeed(su(user, "task sync init >&2"))
153
154
155 def restart_server():
156 server.systemctl("restart taskserver.service")
157 server.wait_for_open_port(${portStr})
158
159
160 def re_add_imperative_user():
161 with server.nested("(re-)add imperative user bar"):
162 server.execute("nixos-taskserver org remove imperativeOrg")
163 server.succeed(
164 "nixos-taskserver org add imperativeOrg",
165 "nixos-taskserver user add imperativeOrg bar",
166 )
167 setup_clients_for("imperativeOrg", "bar")
168
169
170 def test_sync(user):
171 with subtest(f"sync for user {user}"):
172 client1.succeed(su(user, "task add foo >&2"))
173 client1.succeed(su(user, "task sync >&2"))
174 client2.fail(su(user, "task list >&2"))
175 client2.succeed(su(user, "task sync >&2"))
176 client2.succeed(su(user, "task list >&2"))
177
178
179 def check_client_cert(user):
180 # debug level 3 is a workaround for gnutls issue https://gitlab.com/gnutls/gnutls/-/issues/1040
181 cmd = (
182 f"gnutls-cli -d 3"
183 f" --x509cafile=/home/{user}/.task/keys/ca.cert"
184 f" --x509keyfile=/home/{user}/.task/keys/private.key"
185 f" --x509certfile=/home/{user}/.task/keys/public.cert"
186 f" --port=${portStr} server < /dev/null"
187 )
188 return su(user, cmd)
189
190
191 # Explicitly start the VMs so that we don't accidentally start newServer
192 server.start()
193 client1.start()
194 client2.start()
195
196 server.wait_for_unit("taskserver.service")
197
198 server.succeed(
199 "nixos-taskserver user list testOrganisation | grep -qxF alice",
200 "nixos-taskserver user list testOrganisation | grep -qxF foo",
201 "nixos-taskserver user list anotherOrganisation | grep -qxF bob",
202 )
203
204 server.wait_for_open_port(${portStr})
205
206 client1.wait_for_unit("multi-user.target")
207 client2.wait_for_unit("multi-user.target")
208
209 setup_clients_for("testOrganisation", "alice")
210 setup_clients_for("testOrganisation", "foo")
211 setup_clients_for("anotherOrganisation", "bob")
212
213 for user in ["alice", "bob", "foo"]:
214 test_sync(user)
215
216 server.fail("nixos-taskserver user add imperativeOrg bar")
217 re_add_imperative_user()
218
219 test_sync("bar")
220
221 with subtest("checking certificate revocation of user bar"):
222 client1.succeed(check_client_cert("bar"))
223
224 server.succeed("nixos-taskserver user remove imperativeOrg bar")
225 restart_server()
226
227 client1.fail(check_client_cert("bar"))
228
229 client1.succeed(su("bar", "task add destroy everything >&2"))
230 client1.fail(su("bar", "task sync >&2"))
231
232 re_add_imperative_user()
233
234 with subtest("checking certificate revocation of org imperativeOrg"):
235 client1.succeed(check_client_cert("bar"))
236
237 server.succeed("nixos-taskserver org remove imperativeOrg")
238 restart_server()
239
240 client1.fail(check_client_cert("bar"))
241
242 client1.succeed(su("bar", "task add destroy even more >&2"))
243 client1.fail(su("bar", "task sync >&2"))
244
245 re_add_imperative_user()
246
247 with subtest("check whether declarative config overrides user bar"):
248 restart_server()
249 test_sync("bar")
250
251
252 def init_manual_config(client, org, user):
253 cfgpath = f"/home/{user}/.task"
254
255 client.copy_from_host(
256 "${snakeOil.cacert}",
257 f"{cfgpath}/ca.cert",
258 )
259 for file in ["alice.key", "alice.cert"]:
260 client.copy_from_host(
261 f"${snakeOil}/{file}",
262 f"{cfgpath}/{file}",
263 )
264
265 for file in [f"{user}.key", f"{user}.cert"]:
266 client.copy_from_host(
267 f"${snakeOil}/{file}",
268 f"{cfgpath}/{file}",
269 )
270
271 client.succeed(
272 su("alice", f"task config taskd.ca {cfgpath}/ca.cert"),
273 su("alice", f"task config taskd.key {cfgpath}/{user}.key"),
274 su(user, f"task config taskd.certificate {cfgpath}/{user}.cert"),
275 )
276
277
278 with subtest("check manual configuration"):
279 # Remove the keys from automatic CA creation, to make sure the new
280 # generation doesn't use keys from before.
281 server.succeed("rm -rf ${cfg.dataDir}/keys/* >&2")
282
283 server.succeed(
284 "${switchToNewServer} >&2"
285 )
286 server.wait_for_unit("taskserver.service")
287 server.wait_for_open_port(${portStr})
288
289 server.succeed(
290 "nixos-taskserver org add manualOrg",
291 "nixos-taskserver user add manualOrg alice",
292 )
293
294 setup_clients_for("manualOrg", "alice", init_manual_config)
295
296 test_sync("alice")
297 '';
298 }
299)