]>
Commit | Line | Data |
---|---|---|
8d769ec7 MAL |
1 | /* |
2 | * This work is licensed under the terms of the GNU GPL, version 2 or later. | |
3 | * See the COPYING file in the top-level directory. | |
4 | */ | |
5 | #include "qemu/osdep.h" | |
6 | ||
7 | #include <glib-unix.h> | |
8 | #include <glib/gstdio.h> | |
9 | #include <locale.h> | |
10 | #include <pwd.h> | |
11 | ||
12 | #include "qapi/error.h" | |
13 | #include "qga-qapi-commands.h" | |
14 | ||
15 | #ifdef QGA_BUILD_UNIT_TEST | |
16 | static struct passwd * | |
17 | test_get_passwd_entry(const gchar *user_name, GError **error) | |
18 | { | |
19 | struct passwd *p; | |
20 | int ret; | |
21 | ||
22 | if (!user_name || g_strcmp0(user_name, g_get_user_name())) { | |
23 | g_set_error(error, G_UNIX_ERROR, 0, "Invalid user name"); | |
24 | return NULL; | |
25 | } | |
26 | ||
27 | p = g_new0(struct passwd, 1); | |
28 | p->pw_dir = (char *)g_get_home_dir(); | |
29 | p->pw_uid = geteuid(); | |
30 | p->pw_gid = getegid(); | |
31 | ||
32 | ret = g_mkdir_with_parents(p->pw_dir, 0700); | |
33 | g_assert(ret == 0); | |
34 | ||
35 | return p; | |
36 | } | |
37 | ||
38 | #define g_unix_get_passwd_entry_qemu(username, err) \ | |
39 | test_get_passwd_entry(username, err) | |
40 | #endif | |
41 | ||
42 | static struct passwd * | |
43 | get_passwd_entry(const char *username, Error **errp) | |
44 | { | |
45 | g_autoptr(GError) err = NULL; | |
46 | struct passwd *p; | |
47 | ||
8d769ec7 MAL |
48 | p = g_unix_get_passwd_entry_qemu(username, &err); |
49 | if (p == NULL) { | |
50 | error_setg(errp, "failed to lookup user '%s': %s", | |
51 | username, err->message); | |
52 | return NULL; | |
53 | } | |
54 | ||
55 | return p; | |
56 | } | |
57 | ||
58 | static bool | |
59 | mkdir_for_user(const char *path, const struct passwd *p, | |
60 | mode_t mode, Error **errp) | |
61 | { | |
8d769ec7 MAL |
62 | if (g_mkdir(path, mode) == -1) { |
63 | error_setg(errp, "failed to create directory '%s': %s", | |
64 | path, g_strerror(errno)); | |
65 | return false; | |
66 | } | |
67 | ||
68 | if (chown(path, p->pw_uid, p->pw_gid) == -1) { | |
69 | error_setg(errp, "failed to set ownership of directory '%s': %s", | |
70 | path, g_strerror(errno)); | |
71 | return false; | |
72 | } | |
73 | ||
74 | if (chmod(path, mode) == -1) { | |
75 | error_setg(errp, "failed to set permissions of directory '%s': %s", | |
76 | path, g_strerror(errno)); | |
77 | return false; | |
78 | } | |
79 | ||
80 | return true; | |
81 | } | |
82 | ||
83 | static bool | |
84 | check_openssh_pub_key(const char *key, Error **errp) | |
85 | { | |
8d769ec7 MAL |
86 | /* simple sanity-check, we may want more? */ |
87 | if (!key || key[0] == '#' || strchr(key, '\n')) { | |
88 | error_setg(errp, "invalid OpenSSH public key: '%s'", key); | |
89 | return false; | |
90 | } | |
91 | ||
92 | return true; | |
93 | } | |
94 | ||
95 | static bool | |
96 | check_openssh_pub_keys(strList *keys, size_t *nkeys, Error **errp) | |
97 | { | |
98 | size_t n = 0; | |
99 | strList *k; | |
100 | ||
8d769ec7 MAL |
101 | for (k = keys; k != NULL; k = k->next) { |
102 | if (!check_openssh_pub_key(k->value, errp)) { | |
103 | return false; | |
104 | } | |
105 | n++; | |
106 | } | |
107 | ||
108 | if (nkeys) { | |
109 | *nkeys = n; | |
110 | } | |
111 | return true; | |
112 | } | |
113 | ||
114 | static bool | |
115 | write_authkeys(const char *path, const GStrv keys, | |
116 | const struct passwd *p, Error **errp) | |
117 | { | |
118 | g_autofree char *contents = NULL; | |
119 | g_autoptr(GError) err = NULL; | |
120 | ||
8d769ec7 MAL |
121 | contents = g_strjoinv("\n", keys); |
122 | if (!g_file_set_contents(path, contents, -1, &err)) { | |
123 | error_setg(errp, "failed to write to '%s': %s", path, err->message); | |
124 | return false; | |
125 | } | |
126 | ||
127 | if (chown(path, p->pw_uid, p->pw_gid) == -1) { | |
128 | error_setg(errp, "failed to set ownership of directory '%s': %s", | |
129 | path, g_strerror(errno)); | |
130 | return false; | |
131 | } | |
132 | ||
133 | if (chmod(path, 0600) == -1) { | |
134 | error_setg(errp, "failed to set permissions of '%s': %s", | |
135 | path, g_strerror(errno)); | |
136 | return false; | |
137 | } | |
138 | ||
139 | return true; | |
140 | } | |
141 | ||
142 | static GStrv | |
143 | read_authkeys(const char *path, Error **errp) | |
144 | { | |
145 | g_autoptr(GError) err = NULL; | |
146 | g_autofree char *contents = NULL; | |
147 | ||
8d769ec7 MAL |
148 | if (!g_file_get_contents(path, &contents, NULL, &err)) { |
149 | error_setg(errp, "failed to read '%s': %s", path, err->message); | |
150 | return NULL; | |
151 | } | |
152 | ||
153 | return g_strsplit(contents, "\n", -1); | |
154 | ||
155 | } | |
156 | ||
157 | void | |
158 | qmp_guest_ssh_add_authorized_keys(const char *username, strList *keys, | |
0e3c9475 | 159 | bool has_reset, bool reset, |
8d769ec7 MAL |
160 | Error **errp) |
161 | { | |
162 | g_autofree struct passwd *p = NULL; | |
163 | g_autofree char *ssh_path = NULL; | |
164 | g_autofree char *authkeys_path = NULL; | |
165 | g_auto(GStrv) authkeys = NULL; | |
166 | strList *k; | |
167 | size_t nkeys, nauthkeys; | |
168 | ||
0e3c9475 | 169 | reset = has_reset && reset; |
8d769ec7 MAL |
170 | |
171 | if (!check_openssh_pub_keys(keys, &nkeys, errp)) { | |
172 | return; | |
173 | } | |
174 | ||
175 | p = get_passwd_entry(username, errp); | |
176 | if (p == NULL) { | |
177 | return; | |
178 | } | |
179 | ||
180 | ssh_path = g_build_filename(p->pw_dir, ".ssh", NULL); | |
181 | authkeys_path = g_build_filename(ssh_path, "authorized_keys", NULL); | |
182 | ||
0e3c9475 MR |
183 | if (!reset) { |
184 | authkeys = read_authkeys(authkeys_path, NULL); | |
185 | } | |
8d769ec7 MAL |
186 | if (authkeys == NULL) { |
187 | if (!g_file_test(ssh_path, G_FILE_TEST_IS_DIR) && | |
188 | !mkdir_for_user(ssh_path, p, 0700, errp)) { | |
189 | return; | |
190 | } | |
191 | } | |
192 | ||
193 | nauthkeys = authkeys ? g_strv_length(authkeys) : 0; | |
194 | authkeys = g_realloc_n(authkeys, nauthkeys + nkeys + 1, sizeof(char *)); | |
195 | memset(authkeys + nauthkeys, 0, (nkeys + 1) * sizeof(char *)); | |
196 | ||
197 | for (k = keys; k != NULL; k = k->next) { | |
198 | if (g_strv_contains((const gchar * const *)authkeys, k->value)) { | |
199 | continue; | |
200 | } | |
201 | authkeys[nauthkeys++] = g_strdup(k->value); | |
202 | } | |
203 | ||
204 | write_authkeys(authkeys_path, authkeys, p, errp); | |
205 | } | |
206 | ||
207 | void | |
208 | qmp_guest_ssh_remove_authorized_keys(const char *username, strList *keys, | |
209 | Error **errp) | |
210 | { | |
211 | g_autofree struct passwd *p = NULL; | |
212 | g_autofree char *authkeys_path = NULL; | |
213 | g_autofree GStrv new_keys = NULL; /* do not own the strings */ | |
214 | g_auto(GStrv) authkeys = NULL; | |
215 | GStrv a; | |
216 | size_t nkeys = 0; | |
217 | ||
8d769ec7 MAL |
218 | if (!check_openssh_pub_keys(keys, NULL, errp)) { |
219 | return; | |
220 | } | |
221 | ||
222 | p = get_passwd_entry(username, errp); | |
223 | if (p == NULL) { | |
224 | return; | |
225 | } | |
226 | ||
227 | authkeys_path = g_build_filename(p->pw_dir, ".ssh", | |
228 | "authorized_keys", NULL); | |
229 | if (!g_file_test(authkeys_path, G_FILE_TEST_EXISTS)) { | |
230 | return; | |
231 | } | |
232 | authkeys = read_authkeys(authkeys_path, errp); | |
233 | if (authkeys == NULL) { | |
234 | return; | |
235 | } | |
236 | ||
237 | new_keys = g_new0(char *, g_strv_length(authkeys) + 1); | |
238 | for (a = authkeys; *a != NULL; a++) { | |
239 | strList *k; | |
240 | ||
241 | for (k = keys; k != NULL; k = k->next) { | |
242 | if (g_str_equal(k->value, *a)) { | |
243 | break; | |
244 | } | |
245 | } | |
246 | if (k != NULL) { | |
247 | continue; | |
248 | } | |
249 | ||
250 | new_keys[nkeys++] = *a; | |
251 | } | |
252 | ||
253 | write_authkeys(authkeys_path, new_keys, p, errp); | |
254 | } | |
255 | ||
cad97c08 MAL |
256 | GuestAuthorizedKeys * |
257 | qmp_guest_ssh_get_authorized_keys(const char *username, Error **errp) | |
258 | { | |
259 | g_autofree struct passwd *p = NULL; | |
260 | g_autofree char *authkeys_path = NULL; | |
261 | g_auto(GStrv) authkeys = NULL; | |
262 | g_autoptr(GuestAuthorizedKeys) ret = NULL; | |
263 | int i; | |
264 | ||
cad97c08 MAL |
265 | p = get_passwd_entry(username, errp); |
266 | if (p == NULL) { | |
267 | return NULL; | |
268 | } | |
269 | ||
270 | authkeys_path = g_build_filename(p->pw_dir, ".ssh", | |
271 | "authorized_keys", NULL); | |
272 | authkeys = read_authkeys(authkeys_path, errp); | |
273 | if (authkeys == NULL) { | |
274 | return NULL; | |
275 | } | |
276 | ||
277 | ret = g_new0(GuestAuthorizedKeys, 1); | |
278 | for (i = 0; authkeys[i] != NULL; i++) { | |
cad97c08 MAL |
279 | g_strstrip(authkeys[i]); |
280 | if (!authkeys[i][0] || authkeys[i][0] == '#') { | |
281 | continue; | |
282 | } | |
283 | ||
54aa3de7 | 284 | QAPI_LIST_PREPEND(ret->keys, g_strdup(authkeys[i])); |
cad97c08 MAL |
285 | } |
286 | ||
287 | return g_steal_pointer(&ret); | |
288 | } | |
8d769ec7 MAL |
289 | |
290 | #ifdef QGA_BUILD_UNIT_TEST | |
291 | #if GLIB_CHECK_VERSION(2, 60, 0) | |
292 | static const strList test_key2 = { | |
293 | .value = (char *)"algo key2 comments" | |
294 | }; | |
295 | ||
296 | static const strList test_key1_2 = { | |
297 | .value = (char *)"algo key1 comments", | |
298 | .next = (strList *)&test_key2, | |
299 | }; | |
300 | ||
301 | static char * | |
302 | test_get_authorized_keys_path(void) | |
303 | { | |
304 | return g_build_filename(g_get_home_dir(), ".ssh", "authorized_keys", NULL); | |
305 | } | |
306 | ||
307 | static void | |
308 | test_authorized_keys_set(const char *contents) | |
309 | { | |
310 | g_autoptr(GError) err = NULL; | |
311 | g_autofree char *path = NULL; | |
312 | int ret; | |
313 | ||
314 | path = g_build_filename(g_get_home_dir(), ".ssh", NULL); | |
315 | ret = g_mkdir_with_parents(path, 0700); | |
316 | g_assert(ret == 0); | |
317 | g_free(path); | |
318 | ||
319 | path = test_get_authorized_keys_path(); | |
320 | g_file_set_contents(path, contents, -1, &err); | |
321 | g_assert(err == NULL); | |
322 | } | |
323 | ||
324 | static void | |
325 | test_authorized_keys_equal(const char *expected) | |
326 | { | |
327 | g_autoptr(GError) err = NULL; | |
328 | g_autofree char *path = NULL; | |
329 | g_autofree char *contents = NULL; | |
330 | ||
331 | path = test_get_authorized_keys_path(); | |
332 | g_file_get_contents(path, &contents, NULL, &err); | |
333 | g_assert(err == NULL); | |
334 | ||
335 | g_assert(g_strcmp0(contents, expected) == 0); | |
336 | } | |
337 | ||
338 | static void | |
339 | test_invalid_user(void) | |
340 | { | |
341 | Error *err = NULL; | |
342 | ||
0e3c9475 | 343 | qmp_guest_ssh_add_authorized_keys("", NULL, FALSE, FALSE, &err); |
8d769ec7 MAL |
344 | error_free_or_abort(&err); |
345 | ||
346 | qmp_guest_ssh_remove_authorized_keys("", NULL, &err); | |
347 | error_free_or_abort(&err); | |
348 | } | |
349 | ||
350 | static void | |
351 | test_invalid_key(void) | |
352 | { | |
353 | strList key = { | |
354 | .value = (char *)"not a valid\nkey" | |
355 | }; | |
356 | Error *err = NULL; | |
357 | ||
0e3c9475 MR |
358 | qmp_guest_ssh_add_authorized_keys(g_get_user_name(), &key, |
359 | FALSE, FALSE, &err); | |
8d769ec7 MAL |
360 | error_free_or_abort(&err); |
361 | ||
362 | qmp_guest_ssh_remove_authorized_keys(g_get_user_name(), &key, &err); | |
363 | error_free_or_abort(&err); | |
364 | } | |
365 | ||
366 | static void | |
367 | test_add_keys(void) | |
368 | { | |
369 | Error *err = NULL; | |
370 | ||
371 | qmp_guest_ssh_add_authorized_keys(g_get_user_name(), | |
0e3c9475 MR |
372 | (strList *)&test_key2, |
373 | FALSE, FALSE, | |
374 | &err); | |
8d769ec7 MAL |
375 | g_assert(err == NULL); |
376 | ||
377 | test_authorized_keys_equal("algo key2 comments"); | |
378 | ||
379 | qmp_guest_ssh_add_authorized_keys(g_get_user_name(), | |
0e3c9475 MR |
380 | (strList *)&test_key1_2, |
381 | FALSE, FALSE, | |
382 | &err); | |
8d769ec7 MAL |
383 | g_assert(err == NULL); |
384 | ||
01dc0651 | 385 | /* key2 came first, and shouldn't be duplicated */ |
8d769ec7 MAL |
386 | test_authorized_keys_equal("algo key2 comments\n" |
387 | "algo key1 comments"); | |
388 | } | |
389 | ||
0e3c9475 MR |
390 | static void |
391 | test_add_reset_keys(void) | |
392 | { | |
393 | Error *err = NULL; | |
394 | ||
395 | qmp_guest_ssh_add_authorized_keys(g_get_user_name(), | |
396 | (strList *)&test_key1_2, | |
397 | FALSE, FALSE, | |
398 | &err); | |
399 | g_assert(err == NULL); | |
400 | ||
401 | /* reset with key2 only */ | |
402 | test_authorized_keys_equal("algo key1 comments\n" | |
403 | "algo key2 comments"); | |
404 | ||
405 | qmp_guest_ssh_add_authorized_keys(g_get_user_name(), | |
406 | (strList *)&test_key2, | |
407 | TRUE, TRUE, | |
408 | &err); | |
409 | g_assert(err == NULL); | |
410 | ||
411 | test_authorized_keys_equal("algo key2 comments"); | |
412 | ||
413 | /* empty should clear file */ | |
414 | qmp_guest_ssh_add_authorized_keys(g_get_user_name(), | |
415 | (strList *)NULL, | |
416 | TRUE, TRUE, | |
417 | &err); | |
418 | g_assert(err == NULL); | |
419 | ||
420 | test_authorized_keys_equal(""); | |
421 | } | |
422 | ||
8d769ec7 MAL |
423 | static void |
424 | test_remove_keys(void) | |
425 | { | |
426 | Error *err = NULL; | |
427 | static const char *authkeys = | |
428 | "algo key1 comments\n" | |
429 | /* originally duplicated */ | |
430 | "algo key1 comments\n" | |
431 | "# a commented line\n" | |
432 | "algo some-key another\n"; | |
433 | ||
434 | test_authorized_keys_set(authkeys); | |
435 | qmp_guest_ssh_remove_authorized_keys(g_get_user_name(), | |
436 | (strList *)&test_key2, &err); | |
437 | g_assert(err == NULL); | |
438 | test_authorized_keys_equal(authkeys); | |
439 | ||
440 | qmp_guest_ssh_remove_authorized_keys(g_get_user_name(), | |
441 | (strList *)&test_key1_2, &err); | |
442 | g_assert(err == NULL); | |
443 | test_authorized_keys_equal("# a commented line\n" | |
444 | "algo some-key another\n"); | |
445 | } | |
446 | ||
cad97c08 MAL |
447 | static void |
448 | test_get_keys(void) | |
449 | { | |
450 | Error *err = NULL; | |
451 | static const char *authkeys = | |
452 | "algo key1 comments\n" | |
453 | "# a commented line\n" | |
454 | "algo some-key another\n"; | |
455 | g_autoptr(GuestAuthorizedKeys) ret = NULL; | |
456 | strList *k; | |
457 | size_t len = 0; | |
458 | ||
459 | test_authorized_keys_set(authkeys); | |
460 | ||
461 | ret = qmp_guest_ssh_get_authorized_keys(g_get_user_name(), &err); | |
462 | g_assert(err == NULL); | |
463 | ||
464 | for (len = 0, k = ret->keys; k != NULL; k = k->next) { | |
465 | g_assert(g_str_has_prefix(k->value, "algo ")); | |
466 | len++; | |
467 | } | |
468 | ||
469 | g_assert(len == 2); | |
470 | } | |
471 | ||
8d769ec7 MAL |
472 | int main(int argc, char *argv[]) |
473 | { | |
474 | setlocale(LC_ALL, ""); | |
475 | ||
476 | g_test_init(&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL); | |
477 | ||
478 | g_test_add_func("/qga/ssh/invalid_user", test_invalid_user); | |
479 | g_test_add_func("/qga/ssh/invalid_key", test_invalid_key); | |
480 | g_test_add_func("/qga/ssh/add_keys", test_add_keys); | |
0e3c9475 | 481 | g_test_add_func("/qga/ssh/add_reset_keys", test_add_reset_keys); |
8d769ec7 | 482 | g_test_add_func("/qga/ssh/remove_keys", test_remove_keys); |
cad97c08 | 483 | g_test_add_func("/qga/ssh/get_keys", test_get_keys); |
8d769ec7 MAL |
484 | |
485 | return g_test_run(); | |
486 | } | |
487 | #else | |
488 | int main(int argc, char *argv[]) | |
489 | { | |
490 | g_test_message("test skipped, needs glib >= 2.60"); | |
491 | return 0; | |
492 | } | |
493 | #endif /* GLIB_2_60 */ | |
494 | #endif /* BUILD_UNIT_TEST */ |