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