]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/userdb/userdbctl.c
918b4d772f28ea126783e0fad82945a2ff32880e
[thirdparty/systemd.git] / src / userdb / userdbctl.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #include <getopt.h>
4 #include <utmp.h>
5
6 #include "build.h"
7 #include "dirent-util.h"
8 #include "errno-list.h"
9 #include "escape.h"
10 #include "fd-util.h"
11 #include "format-table.h"
12 #include "format-util.h"
13 #include "main-func.h"
14 #include "pager.h"
15 #include "parse-argument.h"
16 #include "parse-util.h"
17 #include "pretty-print.h"
18 #include "socket-util.h"
19 #include "strv.h"
20 #include "terminal-util.h"
21 #include "uid-range.h"
22 #include "user-record-show.h"
23 #include "user-util.h"
24 #include "userdb.h"
25 #include "verbs.h"
26
27 static enum {
28 OUTPUT_CLASSIC,
29 OUTPUT_TABLE,
30 OUTPUT_FRIENDLY,
31 OUTPUT_JSON,
32 _OUTPUT_INVALID = -EINVAL,
33 } arg_output = _OUTPUT_INVALID;
34
35 static PagerFlags arg_pager_flags = 0;
36 static bool arg_legend = true;
37 static char** arg_services = NULL;
38 static UserDBFlags arg_userdb_flags = 0;
39 static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
40 static bool arg_chain = false;
41
42 STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep);
43
44 static const char *user_disposition_to_color(UserDisposition d) {
45 assert(d >= 0);
46 assert(d < _USER_DISPOSITION_MAX);
47
48 switch (d) {
49 case USER_INTRINSIC:
50 return ansi_red();
51
52 case USER_SYSTEM:
53 case USER_DYNAMIC:
54 return ansi_green();
55
56 case USER_CONTAINER:
57 return ansi_cyan();
58
59 case USER_RESERVED:
60 return ansi_red();
61
62 default:
63 return NULL;
64 }
65 }
66
67 static int show_user(UserRecord *ur, Table *table) {
68 int r;
69
70 assert(ur);
71
72 switch (arg_output) {
73
74 case OUTPUT_CLASSIC:
75 if (!uid_is_valid(ur->uid))
76 break;
77
78 printf("%s:x:" UID_FMT ":" GID_FMT ":%s:%s:%s\n",
79 ur->user_name,
80 ur->uid,
81 user_record_gid(ur),
82 strempty(user_record_real_name(ur)),
83 user_record_home_directory(ur),
84 user_record_shell(ur));
85
86 break;
87
88 case OUTPUT_JSON:
89 json_variant_dump(ur->json, arg_json_format_flags, NULL, 0);
90 break;
91
92 case OUTPUT_FRIENDLY:
93 user_record_show(ur, true);
94
95 if (ur->incomplete) {
96 fflush(stdout);
97 log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name);
98 }
99
100 break;
101
102 case OUTPUT_TABLE: {
103 UserDisposition d;
104
105 assert(table);
106 d = user_record_disposition(ur);
107
108 r = table_add_many(
109 table,
110 TABLE_STRING, "",
111 TABLE_STRING, ur->user_name,
112 TABLE_SET_COLOR, user_disposition_to_color(d),
113 TABLE_STRING, user_disposition_to_string(d),
114 TABLE_UID, ur->uid,
115 TABLE_GID, user_record_gid(ur),
116 TABLE_STRING, empty_to_null(ur->real_name),
117 TABLE_STRING, user_record_home_directory(ur),
118 TABLE_STRING, user_record_shell(ur),
119 TABLE_INT, 0);
120 if (r < 0)
121 return table_log_add_error(r);
122
123 break;
124 }
125
126 default:
127 assert_not_reached();
128 }
129
130 return 0;
131 }
132
133 static const struct {
134 uid_t first, last;
135 const char *name;
136 UserDisposition disposition;
137 } uid_range_table[] = {
138 {
139 .first = 1,
140 .last = SYSTEM_UID_MAX,
141 .name = "system",
142 .disposition = USER_SYSTEM,
143 },
144 {
145 .first = DYNAMIC_UID_MIN,
146 .last = DYNAMIC_UID_MAX,
147 .name = "dynamic system",
148 .disposition = USER_DYNAMIC,
149 },
150 {
151 .first = CONTAINER_UID_BASE_MIN,
152 .last = CONTAINER_UID_BASE_MAX,
153 .name = "container",
154 .disposition = USER_CONTAINER,
155 },
156 #if ENABLE_HOMED
157 {
158 .first = HOME_UID_MIN,
159 .last = HOME_UID_MAX,
160 .name = "systemd-homed",
161 .disposition = USER_REGULAR,
162 },
163 #endif
164 {
165 .first = MAP_UID_MIN,
166 .last = MAP_UID_MAX,
167 .name = "mapped",
168 .disposition = USER_REGULAR,
169 },
170 };
171
172 static int table_add_uid_boundaries(Table *table, const UIDRange *p) {
173 int r;
174
175 assert(table);
176
177 FOREACH_ARRAY(i, uid_range_table, ELEMENTSOF(uid_range_table)) {
178 _cleanup_free_ char *name = NULL, *comment = NULL;
179
180 if (!uid_range_covers(p, i->first, i->last - i->first + 1))
181 continue;
182
183 name = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_DOWN),
184 " begin ", i->name, " users ",
185 special_glyph(SPECIAL_GLYPH_ARROW_DOWN));
186 if (!name)
187 return log_oom();
188
189 comment = strjoin("First ", i->name, " user");
190 if (!comment)
191 return log_oom();
192
193 r = table_add_many(
194 table,
195 TABLE_STRING, special_glyph(SPECIAL_GLYPH_TREE_TOP),
196 TABLE_STRING, name,
197 TABLE_SET_COLOR, ansi_grey(),
198 TABLE_STRING, user_disposition_to_string(i->disposition),
199 TABLE_SET_COLOR, ansi_grey(),
200 TABLE_UID, i->first,
201 TABLE_SET_COLOR, ansi_grey(),
202 TABLE_EMPTY,
203 TABLE_STRING, comment,
204 TABLE_SET_COLOR, ansi_grey(),
205 TABLE_EMPTY,
206 TABLE_EMPTY,
207 TABLE_INT, -1); /* sort before any other entry with the same UID */
208 if (r < 0)
209 return table_log_add_error(r);
210
211 free(name);
212 name = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_UP),
213 " end ", i->name, " users ",
214 special_glyph(SPECIAL_GLYPH_ARROW_UP));
215 if (!name)
216 return log_oom();
217
218 free(comment);
219 comment = strjoin("Last ", i->name, " user");
220 if (!comment)
221 return log_oom();
222
223 r = table_add_many(
224 table,
225 TABLE_STRING, special_glyph(SPECIAL_GLYPH_TREE_RIGHT),
226 TABLE_STRING, name,
227 TABLE_SET_COLOR, ansi_grey(),
228 TABLE_STRING, user_disposition_to_string(i->disposition),
229 TABLE_SET_COLOR, ansi_grey(),
230 TABLE_UID, i->last,
231 TABLE_SET_COLOR, ansi_grey(),
232 TABLE_EMPTY,
233 TABLE_STRING, comment,
234 TABLE_SET_COLOR, ansi_grey(),
235 TABLE_EMPTY,
236 TABLE_EMPTY,
237 TABLE_INT, 1); /* sort after any other entry with the same UID */
238 if (r < 0)
239 return table_log_add_error(r);
240 }
241
242 return ELEMENTSOF(uid_range_table) * 2;
243 }
244
245 static int add_unavailable_uid(Table *table, uid_t start, uid_t end) {
246 _cleanup_free_ char *name = NULL;
247 int r;
248
249 assert(table);
250 assert(start <= end);
251
252 name = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_DOWN),
253 " begin unavailable users ",
254 special_glyph(SPECIAL_GLYPH_ARROW_DOWN));
255 if (!name)
256 return log_oom();
257
258 r = table_add_many(
259 table,
260 TABLE_STRING, special_glyph(SPECIAL_GLYPH_TREE_TOP),
261 TABLE_STRING, name,
262 TABLE_SET_COLOR, ansi_grey(),
263 TABLE_EMPTY,
264 TABLE_UID, start,
265 TABLE_SET_COLOR, ansi_grey(),
266 TABLE_EMPTY,
267 TABLE_STRING, "First unavailable user",
268 TABLE_SET_COLOR, ansi_grey(),
269 TABLE_EMPTY,
270 TABLE_EMPTY,
271 TABLE_INT, -1); /* sort before an other entry with the same UID */
272 if (r < 0)
273 return table_log_add_error(r);
274
275 free(name);
276 name = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_UP),
277 " end unavailable users ",
278 special_glyph(SPECIAL_GLYPH_ARROW_UP));
279 if (!name)
280 return log_oom();
281
282 r = table_add_many(
283 table,
284 TABLE_STRING, special_glyph(SPECIAL_GLYPH_TREE_RIGHT),
285 TABLE_STRING, name,
286 TABLE_SET_COLOR, ansi_grey(),
287 TABLE_EMPTY,
288 TABLE_UID, end,
289 TABLE_SET_COLOR, ansi_grey(),
290 TABLE_EMPTY,
291 TABLE_STRING, "Last unavailable user",
292 TABLE_SET_COLOR, ansi_grey(),
293 TABLE_EMPTY,
294 TABLE_EMPTY,
295 TABLE_INT, 1); /* sort after any other entry with the same UID */
296 if (r < 0)
297 return table_log_add_error(r);
298
299 return 2;
300 }
301
302 static int table_add_uid_map(
303 Table *table,
304 const UIDRange *p,
305 int (*add_unavailable)(Table *t, uid_t start, uid_t end)) {
306
307 uid_t focus = 0;
308 int n_added = 0, r;
309
310 assert(table);
311 assert(add_unavailable);
312
313 if (!p)
314 return 0;
315
316 FOREACH_ARRAY(x, p->entries, p->n_entries) {
317 if (focus < x->start) {
318 r = add_unavailable(table, focus, x->start-1);
319 if (r < 0)
320 return r;
321
322 n_added += r;
323 }
324
325 if (x->start > UINT32_MAX - x->nr) { /* overflow check */
326 focus = UINT32_MAX;
327 break;
328 }
329
330 focus = x->start + x->nr;
331 }
332
333 if (focus < UINT32_MAX-1) {
334 r = add_unavailable(table, focus, UINT32_MAX-1);
335 if (r < 0)
336 return r;
337
338 n_added += r;
339 }
340
341 return n_added;
342 }
343
344 static int display_user(int argc, char *argv[], void *userdata) {
345 _cleanup_(table_unrefp) Table *table = NULL;
346 bool draw_separator = false;
347 int ret = 0, r;
348
349 if (arg_output < 0)
350 arg_output = argc > 1 ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
351
352 if (arg_output == OUTPUT_TABLE) {
353 table = table_new(" ", "name", "disposition", "uid", "gid", "realname", "home", "shell", "order");
354 if (!table)
355 return log_oom();
356
357 (void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100);
358 (void) table_set_align_percent(table, table_get_cell(table, 0, 4), 100);
359 table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
360 (void) table_set_sort(table, (size_t) 3, (size_t) 8);
361 (void) table_set_display(table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 4, (size_t) 5, (size_t) 6, (size_t) 7);
362 }
363
364 if (argc > 1)
365 STRV_FOREACH(i, argv + 1) {
366 _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
367 uid_t uid;
368
369 if (parse_uid(*i, &uid) >= 0)
370 r = userdb_by_uid(uid, arg_userdb_flags, &ur);
371 else
372 r = userdb_by_name(*i, arg_userdb_flags, &ur);
373 if (r < 0) {
374 if (r == -ESRCH)
375 log_error_errno(r, "User %s does not exist.", *i);
376 else if (r == -EHOSTDOWN)
377 log_error_errno(r, "Selected user database service is not available for this request.");
378 else
379 log_error_errno(r, "Failed to find user %s: %m", *i);
380
381 if (ret >= 0)
382 ret = r;
383 } else {
384 if (draw_separator && arg_output == OUTPUT_FRIENDLY)
385 putchar('\n');
386
387 r = show_user(ur, table);
388 if (r < 0)
389 return r;
390
391 draw_separator = true;
392 }
393 }
394 else {
395 _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
396
397 r = userdb_all(arg_userdb_flags, &iterator);
398 if (r == -ENOLINK) /* ENOLINK → Didn't find answer without Varlink, and didn't try Varlink because was configured to off. */
399 log_debug_errno(r, "No entries found. (Didn't check via Varlink.)");
400 else if (r == -ESRCH) /* ESRCH → Couldn't find any suitable entry, but we checked all sources */
401 log_debug_errno(r, "No entries found.");
402 else if (r < 0)
403 return log_error_errno(r, "Failed to enumerate users: %m");
404 else {
405 for (;;) {
406 _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
407
408 r = userdb_iterator_get(iterator, &ur);
409 if (r == -ESRCH)
410 break;
411 if (r == -EHOSTDOWN)
412 return log_error_errno(r, "Selected user database service is not available for this request.");
413 if (r < 0)
414 return log_error_errno(r, "Failed acquire next user: %m");
415
416 if (draw_separator && arg_output == OUTPUT_FRIENDLY)
417 putchar('\n');
418
419 r = show_user(ur, table);
420 if (r < 0)
421 return r;
422
423 draw_separator = true;
424 }
425 }
426 }
427
428 if (table) {
429 _cleanup_(uid_range_freep) UIDRange *uid_range = NULL;
430 int boundary_lines, uid_map_lines;
431
432 r = uid_range_load_userns(/* path = */ NULL, UID_RANGE_USERNS_INSIDE, &uid_range);
433 if (r < 0)
434 log_debug_errno(r, "Failed to load /proc/self/uid_map, ignoring: %m");
435
436 boundary_lines = table_add_uid_boundaries(table, uid_range);
437 if (boundary_lines < 0)
438 return boundary_lines;
439
440 uid_map_lines = table_add_uid_map(table, uid_range, add_unavailable_uid);
441 if (uid_map_lines < 0)
442 return uid_map_lines;
443
444 if (!table_isempty(table)) {
445 r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
446 if (r < 0)
447 return table_log_print_error(r);
448 }
449
450 if (arg_legend) {
451 size_t k;
452
453 k = table_get_rows(table) - 1 - boundary_lines - uid_map_lines;
454 if (k > 0)
455 printf("\n%zu users listed.\n", k);
456 else
457 printf("No users.\n");
458 }
459 }
460
461 return ret;
462 }
463
464 static int show_group(GroupRecord *gr, Table *table) {
465 int r;
466
467 assert(gr);
468
469 switch (arg_output) {
470
471 case OUTPUT_CLASSIC: {
472 _cleanup_free_ char *m = NULL;
473
474 if (!gid_is_valid(gr->gid))
475 break;
476
477 m = strv_join(gr->members, ",");
478 if (!m)
479 return log_oom();
480
481 printf("%s:x:" GID_FMT ":%s\n",
482 gr->group_name,
483 gr->gid,
484 m);
485 break;
486 }
487
488 case OUTPUT_JSON:
489 json_variant_dump(gr->json, arg_json_format_flags, NULL, 0);
490 break;
491
492 case OUTPUT_FRIENDLY:
493 group_record_show(gr, true);
494
495 if (gr->incomplete) {
496 fflush(stdout);
497 log_warning("Warning: lacking rights to acquire privileged fields of group record of '%s', output incomplete.", gr->group_name);
498 }
499
500 break;
501
502 case OUTPUT_TABLE: {
503 UserDisposition d;
504
505 assert(table);
506 d = group_record_disposition(gr);
507
508 r = table_add_many(
509 table,
510 TABLE_STRING, "",
511 TABLE_STRING, gr->group_name,
512 TABLE_SET_COLOR, user_disposition_to_color(d),
513 TABLE_STRING, user_disposition_to_string(d),
514 TABLE_GID, gr->gid,
515 TABLE_STRING, gr->description,
516 TABLE_INT, 0);
517 if (r < 0)
518 return table_log_add_error(r);
519
520 break;
521 }
522
523 default:
524 assert_not_reached();
525 }
526
527 return 0;
528 }
529
530 static int table_add_gid_boundaries(Table *table, const UIDRange *p) {
531 int r;
532
533 assert(table);
534
535 FOREACH_ARRAY(i, uid_range_table, ELEMENTSOF(uid_range_table)) {
536 _cleanup_free_ char *name = NULL, *comment = NULL;
537
538 if (!uid_range_covers(p, i->first, i->last - i->first + 1))
539 continue;
540
541 name = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_DOWN),
542 " begin ", i->name, " groups ",
543 special_glyph(SPECIAL_GLYPH_ARROW_DOWN));
544 if (!name)
545 return log_oom();
546
547 comment = strjoin("First ", i->name, " group");
548 if (!comment)
549 return log_oom();
550
551 r = table_add_many(
552 table,
553 TABLE_STRING, special_glyph(SPECIAL_GLYPH_TREE_TOP),
554 TABLE_STRING, name,
555 TABLE_SET_COLOR, ansi_grey(),
556 TABLE_STRING, user_disposition_to_string(i->disposition),
557 TABLE_SET_COLOR, ansi_grey(),
558 TABLE_GID, i->first,
559 TABLE_SET_COLOR, ansi_grey(),
560 TABLE_STRING, comment,
561 TABLE_SET_COLOR, ansi_grey(),
562 TABLE_INT, -1); /* sort before any other entry with the same GID */
563 if (r < 0)
564 return table_log_add_error(r);
565
566 free(name);
567 name = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_UP),
568 " end ", i->name, " groups ",
569 special_glyph(SPECIAL_GLYPH_ARROW_UP));
570 if (!name)
571 return log_oom();
572
573 free(comment);
574 comment = strjoin("Last ", i->name, " group");
575 if (!comment)
576 return log_oom();
577
578 r = table_add_many(
579 table,
580 TABLE_STRING, special_glyph(SPECIAL_GLYPH_TREE_RIGHT),
581 TABLE_STRING, name,
582 TABLE_SET_COLOR, ansi_grey(),
583 TABLE_STRING, user_disposition_to_string(i->disposition),
584 TABLE_SET_COLOR, ansi_grey(),
585 TABLE_GID, i->last,
586 TABLE_SET_COLOR, ansi_grey(),
587 TABLE_STRING, comment,
588 TABLE_SET_COLOR, ansi_grey(),
589 TABLE_INT, 1); /* sort after any other entry with the same GID */
590 if (r < 0)
591 return table_log_add_error(r);
592 }
593
594 return ELEMENTSOF(uid_range_table) * 2;
595 }
596
597 static int add_unavailable_gid(Table *table, uid_t start, uid_t end) {
598 _cleanup_free_ char *name = NULL;
599 int r;
600
601 assert(table);
602 assert(start <= end);
603
604 name = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_DOWN),
605 " begin unavailable groups ",
606 special_glyph(SPECIAL_GLYPH_ARROW_DOWN));
607 if (!name)
608 return log_oom();
609
610 r = table_add_many(
611 table,
612 TABLE_STRING, special_glyph(SPECIAL_GLYPH_TREE_TOP),
613 TABLE_STRING, name,
614 TABLE_SET_COLOR, ansi_grey(),
615 TABLE_EMPTY,
616 TABLE_GID, start,
617 TABLE_SET_COLOR, ansi_grey(),
618 TABLE_STRING, "First unavailable group",
619 TABLE_SET_COLOR, ansi_grey(),
620 TABLE_INT, -1); /* sort before any other entry with the same GID */
621 if (r < 0)
622 return table_log_add_error(r);
623
624 free(name);
625 name = strjoin(special_glyph(SPECIAL_GLYPH_ARROW_UP),
626 " end unavailable groups ",
627 special_glyph(SPECIAL_GLYPH_ARROW_UP));
628 if (!name)
629 return log_oom();
630
631 r = table_add_many(
632 table,
633 TABLE_STRING, special_glyph(SPECIAL_GLYPH_TREE_RIGHT),
634 TABLE_STRING, name,
635 TABLE_SET_COLOR, ansi_grey(),
636 TABLE_EMPTY,
637 TABLE_GID, end,
638 TABLE_SET_COLOR, ansi_grey(),
639 TABLE_STRING, "Last unavailable group",
640 TABLE_SET_COLOR, ansi_grey(),
641 TABLE_INT, 1); /* sort after any other entry with the same GID */
642 if (r < 0)
643 return table_log_add_error(r);
644
645 return 2;
646 }
647
648 static int display_group(int argc, char *argv[], void *userdata) {
649 _cleanup_(table_unrefp) Table *table = NULL;
650 bool draw_separator = false;
651 int ret = 0, r;
652
653 if (arg_output < 0)
654 arg_output = argc > 1 ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
655
656 if (arg_output == OUTPUT_TABLE) {
657 table = table_new(" ", "name", "disposition", "gid", "description", "order");
658 if (!table)
659 return log_oom();
660
661 (void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100);
662 table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
663 (void) table_set_sort(table, (size_t) 3, (size_t) 5);
664 (void) table_set_display(table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 4);
665 }
666
667 if (argc > 1)
668 STRV_FOREACH(i, argv + 1) {
669 _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
670 gid_t gid;
671
672 if (parse_gid(*i, &gid) >= 0)
673 r = groupdb_by_gid(gid, arg_userdb_flags, &gr);
674 else
675 r = groupdb_by_name(*i, arg_userdb_flags, &gr);
676 if (r < 0) {
677 if (r == -ESRCH)
678 log_error_errno(r, "Group %s does not exist.", *i);
679 else if (r == -EHOSTDOWN)
680 log_error_errno(r, "Selected group database service is not available for this request.");
681 else
682 log_error_errno(r, "Failed to find group %s: %m", *i);
683
684 if (ret >= 0)
685 ret = r;
686 } else {
687 if (draw_separator && arg_output == OUTPUT_FRIENDLY)
688 putchar('\n');
689
690 r = show_group(gr, table);
691 if (r < 0)
692 return r;
693
694 draw_separator = true;
695 }
696 }
697 else {
698 _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
699
700 r = groupdb_all(arg_userdb_flags, &iterator);
701 if (r == -ENOLINK)
702 log_debug_errno(r, "No entries found. (Didn't check via Varlink.)");
703 else if (r == -ESRCH)
704 log_debug_errno(r, "No entries found.");
705 else if (r < 0)
706 return log_error_errno(r, "Failed to enumerate groups: %m");
707 else {
708 for (;;) {
709 _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
710
711 r = groupdb_iterator_get(iterator, &gr);
712 if (r == -ESRCH)
713 break;
714 if (r == -EHOSTDOWN)
715 return log_error_errno(r, "Selected group database service is not available for this request.");
716 if (r < 0)
717 return log_error_errno(r, "Failed acquire next group: %m");
718
719 if (draw_separator && arg_output == OUTPUT_FRIENDLY)
720 putchar('\n');
721
722 r = show_group(gr, table);
723 if (r < 0)
724 return r;
725
726 draw_separator = true;
727 }
728 }
729 }
730
731 if (table) {
732 _cleanup_(uid_range_freep) UIDRange *gid_range = NULL;
733 int boundary_lines, gid_map_lines;
734
735 r = uid_range_load_userns(/* path = */ NULL, GID_RANGE_USERNS_INSIDE, &gid_range);
736 if (r < 0)
737 log_debug_errno(r, "Failed to load /proc/self/gid_map, ignoring: %m");
738
739 boundary_lines = table_add_gid_boundaries(table, gid_range);
740 if (boundary_lines < 0)
741 return boundary_lines;
742
743 gid_map_lines = table_add_uid_map(table, gid_range, add_unavailable_gid);
744 if (gid_map_lines < 0)
745 return gid_map_lines;
746
747 if (!table_isempty(table)) {
748 r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
749 if (r < 0)
750 return table_log_print_error(r);
751 }
752
753 if (arg_legend) {
754 size_t k;
755
756 k = table_get_rows(table) - 1 - boundary_lines - gid_map_lines;
757 if (k > 0)
758 printf("\n%zu groups listed.\n", k);
759 else
760 printf("No groups.\n");
761 }
762 }
763
764 return ret;
765 }
766
767 static int show_membership(const char *user, const char *group, Table *table) {
768 int r;
769
770 assert(user);
771 assert(group);
772
773 switch (arg_output) {
774
775 case OUTPUT_CLASSIC:
776 /* Strictly speaking there's no 'classic' output for this concept, but let's output it in
777 * similar style to the classic output for user/group info */
778
779 printf("%s:%s\n", user, group);
780 break;
781
782 case OUTPUT_JSON: {
783 _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
784
785 r = json_build(&v, JSON_BUILD_OBJECT(
786 JSON_BUILD_PAIR("user", JSON_BUILD_STRING(user)),
787 JSON_BUILD_PAIR("group", JSON_BUILD_STRING(group))));
788 if (r < 0)
789 return log_error_errno(r, "Failed to build JSON object: %m");
790
791 json_variant_dump(v, arg_json_format_flags, NULL, NULL);
792 break;
793 }
794
795 case OUTPUT_FRIENDLY:
796 /* Hmm, this is not particularly friendly, but not sure how we could do this better */
797 printf("%s: %s\n", group, user);
798 break;
799
800 case OUTPUT_TABLE:
801 assert(table);
802
803 r = table_add_many(
804 table,
805 TABLE_STRING, user,
806 TABLE_STRING, group);
807 if (r < 0)
808 return table_log_add_error(r);
809
810 break;
811
812 default:
813 assert_not_reached();
814 }
815
816 return 0;
817 }
818
819 static int display_memberships(int argc, char *argv[], void *userdata) {
820 _cleanup_(table_unrefp) Table *table = NULL;
821 int ret = 0, r;
822
823 if (arg_output < 0)
824 arg_output = OUTPUT_TABLE;
825
826 if (arg_output == OUTPUT_TABLE) {
827 table = table_new("user", "group");
828 if (!table)
829 return log_oom();
830
831 (void) table_set_sort(table, (size_t) 0, (size_t) 1);
832 }
833
834 if (argc > 1)
835 STRV_FOREACH(i, argv + 1) {
836 _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
837
838 if (streq(argv[0], "users-in-group")) {
839 r = membershipdb_by_group(*i, arg_userdb_flags, &iterator);
840 if (r < 0)
841 return log_error_errno(r, "Failed to enumerate users in group: %m");
842 } else if (streq(argv[0], "groups-of-user")) {
843 r = membershipdb_by_user(*i, arg_userdb_flags, &iterator);
844 if (r < 0)
845 return log_error_errno(r, "Failed to enumerate groups of user: %m");
846 } else
847 assert_not_reached();
848
849 for (;;) {
850 _cleanup_free_ char *user = NULL, *group = NULL;
851
852 r = membershipdb_iterator_get(iterator, &user, &group);
853 if (r == -ESRCH)
854 break;
855 if (r == -EHOSTDOWN)
856 return log_error_errno(r, "Selected membership database service is not available for this request.");
857 if (r < 0)
858 return log_error_errno(r, "Failed acquire next membership: %m");
859
860 r = show_membership(user, group, table);
861 if (r < 0)
862 return r;
863 }
864 }
865 else {
866 _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
867
868 r = membershipdb_all(arg_userdb_flags, &iterator);
869 if (r == -ENOLINK)
870 log_debug_errno(r, "No entries found. (Didn't check via Varlink.)");
871 else if (r == -ESRCH)
872 log_debug_errno(r, "No entries found.");
873 else if (r < 0)
874 return log_error_errno(r, "Failed to enumerate memberships: %m");
875 else {
876 for (;;) {
877 _cleanup_free_ char *user = NULL, *group = NULL;
878
879 r = membershipdb_iterator_get(iterator, &user, &group);
880 if (r == -ESRCH)
881 break;
882 if (r == -EHOSTDOWN)
883 return log_error_errno(r, "Selected membership database service is not available for this request.");
884 if (r < 0)
885 return log_error_errno(r, "Failed acquire next membership: %m");
886
887 r = show_membership(user, group, table);
888 if (r < 0)
889 return r;
890 }
891 }
892 }
893
894 if (table) {
895 if (!table_isempty(table)) {
896 r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
897 if (r < 0)
898 return table_log_print_error(r);
899 }
900
901 if (arg_legend) {
902 if (table_isempty(table))
903 printf("No memberships.\n");
904 else
905 printf("\n%zu memberships listed.\n", table_get_rows(table) - 1);
906 }
907 }
908
909 return ret;
910 }
911
912 static int display_services(int argc, char *argv[], void *userdata) {
913 _cleanup_(table_unrefp) Table *t = NULL;
914 _cleanup_closedir_ DIR *d = NULL;
915 int r;
916
917 d = opendir("/run/systemd/userdb/");
918 if (!d) {
919 if (errno == ENOENT) {
920 log_info("No services.");
921 return 0;
922 }
923
924 return log_error_errno(errno, "Failed to open /run/systemd/userdb/: %m");
925 }
926
927 t = table_new("service", "listening");
928 if (!t)
929 return log_oom();
930
931 (void) table_set_sort(t, (size_t) 0);
932
933 FOREACH_DIRENT(de, d, return -errno) {
934 _cleanup_free_ char *j = NULL, *no = NULL;
935 _cleanup_close_ int fd = -EBADF;
936
937 j = path_join("/run/systemd/userdb/", de->d_name);
938 if (!j)
939 return log_oom();
940
941 fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
942 if (fd < 0)
943 return log_error_errno(errno, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m");
944
945 r = connect_unix_path(fd, dirfd(d), de->d_name);
946 if (r < 0) {
947 no = strjoin("No (", errno_to_name(r), ")");
948 if (!no)
949 return log_oom();
950 }
951
952 r = table_add_many(t,
953 TABLE_STRING, de->d_name,
954 TABLE_STRING, no ?: "yes",
955 TABLE_SET_COLOR, no ? ansi_highlight_red() : ansi_highlight_green());
956 if (r < 0)
957 return table_log_add_error(r);
958 }
959
960 if (!table_isempty(t)) {
961 r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
962 if (r < 0)
963 return table_log_print_error(r);
964 }
965
966 if (arg_legend && arg_output != OUTPUT_JSON) {
967 if (table_isempty(t))
968 printf("No services.\n");
969 else
970 printf("\n%zu services listed.\n", table_get_rows(t) - 1);
971 }
972
973 return 0;
974 }
975
976 static int ssh_authorized_keys(int argc, char *argv[], void *userdata) {
977 _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
978 char **chain_invocation;
979 int r;
980
981 assert(argc >= 2);
982
983 if (arg_chain) {
984 /* If --chain is specified, the rest of the command line is the chain command */
985
986 if (argc < 3)
987 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
988 "No chain command line specified, refusing.");
989
990 /* Make similar restrictions on the chain command as OpenSSH itself makes on the primary command. */
991 if (!path_is_absolute(argv[2]))
992 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
993 "Chain invocation of ssh-authorized-keys commands requires an absolute binary path argument.");
994
995 if (!path_is_normalized(argv[2]))
996 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
997 "Chain invocation of ssh-authorized-keys commands requires an normalized binary path argument.");
998
999 chain_invocation = argv + 2;
1000 } else {
1001 /* If --chain is not specified, then refuse any further arguments */
1002
1003 if (argc > 2)
1004 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments.");
1005
1006 chain_invocation = NULL;
1007 }
1008
1009 r = userdb_by_name(argv[1], arg_userdb_flags, &ur);
1010 if (r == -ESRCH)
1011 log_error_errno(r, "User %s does not exist.", argv[1]);
1012 else if (r == -EHOSTDOWN)
1013 log_error_errno(r, "Selected user database service is not available for this request.");
1014 else if (r == -EINVAL)
1015 log_error_errno(r, "Failed to find user %s: %m (Invalid user name?)", argv[1]);
1016 else if (r < 0)
1017 log_error_errno(r, "Failed to find user %s: %m", argv[1]);
1018 else {
1019 if (strv_isempty(ur->ssh_authorized_keys))
1020 log_debug("User record for %s has no public SSH keys.", argv[1]);
1021 else
1022 STRV_FOREACH(i, ur->ssh_authorized_keys)
1023 printf("%s\n", *i);
1024
1025 if (ur->incomplete) {
1026 fflush(stdout);
1027 log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name);
1028 }
1029 }
1030
1031 if (chain_invocation) {
1032 if (DEBUG_LOGGING) {
1033 _cleanup_free_ char *s = NULL;
1034
1035 s = quote_command_line(chain_invocation, SHELL_ESCAPE_EMPTY);
1036 if (!s)
1037 return log_oom();
1038
1039 log_debug("Chain invoking: %s", s);
1040 }
1041
1042 fflush(stdout);
1043 execv(chain_invocation[0], chain_invocation);
1044 if (errno == ENOENT) /* Let's handle ENOENT gracefully */
1045 log_warning_errno(errno, "Chain executable '%s' does not exist, ignoring chain invocation.", chain_invocation[0]);
1046 else {
1047 log_error_errno(errno, "Failed to invoke chain executable '%s': %m", chain_invocation[0]);
1048 if (r >= 0)
1049 r = -errno;
1050 }
1051 }
1052
1053 return r;
1054 }
1055
1056 static int help(int argc, char *argv[], void *userdata) {
1057 _cleanup_free_ char *link = NULL;
1058 int r;
1059
1060 pager_open(arg_pager_flags);
1061
1062 r = terminal_urlify_man("userdbctl", "1", &link);
1063 if (r < 0)
1064 return log_oom();
1065
1066 printf("%s [OPTIONS...] COMMAND ...\n\n"
1067 "%sShow user and group information.%s\n"
1068 "\nCommands:\n"
1069 " user [USER…] Inspect user\n"
1070 " group [GROUP…] Inspect group\n"
1071 " users-in-group [GROUP…] Show users that are members of specified groups\n"
1072 " groups-of-user [USER…] Show groups the specified users are members of\n"
1073 " services Show enabled database services\n"
1074 " ssh-authorized-keys USER Show SSH authorized keys for user\n"
1075 "\nOptions:\n"
1076 " -h --help Show this help\n"
1077 " --version Show package version\n"
1078 " --no-pager Do not pipe output into a pager\n"
1079 " --no-legend Do not show the headers and footers\n"
1080 " --output=MODE Select output mode (classic, friendly, table, json)\n"
1081 " -j Equivalent to --output=json\n"
1082 " -s --service=SERVICE[:SERVICE…]\n"
1083 " Query the specified service\n"
1084 " --with-nss=BOOL Control whether to include glibc NSS data\n"
1085 " -N Do not synthesize or include glibc NSS data\n"
1086 " (Same as --synthesize=no --with-nss=no)\n"
1087 " --synthesize=BOOL Synthesize root/nobody user\n"
1088 " --with-dropin=BOOL Control whether to include drop-in records\n"
1089 " --with-varlink=BOOL Control whether to talk to services at all\n"
1090 " --multiplexer=BOOL Control whether to use the multiplexer\n"
1091 " --json=pretty|short JSON output mode\n"
1092 " --chain Chain another command\n"
1093 "\nSee the %s for details.\n",
1094 program_invocation_short_name,
1095 ansi_highlight(),
1096 ansi_normal(),
1097 link);
1098
1099 return 0;
1100 }
1101
1102 static int parse_argv(int argc, char *argv[]) {
1103
1104 enum {
1105 ARG_VERSION = 0x100,
1106 ARG_NO_PAGER,
1107 ARG_NO_LEGEND,
1108 ARG_OUTPUT,
1109 ARG_WITH_NSS,
1110 ARG_WITH_DROPIN,
1111 ARG_WITH_VARLINK,
1112 ARG_SYNTHESIZE,
1113 ARG_MULTIPLEXER,
1114 ARG_JSON,
1115 ARG_CHAIN,
1116 };
1117
1118 static const struct option options[] = {
1119 { "help", no_argument, NULL, 'h' },
1120 { "version", no_argument, NULL, ARG_VERSION },
1121 { "no-pager", no_argument, NULL, ARG_NO_PAGER },
1122 { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
1123 { "output", required_argument, NULL, ARG_OUTPUT },
1124 { "service", required_argument, NULL, 's' },
1125 { "with-nss", required_argument, NULL, ARG_WITH_NSS },
1126 { "with-dropin", required_argument, NULL, ARG_WITH_DROPIN },
1127 { "with-varlink", required_argument, NULL, ARG_WITH_VARLINK },
1128 { "synthesize", required_argument, NULL, ARG_SYNTHESIZE },
1129 { "multiplexer", required_argument, NULL, ARG_MULTIPLEXER },
1130 { "json", required_argument, NULL, ARG_JSON },
1131 { "chain", no_argument, NULL, ARG_CHAIN },
1132 {}
1133 };
1134
1135 const char *e;
1136 int r;
1137
1138 assert(argc >= 0);
1139 assert(argv);
1140
1141 /* We are going to update this environment variable with our own, hence let's first read what is already set */
1142 e = getenv("SYSTEMD_ONLY_USERDB");
1143 if (e) {
1144 char **l;
1145
1146 l = strv_split(e, ":");
1147 if (!l)
1148 return log_oom();
1149
1150 strv_free(arg_services);
1151 arg_services = l;
1152 }
1153
1154 /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
1155 * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
1156 optind = 0;
1157
1158 for (;;) {
1159 int c;
1160
1161 c = getopt_long(argc, argv,
1162 arg_chain ? "+hjs:N" : "hjs:N", /* When --chain was used disable parsing of further switches */
1163 options, NULL);
1164 if (c < 0)
1165 break;
1166
1167 switch (c) {
1168
1169 case 'h':
1170 return help(0, NULL, NULL);
1171
1172 case ARG_VERSION:
1173 return version();
1174
1175 case ARG_NO_PAGER:
1176 arg_pager_flags |= PAGER_DISABLE;
1177 break;
1178
1179 case ARG_NO_LEGEND:
1180 arg_legend = false;
1181 break;
1182
1183 case ARG_OUTPUT:
1184 if (isempty(optarg))
1185 arg_output = _OUTPUT_INVALID;
1186 else if (streq(optarg, "classic"))
1187 arg_output = OUTPUT_CLASSIC;
1188 else if (streq(optarg, "friendly"))
1189 arg_output = OUTPUT_FRIENDLY;
1190 else if (streq(optarg, "json"))
1191 arg_output = OUTPUT_JSON;
1192 else if (streq(optarg, "table"))
1193 arg_output = OUTPUT_TABLE;
1194 else if (streq(optarg, "help")) {
1195 puts("classic\n"
1196 "friendly\n"
1197 "json\n"
1198 "table");
1199 return 0;
1200 } else
1201 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid --output= mode: %s", optarg);
1202
1203 arg_json_format_flags = arg_output == OUTPUT_JSON ? JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO : JSON_FORMAT_OFF;
1204 break;
1205
1206 case ARG_JSON:
1207 r = parse_json_argument(optarg, &arg_json_format_flags);
1208 if (r <= 0)
1209 return r;
1210
1211 arg_output = FLAGS_SET(arg_json_format_flags, JSON_FORMAT_OFF) ? _OUTPUT_INVALID : OUTPUT_JSON;
1212 break;
1213
1214 case 'j':
1215 arg_json_format_flags = JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO;
1216 arg_output = OUTPUT_JSON;
1217 break;
1218
1219 case 's':
1220 if (isempty(optarg))
1221 arg_services = strv_free(arg_services);
1222 else {
1223 _cleanup_strv_free_ char **l = NULL;
1224
1225 l = strv_split(optarg, ":");
1226 if (!l)
1227 return log_oom();
1228
1229 r = strv_extend_strv(&arg_services, l, true);
1230 if (r < 0)
1231 return log_oom();
1232 }
1233
1234 break;
1235
1236 case 'N':
1237 arg_userdb_flags |= USERDB_EXCLUDE_NSS|USERDB_DONT_SYNTHESIZE;
1238 break;
1239
1240 case ARG_WITH_NSS:
1241 r = parse_boolean_argument("--with-nss=", optarg, NULL);
1242 if (r < 0)
1243 return r;
1244
1245 SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_NSS, !r);
1246 break;
1247
1248 case ARG_WITH_DROPIN:
1249 r = parse_boolean_argument("--with-dropin=", optarg, NULL);
1250 if (r < 0)
1251 return r;
1252
1253 SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_DROPIN, !r);
1254 break;
1255
1256 case ARG_WITH_VARLINK:
1257 r = parse_boolean_argument("--with-varlink=", optarg, NULL);
1258 if (r < 0)
1259 return r;
1260
1261 SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_VARLINK, !r);
1262 break;
1263
1264 case ARG_SYNTHESIZE:
1265 r = parse_boolean_argument("--synthesize=", optarg, NULL);
1266 if (r < 0)
1267 return r;
1268
1269 SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE, !r);
1270 break;
1271
1272 case ARG_MULTIPLEXER:
1273 r = parse_boolean_argument("--multiplexer=", optarg, NULL);
1274 if (r < 0)
1275 return r;
1276
1277 SET_FLAG(arg_userdb_flags, USERDB_AVOID_MULTIPLEXER, !r);
1278 break;
1279
1280 case ARG_CHAIN:
1281 arg_chain = true;
1282 break;
1283
1284 case '?':
1285 return -EINVAL;
1286
1287 default:
1288 assert_not_reached();
1289 }
1290 }
1291
1292 return 1;
1293 }
1294
1295 static int run(int argc, char *argv[]) {
1296 static const Verb verbs[] = {
1297 { "help", VERB_ANY, VERB_ANY, 0, help },
1298 { "user", VERB_ANY, VERB_ANY, VERB_DEFAULT, display_user },
1299 { "group", VERB_ANY, VERB_ANY, 0, display_group },
1300 { "users-in-group", VERB_ANY, VERB_ANY, 0, display_memberships },
1301 { "groups-of-user", VERB_ANY, VERB_ANY, 0, display_memberships },
1302 { "services", VERB_ANY, 1, 0, display_services },
1303
1304 /* This one is a helper for sshd_config's AuthorizedKeysCommand= setting, it's not a
1305 * user-facing verb and thus should not appear in man pages or --help texts. */
1306 { "ssh-authorized-keys", 2, VERB_ANY, 0, ssh_authorized_keys },
1307 {}
1308 };
1309
1310 int r;
1311
1312 log_setup();
1313
1314 r = parse_argv(argc, argv);
1315 if (r <= 0)
1316 return r;
1317
1318 if (arg_services) {
1319 _cleanup_free_ char *e = NULL;
1320
1321 e = strv_join(arg_services, ":");
1322 if (!e)
1323 return log_oom();
1324
1325 if (setenv("SYSTEMD_ONLY_USERDB", e, true) < 0)
1326 return log_error_errno(r, "Failed to set $SYSTEMD_ONLY_USERDB: %m");
1327
1328 log_info("Enabled services: %s", e);
1329 } else
1330 assert_se(unsetenv("SYSTEMD_ONLY_USERDB") == 0);
1331
1332 return dispatch_verb(argc, argv, verbs, NULL);
1333 }
1334
1335 DEFINE_MAIN_FUNCTION(run);