• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

systemd / systemd / 18988181302

31 Oct 2025 09:30PM UTC coverage: 72.241% (+0.2%) from 72.046%
18988181302

push

github

web-flow
core: Add RootDirectoryFileDescriptor= (#39480)

RootDirectory= but via a open_tree() file descriptor. This allows
setting up the execution environment for a service by the client in a
mount namespace and then starting a transient unit in that execution
environment using the new property.

We also add --root-directory= and --same-root-dir= to systemd-run to
have it run services within the given root directory. As systemd-run
might be invoked from a different mount namespace than what systemd is
running in, systemd-run opens the given path with open_tree() and then
sends it to systemd using the new RootDirectoryFileDescriptor= property.

45 of 76 new or added lines in 8 files covered. (59.21%)

2101 existing lines in 44 files now uncovered.

305020 of 422226 relevant lines covered (72.24%)

1081585.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.99
/src/userdb/userdbctl.c
1
/* SPDX-License-Identifier: LGPL-2.1-or-later */
2

3
#include <getopt.h>
4
#include <stdlib.h>
5
#include <unistd.h>
6

7
#include "alloc-util.h"
8
#include "bitfield.h"
9
#include "build.h"
10
#include "copy.h"
11
#include "creds-util.h"
12
#include "dirent-util.h"
13
#include "errno-list.h"
14
#include "errno-util.h"
15
#include "escape.h"
16
#include "fd-util.h"
17
#include "fileio.h"
18
#include "format-table.h"
19
#include "format-util.h"
20
#include "fs-util.h"
21
#include "io-util.h"
22
#include "log.h"
23
#include "main-func.h"
24
#include "mkdir.h"
25
#include "pager.h"
26
#include "parse-argument.h"
27
#include "pretty-print.h"
28
#include "recurse-dir.h"
29
#include "socket-util.h"
30
#include "string-table.h"
31
#include "string-util.h"
32
#include "strv.h"
33
#include "uid-classification.h"
34
#include "uid-range.h"
35
#include "umask-util.h"
36
#include "user-record-show.h"
37
#include "user-util.h"
38
#include "userdb.h"
39
#include "verbs.h"
40
#include "virt.h"
41

42
typedef enum {
43
        OUTPUT_CLASSIC,
44
        OUTPUT_TABLE,
45
        OUTPUT_FRIENDLY,
46
        OUTPUT_JSON,
47
        _OUTPUT_MAX,
48
        _OUTPUT_INVALID = -EINVAL,
49
} Output;
50

51
static Output arg_output = _OUTPUT_INVALID;
52
static PagerFlags arg_pager_flags = 0;
53
static bool arg_legend = true;
54
static char** arg_services = NULL;
55
static UserDBFlags arg_userdb_flags = 0;
56
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
57
static bool arg_chain = false;
58
static uint64_t arg_disposition_mask = UINT64_MAX;
59
static uid_t arg_uid_min = 0;
60
static uid_t arg_uid_max = UID_INVALID-1;
61
static sd_id128_t arg_uuid = SD_ID128_NULL;
62
static bool arg_fuzzy = false;
63
static bool arg_boundaries = true;
64
static sd_json_variant *arg_from_file = NULL;
65

66
STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep);
312✔
67
STATIC_DESTRUCTOR_REGISTER(arg_from_file, sd_json_variant_unrefp);
312✔
68

69
static const char *output_table[_OUTPUT_MAX] = {
70
        [OUTPUT_CLASSIC]  = "classic",
71
        [OUTPUT_TABLE]    = "table",
72
        [OUTPUT_FRIENDLY] = "friendly",
73
        [OUTPUT_JSON]     = "json",
74
};
75

76
DEFINE_PRIVATE_STRING_TABLE_LOOKUP(output, Output);
8✔
77

78
static const char *user_disposition_to_color(UserDisposition d) {
534✔
79
        assert(d >= 0);
534✔
80
        assert(d < _USER_DISPOSITION_MAX);
534✔
81

82
        switch (d) {
534✔
83
        case USER_INTRINSIC:
84
                return ansi_red();
28✔
85

86
        case USER_SYSTEM:
87
        case USER_DYNAMIC:
88
                return ansi_green();
482✔
89

90
        case USER_CONTAINER:
91
        case USER_FOREIGN:
92
                return ansi_cyan();
10✔
93

94
        case USER_RESERVED:
UNCOV
95
                return ansi_red();
×
96

97
        default:
98
                return NULL;
99
        }
100
}
101

102
static const char* shell_to_color(const char *shell) {
241✔
103
        return !shell || is_nologin_shell(shell) ? ansi_grey() : NULL;
241✔
104
}
105

106
static int show_user(UserRecord *ur, Table *table) {
452✔
107
        int r;
452✔
108

109
        assert(ur);
452✔
110

111
        switch (arg_output) {
452✔
112

113
        case OUTPUT_CLASSIC:
27✔
114
                if (!uid_is_valid(ur->uid))
27✔
115
                        break;
116

117
                printf("%s:x:" UID_FMT ":" GID_FMT ":%s:%s:%s\n",
54✔
118
                       ur->user_name,
119
                       ur->uid,
120
                       user_record_gid(ur),
121
                       strempty(user_record_real_name(ur)),
27✔
122
                       user_record_home_directory(ur),
123
                       user_record_shell(ur));
124

125
                break;
27✔
126

127
        case OUTPUT_JSON:
99✔
128
                sd_json_variant_dump(ur->json, arg_json_format_flags, NULL, NULL);
99✔
129
                break;
99✔
130

131
        case OUTPUT_FRIENDLY:
85✔
132
                user_record_show(ur, true);
85✔
133

134
                if (ur->incomplete) {
85✔
135
                        fflush(stdout);
×
UNCOV
136
                        log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name);
×
137
                }
138

139
                break;
140

141
        case OUTPUT_TABLE: {
241✔
142
                assert(table);
241✔
143
                UserDisposition d = user_record_disposition(ur);
241✔
144
                const char *sh = user_record_shell(ur);
241✔
145

146
                r = table_add_many(
418✔
147
                                table,
148
                                TABLE_STRING, "",
149
                                TABLE_STRING, ur->user_name,
150
                                TABLE_SET_COLOR, user_disposition_to_color(d),
151
                                TABLE_STRING, user_disposition_to_string(d),
152
                                TABLE_UID, ur->uid,
153
                                TABLE_GID, user_record_gid(ur),
154
                                TABLE_STRING, empty_to_null(ur->real_name),
155
                                TABLE_PATH, user_record_home_directory(ur),
156
                                TABLE_PATH, sh,
157
                                TABLE_SET_COLOR, shell_to_color(sh),
158
                                TABLE_INT, 0);
159
                if (r < 0)
241✔
UNCOV
160
                        return table_log_add_error(r);
×
161

162
                break;
163
        }
164

165
        default:
×
UNCOV
166
                assert_not_reached();
×
167
        }
168

169
        return 0;
170
}
171

172
static bool test_show_mapped(void) {
16✔
173
        /* Show mapped user range only in environments where user mapping is a thing. */
174
        return running_in_userns() > 0;
16✔
175
}
176

177
static const struct {
178
        uid_t first, last;
179
        const char *name;
180
        UserDisposition disposition;
181
        bool (*test)(void);
182
} uid_range_table[] = {
183
        {
184
                .first = 1,
185
                .last = SYSTEM_UID_MAX,
186
                .name = "system",
187
                .disposition = USER_SYSTEM,
188
        },
189
        {
190
                .first = GREETER_UID_MIN,
191
                .last = GREETER_UID_MAX,
192
                .name = "dynamic greeter",
193
                .disposition = USER_DYNAMIC,
194
        },
195
        {
196
                .first = DYNAMIC_UID_MIN,
197
                .last = DYNAMIC_UID_MAX,
198
                .name = "dynamic system",
199
                .disposition = USER_DYNAMIC,
200
        },
201
        {
202
                .first = CONTAINER_UID_MIN,
203
                .last = CONTAINER_UID_MAX,
204
                .name = "container",
205
                .disposition = USER_CONTAINER,
206
        },
207
        {
208
                .first = FOREIGN_UID_MIN,
209
                .last = FOREIGN_UID_MAX,
210
                .name = "foreign",
211
                .disposition = USER_FOREIGN,
212
        },
213
#if ENABLE_HOMED
214
        {
215
                .first = HOME_UID_MIN,
216
                .last = HOME_UID_MAX,
217
                .name = "systemd-homed",
218
                .disposition = USER_REGULAR,
219
        },
220
#endif
221
        {
222
                .first = MAP_UID_MIN,
223
                .last = MAP_UID_MAX,
224
                .name = "mapped",
225
                .disposition = USER_REGULAR,
226
                .test = test_show_mapped,
227
        },
228
};
229

230
static int table_add_uid_boundaries(Table *table, const UIDRange *p) {
13✔
231
        int r, n_added = 0;
13✔
232

233
        assert(table);
13✔
234

235
        FOREACH_ELEMENT(i, uid_range_table) {
104✔
236
                _cleanup_free_ char *name = NULL, *comment = NULL;
53✔
237

238
                if (!BIT_SET(arg_disposition_mask, i->disposition))
91✔
239
                        continue;
28✔
240

241
                if (!uid_range_covers(p, i->first, i->last - i->first + 1))
63✔
UNCOV
242
                        continue;
×
243

244
                if (i->test && !i->test())
63✔
245
                        continue;
10✔
246

247
                name = strjoin(glyph(GLYPH_ARROW_DOWN),
53✔
248
                               " begin ", i->name, " users ",
249
                               glyph(GLYPH_ARROW_DOWN));
250
                if (!name)
53✔
UNCOV
251
                        return log_oom();
×
252

253
                comment = strjoin("First ", i->name, " user");
53✔
254
                if (!comment)
53✔
UNCOV
255
                        return log_oom();
×
256

257
                r = table_add_many(
53✔
258
                                table,
259
                                TABLE_STRING, glyph(GLYPH_TREE_TOP),
260
                                TABLE_STRING, name,
261
                                TABLE_SET_COLOR, ansi_grey(),
262
                                TABLE_STRING, user_disposition_to_string(i->disposition),
263
                                TABLE_SET_COLOR, ansi_grey(),
264
                                TABLE_UID, i->first,
265
                                TABLE_SET_COLOR, ansi_grey(),
266
                                TABLE_EMPTY,
267
                                TABLE_STRING, comment,
268
                                TABLE_SET_COLOR, ansi_grey(),
269
                                TABLE_EMPTY,
270
                                TABLE_EMPTY,
271
                                TABLE_INT, -1); /* sort before any other entry with the same UID */
272
                if (r < 0)
53✔
UNCOV
273
                        return table_log_add_error(r);
×
274

275
                free(name);
53✔
276
                name = strjoin(glyph(GLYPH_ARROW_UP),
53✔
277
                               " end ", i->name, " users ",
278
                               glyph(GLYPH_ARROW_UP));
279
                if (!name)
53✔
UNCOV
280
                        return log_oom();
×
281

282
                free(comment);
53✔
283
                comment = strjoin("Last ", i->name, " user");
53✔
284
                if (!comment)
53✔
UNCOV
285
                        return log_oom();
×
286

287
                r = table_add_many(
53✔
288
                                table,
289
                                TABLE_STRING, glyph(GLYPH_TREE_RIGHT),
290
                                TABLE_STRING, name,
291
                                TABLE_SET_COLOR, ansi_grey(),
292
                                TABLE_STRING, user_disposition_to_string(i->disposition),
293
                                TABLE_SET_COLOR, ansi_grey(),
294
                                TABLE_UID, i->last,
295
                                TABLE_SET_COLOR, ansi_grey(),
296
                                TABLE_EMPTY,
297
                                TABLE_STRING, comment,
298
                                TABLE_SET_COLOR, ansi_grey(),
299
                                TABLE_EMPTY,
300
                                TABLE_EMPTY,
301
                                TABLE_INT, 1); /* sort after any other entry with the same UID */
302
                if (r < 0)
53✔
UNCOV
303
                        return table_log_add_error(r);
×
304

305
                n_added += 2;
53✔
306
        }
307

308
        return n_added;
309
}
310

311
static int add_unavailable_uid(Table *table, uid_t start, uid_t end) {
×
312
        _cleanup_free_ char *name = NULL;
×
UNCOV
313
        int r;
×
314

315
        assert(table);
×
UNCOV
316
        assert(start <= end);
×
317

UNCOV
318
        name = strjoin(glyph(GLYPH_ARROW_DOWN),
×
319
                       " begin unavailable users ",
320
                       glyph(GLYPH_ARROW_DOWN));
321
        if (!name)
×
UNCOV
322
                return log_oom();
×
323

UNCOV
324
        r = table_add_many(
×
325
                        table,
326
                        TABLE_STRING, glyph(GLYPH_TREE_TOP),
327
                        TABLE_STRING, name,
328
                        TABLE_SET_COLOR, ansi_grey(),
329
                        TABLE_EMPTY,
330
                        TABLE_UID, start,
331
                        TABLE_SET_COLOR, ansi_grey(),
332
                        TABLE_EMPTY,
333
                        TABLE_STRING, "First unavailable user",
334
                        TABLE_SET_COLOR, ansi_grey(),
335
                        TABLE_EMPTY,
336
                        TABLE_EMPTY,
337
                        TABLE_INT, -1); /* sort before an other entry with the same UID */
338
        if (r < 0)
×
UNCOV
339
                return table_log_add_error(r);
×
340

341
        free(name);
×
UNCOV
342
        name = strjoin(glyph(GLYPH_ARROW_UP),
×
343
                       " end unavailable users ",
344
                       glyph(GLYPH_ARROW_UP));
345
        if (!name)
×
UNCOV
346
                return log_oom();
×
347

UNCOV
348
        r = table_add_many(
×
349
                        table,
350
                        TABLE_STRING, glyph(GLYPH_TREE_RIGHT),
351
                        TABLE_STRING, name,
352
                        TABLE_SET_COLOR, ansi_grey(),
353
                        TABLE_EMPTY,
354
                        TABLE_UID, end,
355
                        TABLE_SET_COLOR, ansi_grey(),
356
                        TABLE_EMPTY,
357
                        TABLE_STRING, "Last unavailable user",
358
                        TABLE_SET_COLOR, ansi_grey(),
359
                        TABLE_EMPTY,
360
                        TABLE_EMPTY,
361
                        TABLE_INT, 1); /* sort after any other entry with the same UID */
362
        if (r < 0)
×
UNCOV
363
                return table_log_add_error(r);
×
364

365
        return 2;
366
}
367

368
static int table_add_uid_map(
22✔
369
                Table *table,
370
                const UIDRange *p,
371
                int (*add_unavailable)(Table *t, uid_t start, uid_t end)) {
372

373
        uid_t focus = 0;
22✔
374
        int n_added = 0, r;
22✔
375

376
        assert(table);
22✔
377
        assert(add_unavailable);
22✔
378

379
        if (!p)
22✔
380
                return 0;
381

382
        FOREACH_ARRAY(x, p->entries, p->n_entries) {
44✔
383
                if (focus < x->start) {
22✔
384
                        r = add_unavailable(table, focus, x->start-1);
×
UNCOV
385
                        if (r < 0)
×
386
                                return r;
387

UNCOV
388
                        n_added += r;
×
389
                }
390

391
                if (x->start > UINT32_MAX - x->nr) { /* overflow check */
22✔
392
                        focus = UINT32_MAX;
393
                        break;
394
                }
395

396
                focus = x->start + x->nr;
22✔
397
        }
398

399
        if (focus < UINT32_MAX-1) {
22✔
400
                r = add_unavailable(table, focus, UINT32_MAX-1);
×
UNCOV
401
                if (r < 0)
×
402
                        return r;
403

UNCOV
404
                n_added += r;
×
405
        }
406

407
        return n_added;
408
}
409

410
static int display_user(int argc, char *argv[], void *userdata) {
104✔
411
        _cleanup_(table_unrefp) Table *table = NULL;
104✔
412
        bool draw_separator = false;
104✔
413
        int ret = 0, r;
104✔
414

415
        if (arg_output < 0)
104✔
416
                arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) || !sd_id128_is_null(arg_uuid) ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
94✔
417

418
        if (arg_output == OUTPUT_TABLE) {
104✔
419
                table = table_new(" ", "name", "disposition", "uid", "gid", "realname", "home", "shell", "order");
14✔
420
                if (!table)
14✔
UNCOV
421
                        return log_oom();
×
422

423
                (void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100);
14✔
424
                (void) table_set_align_percent(table, table_get_cell(table, 0, 4), 100);
14✔
425
                table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
14✔
426
                (void) table_set_sort(table, (size_t) 3, (size_t) 8);
14✔
427
                (void) table_hide_column_from_display(table, (size_t) 8);
14✔
428
                if (!arg_boundaries)
14✔
429
                        (void) table_hide_column_from_display(table, (size_t) 0);
1✔
430
        }
431

432
        _cleanup_(userdb_match_done) UserDBMatch match = {
104✔
433
                .disposition_mask = arg_disposition_mask,
434
                .uid_min = arg_uid_min,
435
                .uid_max = arg_uid_max,
436
                .uuid = arg_uuid,
437
        };
438

439
        if (arg_from_file) {
104✔
440
                if (argc > 1)
3✔
UNCOV
441
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected when invoked with --from-file=, refusing.");
×
442

443
                _cleanup_(user_record_unrefp) UserRecord *ur = user_record_new();
6✔
444
                if (!ur)
3✔
UNCOV
445
                        return log_oom();
×
446

447
                r = user_record_load(ur, arg_from_file, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG);
3✔
448
                if (r < 0)
3✔
449
                        return r;
450

451
                r = show_user(ur, table);
3✔
452
                if (r < 0)
3✔
453
                        return r;
454

455
        } else if (argc > 1 && !arg_fuzzy)
101✔
456
                STRV_FOREACH(i, argv + 1) {
175✔
457
                        _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
93✔
458

459
                        r = userdb_by_name(*i, &match, arg_userdb_flags|USERDB_PARSE_NUMERIC, &ur);
93✔
460
                        if (r < 0) {
93✔
461
                                if (r == -ESRCH)
20✔
462
                                        log_error_errno(r, "User %s does not exist.", *i);
16✔
463
                                else if (r == -EHOSTDOWN)
4✔
UNCOV
464
                                        log_error_errno(r, "Selected user database service is not available for this request.");
×
465
                                else if (r == -ENOEXEC)
4✔
UNCOV
466
                                        log_error_errno(r, "User '%s' exists but does not match specified filter.", *i);
×
467
                                else
468
                                        log_error_errno(r, "Failed to find user %s: %m", *i);
4✔
469

470
                                RET_GATHER(ret, r);
20✔
471
                        } else {
472
                                if (draw_separator && arg_output == OUTPUT_FRIENDLY)
73✔
473
                                        putchar('\n');
4✔
474

475
                                r = show_user(ur, table);
73✔
476
                                if (r < 0)
73✔
UNCOV
477
                                        return r;
×
478

479
                                draw_separator = true;
480
                        }
481
                }
482
        else {
483
                if (argc > 1) {
1✔
484
                        /* If there are further arguments, they are the fuzzy match strings. */
485
                        match.fuzzy_names = strv_copy(strv_skip(argv, 1));
1✔
486
                        if (!match.fuzzy_names)
1✔
UNCOV
487
                                return log_oom();
×
488
                }
489

UNCOV
490
                _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
×
491
                r = userdb_all(&match, arg_userdb_flags, &iterator);
19✔
492
                if (r == -ENOLINK) /* ENOLINK → Didn't find answer without Varlink, and didn't try Varlink because was configured to off. */
19✔
UNCOV
493
                        log_debug_errno(r, "No entries found. (Didn't check via Varlink.)");
×
494
                else if (r == -ESRCH) /* ESRCH → Couldn't find any suitable entry, but we checked all sources */
19✔
495
                        log_debug_errno(r, "No entries found.");
19✔
496
                else if (r < 0)
19✔
UNCOV
497
                        return log_error_errno(r, "Failed to enumerate users: %m");
×
498
                else {
499
                        for (;;) {
376✔
500
                                _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
19✔
501

502
                                r = userdb_iterator_get(iterator, &match, &ur);
395✔
503
                                if (r == -ESRCH)
395✔
504
                                        break;
505
                                if (r == -EHOSTDOWN)
376✔
UNCOV
506
                                        return log_error_errno(r, "Selected user database service is not available for this request.");
×
507
                                if (r < 0)
376✔
UNCOV
508
                                        return log_error_errno(r, "Failed to acquire next user: %m");
×
509

510
                                if (draw_separator && arg_output == OUTPUT_FRIENDLY)
376✔
511
                                        putchar('\n');
26✔
512

513
                                r = show_user(ur, table);
376✔
514
                                if (r < 0)
376✔
515
                                        return r;
516

517
                                draw_separator = true;
376✔
518
                        }
519
                }
520
        }
521

522
        if (table) {
104✔
523
                int boundary_lines = 0, uid_map_lines = 0;
14✔
524

525
                if (arg_boundaries) {
14✔
526
                        _cleanup_(uid_range_freep) UIDRange *uid_range = NULL;
13✔
527

528
                        r = uid_range_load_userns(/* path = */ NULL, UID_RANGE_USERNS_INSIDE, &uid_range);
13✔
529
                        if (r < 0)
13✔
UNCOV
530
                                log_debug_errno(r, "Failed to load /proc/self/uid_map, ignoring: %m");
×
531

532
                        boundary_lines = table_add_uid_boundaries(table, uid_range);
13✔
533
                        if (boundary_lines < 0)
13✔
534
                                return boundary_lines;
535

536
                        uid_map_lines = table_add_uid_map(table, uid_range, add_unavailable_uid);
13✔
537
                        if (uid_map_lines < 0)
13✔
538
                                return uid_map_lines;
539
                }
540

541
                if (!table_isempty(table)) {
28✔
542
                        r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
14✔
543
                        if (r < 0)
14✔
544
                                return r;
545
                }
546

547
                if (arg_legend) {
14✔
548
                        size_t k;
12✔
549

550
                        k = table_get_rows(table) - 1 - boundary_lines - uid_map_lines;
12✔
551
                        if (k > 0)
12✔
552
                                printf("\n%zu users listed.\n", k);
10✔
553
                        else
554
                                printf("No users.\n");
2✔
555
                }
556
        }
557

558
        return ret;
559
}
560

561
static int show_group(GroupRecord *gr, Table *table) {
317✔
562
        int r;
317✔
563

564
        assert(gr);
317✔
565

566
        switch (arg_output) {
317✔
567

568
        case OUTPUT_CLASSIC: {
×
UNCOV
569
                _cleanup_free_ char *m = NULL;
×
570

UNCOV
571
                if (!gid_is_valid(gr->gid))
×
572
                        break;
573

574
                m = strv_join(gr->members, ",");
×
575
                if (!m)
×
UNCOV
576
                        return log_oom();
×
577

UNCOV
578
                printf("%s:x:" GID_FMT ":%s\n",
×
579
                       gr->group_name,
580
                       gr->gid,
581
                       m);
582
                break;
583
        }
584

585
        case OUTPUT_JSON:
5✔
586
                sd_json_variant_dump(gr->json, arg_json_format_flags, NULL, NULL);
5✔
587
                break;
5✔
588

589
        case OUTPUT_FRIENDLY:
19✔
590
                group_record_show(gr, true);
19✔
591

592
                if (gr->incomplete) {
19✔
593
                        fflush(stdout);
×
UNCOV
594
                        log_warning("Warning: lacking rights to acquire privileged fields of group record of '%s', output incomplete.", gr->group_name);
×
595
                }
596

597
                break;
598

599
        case OUTPUT_TABLE: {
293✔
600
                UserDisposition d;
293✔
601

602
                assert(table);
293✔
603
                d = group_record_disposition(gr);
293✔
604

605
                r = table_add_many(
293✔
606
                                table,
607
                                TABLE_STRING, "",
608
                                TABLE_STRING, gr->group_name,
609
                                TABLE_SET_COLOR, user_disposition_to_color(d),
610
                                TABLE_STRING, user_disposition_to_string(d),
611
                                TABLE_GID, gr->gid,
612
                                TABLE_STRING, gr->description,
613
                                TABLE_INT, 0);
614
                if (r < 0)
293✔
UNCOV
615
                        return table_log_add_error(r);
×
616

617
                break;
618
        }
619

620
        default:
×
UNCOV
621
                assert_not_reached();
×
622
        }
623

624
        return 0;
625
}
626

627
static int table_add_gid_boundaries(Table *table, const UIDRange *p) {
9✔
628
        int r, n_added = 0;
9✔
629

630
        assert(table);
9✔
631

632
        FOREACH_ELEMENT(i, uid_range_table) {
72✔
633
                _cleanup_free_ char *name = NULL, *comment = NULL;
29✔
634

635
                if (!BIT_SET(arg_disposition_mask, i->disposition))
63✔
636
                        continue;
28✔
637

638
                if (!uid_range_covers(p, i->first, i->last - i->first + 1))
35✔
UNCOV
639
                        continue;
×
640

641
                if (i->test && !i->test())
35✔
642
                        continue;
6✔
643

644
                name = strjoin(glyph(GLYPH_ARROW_DOWN),
29✔
645
                               " begin ", i->name, " groups ",
646
                               glyph(GLYPH_ARROW_DOWN));
647
                if (!name)
29✔
UNCOV
648
                        return log_oom();
×
649

650
                comment = strjoin("First ", i->name, " group");
29✔
651
                if (!comment)
29✔
UNCOV
652
                        return log_oom();
×
653

654
                r = table_add_many(
29✔
655
                                table,
656
                                TABLE_STRING, glyph(GLYPH_TREE_TOP),
657
                                TABLE_STRING, name,
658
                                TABLE_SET_COLOR, ansi_grey(),
659
                                TABLE_STRING, user_disposition_to_string(i->disposition),
660
                                TABLE_SET_COLOR, ansi_grey(),
661
                                TABLE_GID, i->first,
662
                                TABLE_SET_COLOR, ansi_grey(),
663
                                TABLE_STRING, comment,
664
                                TABLE_SET_COLOR, ansi_grey(),
665
                                TABLE_INT, -1); /* sort before any other entry with the same GID */
666
                if (r < 0)
29✔
UNCOV
667
                        return table_log_add_error(r);
×
668

669
                free(name);
29✔
670
                name = strjoin(glyph(GLYPH_ARROW_UP),
29✔
671
                               " end ", i->name, " groups ",
672
                               glyph(GLYPH_ARROW_UP));
673
                if (!name)
29✔
UNCOV
674
                        return log_oom();
×
675

676
                free(comment);
29✔
677
                comment = strjoin("Last ", i->name, " group");
29✔
678
                if (!comment)
29✔
UNCOV
679
                        return log_oom();
×
680

681
                r = table_add_many(
29✔
682
                                table,
683
                                TABLE_STRING, glyph(GLYPH_TREE_RIGHT),
684
                                TABLE_STRING, name,
685
                                TABLE_SET_COLOR, ansi_grey(),
686
                                TABLE_STRING, user_disposition_to_string(i->disposition),
687
                                TABLE_SET_COLOR, ansi_grey(),
688
                                TABLE_GID, i->last,
689
                                TABLE_SET_COLOR, ansi_grey(),
690
                                TABLE_STRING, comment,
691
                                TABLE_SET_COLOR, ansi_grey(),
692
                                TABLE_INT, 1); /* sort after any other entry with the same GID */
693
                if (r < 0)
29✔
UNCOV
694
                        return table_log_add_error(r);
×
695

696
                n_added += 2;
29✔
697
        }
698

699
        return n_added;
700
}
701

702
static int add_unavailable_gid(Table *table, uid_t start, uid_t end) {
×
703
        _cleanup_free_ char *name = NULL;
×
UNCOV
704
        int r;
×
705

706
        assert(table);
×
UNCOV
707
        assert(start <= end);
×
708

UNCOV
709
        name = strjoin(glyph(GLYPH_ARROW_DOWN),
×
710
                       " begin unavailable groups ",
711
                       glyph(GLYPH_ARROW_DOWN));
712
        if (!name)
×
UNCOV
713
                return log_oom();
×
714

UNCOV
715
        r = table_add_many(
×
716
                        table,
717
                        TABLE_STRING, glyph(GLYPH_TREE_TOP),
718
                        TABLE_STRING, name,
719
                        TABLE_SET_COLOR, ansi_grey(),
720
                        TABLE_EMPTY,
721
                        TABLE_GID, start,
722
                        TABLE_SET_COLOR, ansi_grey(),
723
                        TABLE_STRING, "First unavailable group",
724
                        TABLE_SET_COLOR, ansi_grey(),
725
                        TABLE_INT, -1); /* sort before any other entry with the same GID */
726
        if (r < 0)
×
UNCOV
727
                return table_log_add_error(r);
×
728

729
        free(name);
×
UNCOV
730
        name = strjoin(glyph(GLYPH_ARROW_UP),
×
731
                       " end unavailable groups ",
732
                       glyph(GLYPH_ARROW_UP));
733
        if (!name)
×
UNCOV
734
                return log_oom();
×
735

UNCOV
736
        r = table_add_many(
×
737
                        table,
738
                        TABLE_STRING, glyph(GLYPH_TREE_RIGHT),
739
                        TABLE_STRING, name,
740
                        TABLE_SET_COLOR, ansi_grey(),
741
                        TABLE_EMPTY,
742
                        TABLE_GID, end,
743
                        TABLE_SET_COLOR, ansi_grey(),
744
                        TABLE_STRING, "Last unavailable group",
745
                        TABLE_SET_COLOR, ansi_grey(),
746
                        TABLE_INT, 1); /* sort after any other entry with the same GID */
747
        if (r < 0)
×
UNCOV
748
                return table_log_add_error(r);
×
749

750
        return 2;
751
}
752

753
static int display_group(int argc, char *argv[], void *userdata) {
36✔
754
        _cleanup_(table_unrefp) Table *table = NULL;
36✔
755
        bool draw_separator = false;
36✔
756
        int ret = 0, r;
36✔
757

758
        if (arg_output < 0)
36✔
759
                arg_output = arg_from_file || (argc > 1 && !arg_fuzzy) || !sd_id128_is_null(arg_uuid) ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
42✔
760

761
        if (arg_output == OUTPUT_TABLE) {
36✔
762
                table = table_new(" ", "name", "disposition", "gid", "description", "order");
10✔
763
                if (!table)
10✔
UNCOV
764
                        return log_oom();
×
765

766
                (void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100);
10✔
767
                table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
10✔
768
                (void) table_set_sort(table, (size_t) 3, (size_t) 5);
10✔
769
                (void) table_hide_column_from_display(table, (size_t) 5);
10✔
770
                if (!arg_boundaries)
10✔
771
                        (void) table_hide_column_from_display(table, (size_t) 0);
1✔
772
        }
773

774
        _cleanup_(userdb_match_done) UserDBMatch match = {
36✔
775
                .disposition_mask = arg_disposition_mask,
776
                .gid_min = arg_uid_min,
777
                .gid_max = arg_uid_max,
778
                .uuid = arg_uuid,
779
        };
780

781
        if (arg_from_file) {
36✔
782
                if (argc > 1)
3✔
UNCOV
783
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No argument expected when invoked with --from-file=, refusing.");
×
784

785
                _cleanup_(group_record_unrefp) GroupRecord *gr = group_record_new();
6✔
786
                if (!gr)
3✔
UNCOV
787
                        return log_oom();
×
788

789
                r = group_record_load(gr, arg_from_file, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG);
3✔
790
                if (r < 0)
3✔
791
                        return r;
792

793
                r = show_group(gr, table);
3✔
794
                if (r < 0)
3✔
795
                        return r;
796

797
        } else if (argc > 1 && !arg_fuzzy)
33✔
798
                STRV_FOREACH(i, argv + 1) {
56✔
799
                        _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
33✔
800

801
                        r = groupdb_by_name(*i, &match, arg_userdb_flags|USERDB_PARSE_NUMERIC, &gr);
33✔
802
                        if (r < 0) {
33✔
803
                                if (r == -ESRCH)
12✔
804
                                        log_error_errno(r, "Group %s does not exist.", *i);
10✔
805
                                else if (r == -EHOSTDOWN)
2✔
UNCOV
806
                                        log_error_errno(r, "Selected group database service is not available for this request.");
×
807
                                else if (r == -ENOEXEC)
2✔
UNCOV
808
                                        log_error_errno(r, "Group '%s' exists but does not match specified filter.", *i);
×
809
                                else
810
                                        log_error_errno(r, "Failed to find group %s: %m", *i);
2✔
811

812
                                RET_GATHER(ret, r);
12✔
813
                        } else {
814
                                if (draw_separator && arg_output == OUTPUT_FRIENDLY)
21✔
815
                                        putchar('\n');
4✔
816

817
                                r = show_group(gr, table);
21✔
818
                                if (r < 0)
21✔
UNCOV
819
                                        return r;
×
820

821
                                draw_separator = true;
822
                        }
823
                }
824
        else {
825
                if (argc > 1) {
1✔
826
                        match.fuzzy_names = strv_copy(strv_skip(argv, 1));
1✔
827
                        if (!match.fuzzy_names)
1✔
UNCOV
828
                                return log_oom();
×
829
                }
830

UNCOV
831
                _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
×
832
                r = groupdb_all(&match, arg_userdb_flags, &iterator);
10✔
833
                if (r == -ENOLINK)
10✔
UNCOV
834
                        log_debug_errno(r, "No entries found. (Didn't check via Varlink.)");
×
835
                else if (r == -ESRCH)
10✔
836
                        log_debug_errno(r, "No entries found.");
10✔
837
                else if (r < 0)
10✔
UNCOV
838
                        return log_error_errno(r, "Failed to enumerate groups: %m");
×
839
                else {
840
                        for (;;) {
293✔
841
                                _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
10✔
842

843
                                r = groupdb_iterator_get(iterator, &match, &gr);
303✔
844
                                if (r == -ESRCH)
303✔
845
                                        break;
846
                                if (r == -EHOSTDOWN)
293✔
UNCOV
847
                                        return log_error_errno(r, "Selected group database service is not available for this request.");
×
848
                                if (r < 0)
293✔
UNCOV
849
                                        return log_error_errno(r, "Failed to acquire next group: %m");
×
850

851
                                if (draw_separator && arg_output == OUTPUT_FRIENDLY)
293✔
UNCOV
852
                                        putchar('\n');
×
853

854
                                r = show_group(gr, table);
293✔
855
                                if (r < 0)
293✔
856
                                        return r;
857

858
                                draw_separator = true;
293✔
859
                        }
860
                }
861
        }
862

863
        if (table) {
36✔
864
                int boundary_lines = 0, gid_map_lines = 0;
10✔
865

866
                if (arg_boundaries) {
10✔
867
                        _cleanup_(uid_range_freep) UIDRange *gid_range = NULL;
9✔
868
                        r = uid_range_load_userns(/* path = */ NULL, GID_RANGE_USERNS_INSIDE, &gid_range);
9✔
869
                        if (r < 0)
9✔
UNCOV
870
                                log_debug_errno(r, "Failed to load /proc/self/gid_map, ignoring: %m");
×
871

872
                        boundary_lines = table_add_gid_boundaries(table, gid_range);
9✔
873
                        if (boundary_lines < 0)
9✔
874
                                return boundary_lines;
875

876
                        gid_map_lines = table_add_uid_map(table, gid_range, add_unavailable_gid);
9✔
877
                        if (gid_map_lines < 0)
9✔
878
                                return gid_map_lines;
879
                }
880

881
                if (!table_isempty(table)) {
20✔
882
                        r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
10✔
883
                        if (r < 0)
10✔
884
                                return r;
885
                }
886

887
                if (arg_legend) {
10✔
888
                        size_t k;
9✔
889

890
                        k = table_get_rows(table) - 1 - boundary_lines - gid_map_lines;
9✔
891
                        if (k > 0)
9✔
892
                                printf("\n%zu groups listed.\n", k);
7✔
893
                        else
894
                                printf("No groups.\n");
2✔
895
                }
896
        }
897

898
        return ret;
899
}
900

901
static int show_membership(const char *user, const char *group, Table *table) {
14✔
902
        int r;
14✔
903

904
        assert(user);
14✔
905
        assert(group);
14✔
906

907
        switch (arg_output) {
14✔
908

UNCOV
909
        case OUTPUT_CLASSIC:
×
910
                /* Strictly speaking there's no 'classic' output for this concept, but let's output it in
911
                 * similar style to the classic output for user/group info */
912

913
                printf("%s:%s\n", user, group);
×
UNCOV
914
                break;
×
915

916
        case OUTPUT_JSON: {
2✔
917
                _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
2✔
918

919
                r = sd_json_buildo(
2✔
920
                                &v,
921
                                SD_JSON_BUILD_PAIR("user", SD_JSON_BUILD_STRING(user)),
922
                                SD_JSON_BUILD_PAIR("group", SD_JSON_BUILD_STRING(group)));
923
                if (r < 0)
2✔
UNCOV
924
                        return log_error_errno(r, "Failed to build JSON object: %m");
×
925

926
                sd_json_variant_dump(v, arg_json_format_flags, NULL, NULL);
2✔
927
                break;
2✔
928
        }
929

UNCOV
930
        case OUTPUT_FRIENDLY:
×
931
                /* Hmm, this is not particularly friendly, but not sure how we could do this better */
932
                printf("%s: %s\n", group, user);
×
UNCOV
933
                break;
×
934

935
        case OUTPUT_TABLE:
12✔
936
                assert(table);
12✔
937

938
                r = table_add_many(
12✔
939
                                table,
940
                                TABLE_STRING, user,
941
                                TABLE_STRING, group);
942
                if (r < 0)
12✔
UNCOV
943
                        return table_log_add_error(r);
×
944

945
                break;
946

947
        default:
×
UNCOV
948
                assert_not_reached();
×
949
        }
950

951
        return 0;
952
}
953

954
static int display_memberships(int argc, char *argv[], void *userdata) {
14✔
955
        _cleanup_(table_unrefp) Table *table = NULL;
14✔
956
        int ret = 0, r;
14✔
957

958
        if (arg_from_file)
14✔
UNCOV
959
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing memberships, refusing.");
×
960

961
        if (arg_output < 0)
14✔
962
                arg_output = OUTPUT_TABLE;
12✔
963

964
        if (arg_output == OUTPUT_TABLE) {
14✔
965
                table = table_new("user", "group");
12✔
966
                if (!table)
12✔
UNCOV
967
                        return log_oom();
×
968

969
                (void) table_set_sort(table, (size_t) 0, (size_t) 1);
12✔
970
        }
971

972
        if (argc > 1)
14✔
973
                STRV_FOREACH(i, argv + 1) {
26✔
974
                        _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
4✔
975

976
                        if (streq(argv[0], "users-in-group")) {
18✔
977
                                r = membershipdb_by_group(*i, arg_userdb_flags, &iterator);
9✔
978
                                if (r < 0)
9✔
979
                                        return log_error_errno(r, "Failed to enumerate users in group: %m");
2✔
980
                        } else if (streq(argv[0], "groups-of-user")) {
9✔
981
                                r = membershipdb_by_user(*i, arg_userdb_flags, &iterator);
9✔
982
                                if (r < 0)
9✔
983
                                        return log_error_errno(r, "Failed to enumerate groups of user: %m");
2✔
984
                        } else
UNCOV
985
                                assert_not_reached();
×
986

987
                        for (;;) {
22✔
988
                                _cleanup_free_ char *user = NULL, *group = NULL;
18✔
989

990
                                r = membershipdb_iterator_get(iterator, &user, &group);
18✔
991
                                if (r == -ESRCH)
18✔
992
                                        break;
993
                                if (r == -EHOSTDOWN)
4✔
UNCOV
994
                                        return log_error_errno(r, "Selected membership database service is not available for this request.");
×
995
                                if (r < 0)
4✔
UNCOV
996
                                        return log_error_errno(r, "Failed to acquire next membership: %m");
×
997

998
                                r = show_membership(user, group, table);
4✔
999
                                if (r < 0)
4✔
1000
                                        return r;
1001
                        }
1002
                }
1003
        else {
UNCOV
1004
                _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
×
1005

1006
                r = membershipdb_all(arg_userdb_flags, &iterator);
2✔
1007
                if (r == -ENOLINK)
2✔
UNCOV
1008
                        log_debug_errno(r, "No entries found. (Didn't check via Varlink.)");
×
1009
                else if (r == -ESRCH)
2✔
1010
                        log_debug_errno(r, "No entries found.");
2✔
1011
                else if (r < 0)
2✔
UNCOV
1012
                        return log_error_errno(r, "Failed to enumerate memberships: %m");
×
1013
                else {
1014
                        for (;;) {
22✔
1015
                                _cleanup_free_ char *user = NULL, *group = NULL;
12✔
1016

1017
                                r = membershipdb_iterator_get(iterator, &user, &group);
12✔
1018
                                if (r == -ESRCH)
12✔
1019
                                        break;
1020
                                if (r == -EHOSTDOWN)
10✔
UNCOV
1021
                                        return log_error_errno(r, "Selected membership database service is not available for this request.");
×
1022
                                if (r < 0)
10✔
UNCOV
1023
                                        return log_error_errno(r, "Failed to acquire next membership: %m");
×
1024

1025
                                r = show_membership(user, group, table);
10✔
1026
                                if (r < 0)
10✔
1027
                                        return r;
1028
                        }
1029
                }
1030
        }
1031

1032
        if (table) {
10✔
1033
                if (!table_isempty(table)) {
8✔
1034
                        r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
4✔
1035
                        if (r < 0)
4✔
1036
                                return r;
1037
                }
1038

1039
                if (arg_legend) {
8✔
1040
                        if (table_isempty(table))
16✔
1041
                                printf("No memberships.\n");
4✔
1042
                        else
1043
                                printf("\n%zu memberships listed.\n", table_get_rows(table) - 1);
4✔
1044
                }
1045
        }
1046

1047
        return ret;
1048
}
1049

1050
static int display_services(int argc, char *argv[], void *userdata) {
2✔
UNCOV
1051
        _cleanup_(table_unrefp) Table *t = NULL;
×
1052
        _cleanup_closedir_ DIR *d = NULL;
2✔
1053
        int r;
2✔
1054

1055
        if (arg_from_file)
2✔
UNCOV
1056
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing services, refusing.");
×
1057

1058
        d = opendir("/run/systemd/userdb/");
2✔
1059
        if (!d) {
2✔
1060
                if (errno == ENOENT) {
×
1061
                        log_info("No services.");
×
UNCOV
1062
                        return 0;
×
1063
                }
1064

UNCOV
1065
                return log_error_errno(errno, "Failed to open %s: %m", "/run/systemd/userdb/");
×
1066
        }
1067

1068
        t = table_new("service", "listening");
2✔
1069
        if (!t)
2✔
UNCOV
1070
                return log_oom();
×
1071

1072
        (void) table_set_sort(t, (size_t) 0);
2✔
1073

1074
        FOREACH_DIRENT(de, d, return -errno) {
18✔
1075
                _cleanup_free_ char *j = NULL, *no = NULL;
12✔
1076
                _cleanup_close_ int fd = -EBADF;
12✔
1077

1078
                j = path_join("/run/systemd/userdb/", de->d_name);
12✔
1079
                if (!j)
12✔
UNCOV
1080
                        return log_oom();
×
1081

1082
                fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
12✔
1083
                if (fd < 0)
12✔
UNCOV
1084
                        return log_error_errno(errno, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m");
×
1085

1086
                r = connect_unix_path(fd, dirfd(d), de->d_name);
12✔
1087
                if (r < 0) {
12✔
1088
                        no = strjoin("No (", ERRNO_NAME(r), ")");
×
1089
                        if (!no)
×
UNCOV
1090
                                return log_oom();
×
1091
                }
1092

1093
                r = table_add_many(t,
24✔
1094
                                   TABLE_STRING, de->d_name,
1095
                                   TABLE_STRING, no ?: "yes",
1096
                                   TABLE_SET_COLOR, ansi_highlight_green_red(!no));
1097
                if (r < 0)
12✔
UNCOV
1098
                        return table_log_add_error(r);
×
1099
        }
1100

1101
        if (!table_isempty(t)) {
4✔
1102
                r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
2✔
1103
                if (r < 0)
2✔
1104
                        return r;
1105
        }
1106

1107
        if (arg_legend && arg_output != OUTPUT_JSON) {
2✔
1108
                if (table_isempty(t))
2✔
UNCOV
1109
                        printf("No services.\n");
×
1110
                else
1111
                        printf("\n%zu services listed.\n", table_get_rows(t) - 1);
1✔
1112
        }
1113

1114
        return 0;
1115
}
1116

1117
static int ssh_authorized_keys(int argc, char *argv[], void *userdata) {
10✔
UNCOV
1118
        _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
×
1119
        char **chain_invocation;
10✔
1120
        int r;
10✔
1121

1122
        assert(argc >= 2);
10✔
1123

1124
        if (arg_from_file)
10✔
UNCOV
1125
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--from-file= not supported when showing SSH authorized keys, refusing.");
×
1126

1127
        if (arg_chain) {
10✔
1128
                /* If --chain is specified, the rest of the command line is the chain command */
1129

1130
                if (argc < 3)
4✔
1131
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
1✔
1132
                                               "No chain command line specified, refusing.");
1133

1134
                /* Make similar restrictions on the chain command as OpenSSH itself makes on the primary command. */
1135
                if (!path_is_absolute(argv[2]))
3✔
1136
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
1✔
1137
                                               "Chain invocation of ssh-authorized-keys commands requires an absolute binary path argument.");
1138

1139
                if (!path_is_normalized(argv[2]))
2✔
UNCOV
1140
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1141
                                               "Chain invocation of ssh-authorized-keys commands requires an normalized binary path argument.");
1142

1143
                chain_invocation = argv + 2;
2✔
1144
        } else {
1145
                /* If --chain is not specified, then refuse any further arguments */
1146

1147
                if (argc > 2)
6✔
UNCOV
1148
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments.");
×
1149

1150
                chain_invocation = NULL;
1151
        }
1152

1153
        r = userdb_by_name(argv[1], /* match= */ NULL, arg_userdb_flags, &ur);
8✔
1154
        if (r == -ESRCH)
8✔
1155
                log_error_errno(r, "User %s does not exist.", argv[1]);
2✔
1156
        else if (r == -EHOSTDOWN)
6✔
UNCOV
1157
                log_error_errno(r, "Selected user database service is not available for this request.");
×
1158
        else if (r == -EINVAL)
6✔
UNCOV
1159
                log_error_errno(r, "Failed to find user %s: %m (Invalid user name?)", argv[1]);
×
1160
        else if (r < 0)
6✔
UNCOV
1161
                log_error_errno(r, "Failed to find user %s: %m", argv[1]);
×
1162
        else {
1163
                if (strv_isempty(ur->ssh_authorized_keys))
6✔
UNCOV
1164
                        log_debug("User record for %s has no public SSH keys.", argv[1]);
×
1165
                else
1166
                        STRV_FOREACH(i, ur->ssh_authorized_keys)
14✔
1167
                                printf("%s\n", *i);
8✔
1168

1169
                if (ur->incomplete) {
6✔
1170
                        fflush(stdout);
×
UNCOV
1171
                        log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name);
×
1172
                }
1173
        }
1174

1175
        if (chain_invocation) {
8✔
1176
                if (DEBUG_LOGGING) {
2✔
1177
                        _cleanup_free_ char *s = NULL;
1✔
1178

1179
                        s = quote_command_line(chain_invocation, SHELL_ESCAPE_EMPTY);
1✔
1180
                        if (!s)
1✔
UNCOV
1181
                                return log_oom();
×
1182

1183
                        log_debug("Chain invoking: %s", s);
1✔
1184
                }
1185

1186
                fflush(stdout);
2✔
1187
                execv(chain_invocation[0], chain_invocation);
×
UNCOV
1188
                if (errno == ENOENT) /* Let's handle ENOENT gracefully */
×
1189
                        log_warning_errno(errno, "Chain executable '%s' does not exist, ignoring chain invocation.", chain_invocation[0]);
8✔
1190
                else {
1191
                        log_error_errno(errno, "Failed to invoke chain executable '%s': %m", chain_invocation[0]);
×
1192
                        if (r >= 0)
×
UNCOV
1193
                                r = -errno;
×
1194
                }
1195
        }
1196

1197
        return r;
1198
}
1199

UNCOV
1200
static int write_membership(int dir_fd, const char *dir, const char *user, const char *group) {
×
UNCOV
1201
        int r;
×
1202

UNCOV
1203
        assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
×
UNCOV
1204
        assert(dir);
×
UNCOV
1205
        assert(user);
×
UNCOV
1206
        assert(group);
×
1207

UNCOV
1208
        _cleanup_free_ char *membership = strjoin(user, ":", group, ".membership");
×
UNCOV
1209
        if (!membership)
×
UNCOV
1210
                return log_oom();
×
1211

UNCOV
1212
        _cleanup_close_ int fd = openat(dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644);
×
UNCOV
1213
        if (fd < 0)
×
UNCOV
1214
                return log_error_errno(errno, "Failed to create %s/%s: %m", dir, membership);
×
1215

UNCOV
1216
        r = loop_write(fd, "{}\n", SIZE_MAX);
×
UNCOV
1217
        if (r < 0)
×
UNCOV
1218
                return log_error_errno(r, "Failed to write empty JSON object into %s/%s: %m", dir, membership);
×
1219

UNCOV
1220
        log_info("Installed %s/%s from credential.", dir, membership);
×
1221

1222
        return 0;
1223
}
1224

1225
static int load_credential_one(
234✔
1226
                int credential_dir_fd,
1227
                const char *name,
1228
                int *userdb_dir_persist_fd,
1229
                int *userdb_dir_transient_fd) {
1230

1231
        int r;
234✔
1232

1233
        assert(credential_dir_fd >= 0);
234✔
1234
        assert(name);
234✔
1235
        assert(userdb_dir_persist_fd);
234✔
1236
        assert(userdb_dir_transient_fd);
234✔
1237

1238
        const char *suffix = startswith(name, "userdb.");
234✔
1239
        if (!suffix)
234✔
1240
                return 0;
234✔
1241

1242
        const char *transient = startswith(suffix, "transient."),
234✔
1243
                *user = startswith(transient ?: suffix, "user."),
468✔
1244
                *group = startswith(transient ?: suffix, "group.");
234✔
1245
        if (!user && !group)
234✔
1246
                return 0;
1247

1248
        const char *userdb_dir = transient ? "/run/userdb" : "/etc/userdb";
234✔
1249

1250
        int *userdb_dir_fd = transient ? userdb_dir_transient_fd : userdb_dir_persist_fd;
234✔
1251
        if (*userdb_dir_fd == -EBADF) {
234✔
1252
                *userdb_dir_fd = xopenat_full(AT_FDCWD, userdb_dir,
117✔
1253
                                              /* open_flags= */ O_DIRECTORY|O_CREAT|O_CLOEXEC,
1254
                                              /* xopen_flags= */ XO_LABEL,
1255
                                              /* mode= */ 0755);
1256
                if (*userdb_dir_fd < 0)
117✔
1257
                        return log_error_errno(*userdb_dir_fd, "Failed to open '%s/': %m", userdb_dir);
×
1258
        } else if (*userdb_dir_fd < 0)
117✔
UNCOV
1259
                return log_debug_errno(*userdb_dir_fd, "Previous attempt to open '%s/' failed, skipping.", userdb_dir);
×
1260

1261
        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
234✔
1262
        unsigned line = 0, column = 0;
234✔
1263
        r = sd_json_parse_file_at(NULL, credential_dir_fd, name, SD_JSON_PARSE_SENSITIVE, &v, &line, &column);
234✔
1264
        if (r < 0)
234✔
1265
                return log_error_errno(r, "Failed to parse credential '%s' as JSON at %u:%u: %m", name, line, column);
×
1266

1267
        _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *ur_stripped = NULL, *ur_privileged = NULL;
234✔
1268
        _cleanup_(group_record_unrefp) GroupRecord *gr = NULL, *gr_stripped = NULL, *gr_privileged = NULL;
234✔
1269
        _cleanup_free_ char *fn = NULL, *link = NULL;
234✔
1270

1271
        if (user) {
234✔
1272
                ur = user_record_new();
117✔
1273
                if (!ur)
117✔
1274
                        return log_oom();
14✔
1275

1276
                r = user_record_load(ur, v, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG);
117✔
1277
                if (r < 0)
117✔
1278
                        return r;
1279

1280
                if (user_record_is_root(ur))
117✔
UNCOV
1281
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'root' user from credentials is not supported.");
×
1282
                if (user_record_is_nobody(ur))
117✔
UNCOV
1283
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'nobody' user from credentials is not supported.");
×
1284

1285
                if (!streq_ptr(user, ur->user_name))
117✔
UNCOV
1286
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1287
                                               "Credential suffix '%s' does not match user record name '%s'",
1288
                                               user, strna(ur->user_name));
1289

1290
                if (!uid_is_valid(ur->uid))
117✔
UNCOV
1291
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON user record missing uid field");
×
1292

1293
                if (!gid_is_valid(user_record_gid(ur)))
117✔
UNCOV
1294
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON user record missing gid field");
×
1295

1296
                _cleanup_(user_record_unrefp) UserRecord *m = NULL;
117✔
1297
                r = userdb_by_name(ur->user_name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m);
117✔
1298
                if (r >= 0) {
117✔
1299
                        if (m->uid != ur->uid)
14✔
UNCOV
1300
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
1301
                                                       "Cannot create user %s from credential %s as it already exists with UID " UID_FMT " instead of " UID_FMT,
1302
                                                       ur->user_name, name, m->uid, ur->uid);
1303

1304
                        log_info("User with name %s and UID " UID_FMT " already exists, not creating user from credential %s", ur->user_name, ur->uid, name);
14✔
1305
                        return 0;
14✔
1306
                }
1307
                if (r != -ESRCH)
103✔
1308
                        return log_error_errno(r, "Failed to check if user with name %s already exists: %m", ur->user_name);
×
1309

1310
                m = user_record_unref(m);
103✔
1311
                r = userdb_by_uid(ur->uid, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m);
103✔
1312
                if (r >= 0) {
103✔
UNCOV
1313
                        if (!streq_ptr(ur->user_name, m->user_name))
×
UNCOV
1314
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
1315
                                                       "Cannot create user %s from credential %s as UID " UID_FMT " is already assigned to user %s",
1316
                                                       ur->user_name, name, ur->uid, m->user_name);
1317

UNCOV
1318
                        log_info("User with name %s and UID " UID_FMT " already exists, not creating user from credential %s", ur->user_name, ur->uid, name);
×
UNCOV
1319
                        return 0;
×
1320
                }
1321
                if (r != -ESRCH)
103✔
UNCOV
1322
                        return log_error_errno(r, "Failed to check if user with UID " UID_FMT " already exists: %m", ur->uid);
×
1323

1324
                r = user_record_clone(ur, USER_RECORD_LOAD_MASK_PRIVILEGED|USER_RECORD_LOG, &ur_stripped);
103✔
1325
                if (r < 0)
103✔
1326
                        return r;
1327

1328
                r = user_record_clone(ur, USER_RECORD_EXTRACT_PRIVILEGED|USER_RECORD_EMPTY_OK|USER_RECORD_LOG, &ur_privileged);
103✔
1329
                if (r < 0)
103✔
1330
                        return r;
1331

1332
                fn = strjoin(ur->user_name, ".user");
103✔
1333
                if (!fn)
103✔
1334
                        return log_oom();
×
1335

1336
                if (asprintf(&link, UID_FMT ".user", ur->uid) < 0)
103✔
UNCOV
1337
                        return log_oom();
×
1338
        } else {
1339
                assert(group);
117✔
1340

1341
                gr = group_record_new();
117✔
1342
                if (!gr)
117✔
1343
                        return log_oom();
14✔
1344

1345
                r = group_record_load(gr, v, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG);
117✔
1346
                if (r < 0)
117✔
1347
                        return r;
1348

1349
                if (group_record_is_root(gr))
117✔
UNCOV
1350
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'root' group from credentials is not supported.");
×
1351
                if (group_record_is_nobody(gr))
117✔
UNCOV
1352
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'nobody' group from credentials is not supported.");
×
1353

1354
                if (!streq_ptr(group, gr->group_name))
117✔
UNCOV
1355
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1356
                                               "Credential suffix '%s' does not match group record name '%s'",
1357
                                               group, strna(gr->group_name));
1358

1359
                if (!gid_is_valid(gr->gid))
117✔
UNCOV
1360
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON group record missing gid field");
×
1361

1362
                _cleanup_(group_record_unrefp) GroupRecord *m = NULL;
117✔
1363
                r = groupdb_by_name(gr->group_name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m);
117✔
1364
                if (r >= 0) {
117✔
1365
                        if (m->gid != gr->gid)
14✔
UNCOV
1366
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
1367
                                                       "Cannot create group %s from credential %s as it already exists with GID " GID_FMT " instead of " GID_FMT,
1368
                                                       gr->group_name, name, m->gid, gr->gid);
1369

1370
                        log_info("Group with name %s and GID " GID_FMT " already exists, not creating group from credential %s", gr->group_name, gr->gid, name);
14✔
1371
                        return 0;
14✔
1372
                }
1373
                if (r != -ESRCH)
103✔
1374
                        return log_error_errno(r, "Failed to check if group with name %s already exists: %m", gr->group_name);
×
1375

1376
                m = group_record_unref(m);
103✔
1377
                r = groupdb_by_gid(gr->gid, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m);
103✔
1378
                if (r >= 0) {
103✔
UNCOV
1379
                        if (!streq_ptr(gr->group_name, m->group_name))
×
UNCOV
1380
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
×
1381
                                                       "Cannot create group %s from credential %s as GID " GID_FMT " is already assigned to group %s",
1382
                                                       gr->group_name, name, gr->gid, m->group_name);
1383

UNCOV
1384
                        log_info("Group with name %s and GID " GID_FMT " already exists, not creating group from credential %s", gr->group_name, gr->gid, name);
×
UNCOV
1385
                        return 0;
×
1386
                }
1387
                if (r != -ESRCH)
103✔
1388
                        return log_error_errno(r, "Failed to check if group with GID " GID_FMT " already exists: %m", gr->gid);
×
1389

1390
                r = group_record_clone(gr, USER_RECORD_LOAD_MASK_PRIVILEGED|USER_RECORD_LOG, &gr_stripped);
103✔
1391
                if (r < 0)
103✔
1392
                        return r;
1393

1394
                r = group_record_clone(gr, USER_RECORD_EXTRACT_PRIVILEGED|USER_RECORD_EMPTY_OK|USER_RECORD_LOG, &gr_privileged);
103✔
1395
                if (r < 0)
103✔
1396
                        return r;
1397

1398
                fn = strjoin(gr->group_name, ".group");
103✔
1399
                if (!fn)
103✔
UNCOV
1400
                        return log_oom();
×
1401

1402
                if (asprintf(&link, GID_FMT ".group", gr->gid) < 0)
103✔
UNCOV
1403
                        return log_oom();
×
1404
        }
1405

1406
        if (!filename_is_valid(fn))
206✔
UNCOV
1407
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
×
1408
                                       "Passed credential '%s' would result in invalid filename '%s'.",
1409
                                       name, fn);
1410

1411
        _cleanup_free_ char *formatted = NULL;
206✔
1412
        r = sd_json_variant_format(ur ? ur_stripped->json : gr_stripped->json, SD_JSON_FORMAT_NEWLINE, &formatted);
206✔
1413
        if (r < 0)
206✔
UNCOV
1414
                return log_error_errno(r, "Failed to format JSON record: %m");
×
1415

1416
        r = write_string_file_at(*userdb_dir_fd, fn, formatted, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
206✔
1417
        if (r < 0)
206✔
UNCOV
1418
                return log_error_errno(r, "Failed to write JSON record to %s/%s: %m", userdb_dir, fn);
×
1419

1420
        if (symlinkat(fn, *userdb_dir_fd, link) < 0)
206✔
1421
                return log_error_errno(errno, "Failed to create symlink from %s to %s: %m", link, fn);
×
1422

1423
        log_info("Installed %s/%s from credential.", userdb_dir, fn);
206✔
1424

1425
        if ((ur && !sd_json_variant_is_blank_object(ur_privileged->json)) ||
206✔
1426
            (gr && !sd_json_variant_is_blank_object(gr_privileged->json))) {
103✔
1427
                fn = mfree(fn);
103✔
1428
                fn = strjoin(ur ? ur->user_name : gr->group_name, ur ? ".user-privileged" : ".group-privileged");
103✔
1429
                if (!fn)
103✔
UNCOV
1430
                        return log_oom();
×
1431

1432
                formatted = mfree(formatted);
103✔
1433
                r = sd_json_variant_format(ur ? ur_privileged->json : gr_privileged->json, SD_JSON_FORMAT_NEWLINE, &formatted);
103✔
1434
                if (r < 0)
103✔
1435
                        return log_error_errno(r, "Failed to format JSON record: %m");
×
1436

1437
                r = write_string_file_at(*userdb_dir_fd, fn, formatted, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MODE_0600);
103✔
1438
                if (r < 0)
103✔
1439
                        return log_error_errno(r, "Failed to write JSON record to %s/%s: %m", userdb_dir, fn);
×
1440

1441
                link = mfree(link);
103✔
1442

1443
                if (ur) {
103✔
1444
                        if (asprintf(&link, UID_FMT ".user-privileged", ur->uid) < 0)
103✔
1445
                                return log_oom();
×
1446
                } else {
1447
                        if (asprintf(&link, GID_FMT ".group-privileged", gr->gid) < 0)
×
UNCOV
1448
                                return log_oom();
×
1449
                }
1450

1451
                if (symlinkat(fn, *userdb_dir_fd, link) < 0)
103✔
UNCOV
1452
                        return log_error_errno(errno, "Failed to create symlink from %s to %s: %m", link, fn);
×
1453

1454
                log_info("Installed %s/%s from credential.", userdb_dir, fn);
103✔
1455
        }
1456

1457
        if (ur)
206✔
1458
                STRV_FOREACH(g, ur->member_of) {
103✔
UNCOV
1459
                        r = write_membership(*userdb_dir_fd, userdb_dir, ur->user_name, *g);
×
UNCOV
1460
                        if (r < 0)
×
1461
                                return r;
1462
                }
1463
        else
1464
                STRV_FOREACH(u, gr->members) {
103✔
UNCOV
1465
                        r = write_membership(*userdb_dir_fd, userdb_dir, *u, gr->group_name);
×
UNCOV
1466
                        if (r < 0)
×
1467
                                return r;
1468
                }
1469

1470
        if (ur && user_record_disposition(ur) == USER_REGULAR) {
206✔
1471
                const char *hd = user_record_home_directory(ur);
103✔
1472

1473
                r = RET_NERRNO(access(hd, F_OK));
309✔
1474
                if (r < 0) {
103✔
1475
                        if (r != -ENOENT)
103✔
UNCOV
1476
                                return log_error_errno(r, "Failed to check if %s exists: %m", hd);
×
1477

1478
                        WITH_UMASK(0000) {
206✔
1479
                                r = mkdir_parents(hd, 0755);
103✔
1480
                                if (r < 0)
103✔
UNCOV
1481
                                        return log_error_errno(r, "Failed to create parent directories of %s: %m", hd);
×
1482

1483
                                if (mkdir(hd, 0700) < 0 && errno != EEXIST)
103✔
UNCOV
1484
                                        return log_error_errno(errno, "Failed to create %s: %m", hd);
×
1485
                        }
1486

1487
                        if (chown(hd, ur->uid, user_record_gid(ur)) < 0)
103✔
UNCOV
1488
                                return log_error_errno(errno, "Failed to chown %s: %m", hd);
×
1489

1490
                        r = copy_tree(user_record_skeleton_directory(ur), hd, ur->uid, user_record_gid(ur),
103✔
1491
                                      COPY_REFLINK|COPY_MERGE, /* denylist= */ NULL, /* subvolumes= */NULL);
1492
                        if (r < 0 && r != -ENOENT)
103✔
1493
                                return log_error_errno(r, "Failed to copy skeleton directory to %s: %m", hd);
×
1494
                }
1495
        }
1496

1497
        return 0;
1498
}
1499

1500
static int load_credentials(int argc, char *argv[], void *userdata) {
117✔
1501
        int r;
117✔
1502

1503
        _cleanup_close_ int credential_dir_fd = open_credentials_dir();
234✔
1504
        if (IN_SET(credential_dir_fd, -ENXIO, -ENOENT)) {
117✔
1505
                /* Credential env var not set, or dir doesn't exist. */
UNCOV
1506
                log_debug("No credentials found.");
×
UNCOV
1507
                return 0;
×
1508
        }
1509
        if (credential_dir_fd < 0)
117✔
UNCOV
1510
                return log_error_errno(credential_dir_fd, "Failed to open credentials directory: %m");
×
1511

1512
        _cleanup_free_ DirectoryEntries *des = NULL;
117✔
1513
        r = readdir_all(credential_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des);
117✔
1514
        if (r < 0)
117✔
UNCOV
1515
                return log_error_errno(r, "Failed to enumerate credentials: %m");
×
1516

1517
        _cleanup_close_ int userdb_persist_dir_fd = -EBADF, userdb_transient_dir_fd = -EBADF;
234✔
1518

1519
        FOREACH_ARRAY(i, des->entries, des->n_entries) {
351✔
1520
                struct dirent *de = *i;
234✔
1521

1522
                if (de->d_type != DT_REG)
234✔
UNCOV
1523
                        continue;
×
1524

1525
                RET_GATHER(r, load_credential_one(
234✔
1526
                                credential_dir_fd,
1527
                                de->d_name,
1528
                                &userdb_persist_dir_fd,
1529
                                &userdb_transient_dir_fd));
1530
        }
1531

1532
        return r;
117✔
1533
}
1534

1535
static int help(int argc, char *argv[], void *userdata) {
1✔
1536
        _cleanup_free_ char *link = NULL;
1✔
1537
        int r;
1✔
1538

1539
        pager_open(arg_pager_flags);
1✔
1540

1541
        r = terminal_urlify_man("userdbctl", "1", &link);
1✔
1542
        if (r < 0)
1✔
UNCOV
1543
                return log_oom();
×
1544

1545
        printf("%s [OPTIONS...] COMMAND ...\n\n"
2✔
1546
               "%sShow user and group information.%s\n"
1547
               "\nCommands:\n"
1548
               "  user [USER…]               Inspect user\n"
1549
               "  group [GROUP…]             Inspect group\n"
1550
               "  users-in-group [GROUP…]    Show users that are members of specified groups\n"
1551
               "  groups-of-user [USER…]     Show groups the specified users are members of\n"
1552
               "  services                   Show enabled database services\n"
1553
               "  ssh-authorized-keys USER   Show SSH authorized keys for user\n"
1554
               "  load-credentials           Write static user/group records from credentials\n"
1555
               "\nOptions:\n"
1556
               "  -h --help                  Show this help\n"
1557
               "     --version               Show package version\n"
1558
               "     --no-pager              Do not pipe output into a pager\n"
1559
               "     --no-legend             Do not show the headers and footers\n"
1560
               "     --output=MODE           Select output mode (classic, friendly, table, json)\n"
1561
               "  -j                         Equivalent to --output=json\n"
1562
               "  -s --service=SERVICE[:SERVICE…]\n"
1563
               "                             Query the specified service\n"
1564
               "     --with-nss=BOOL         Control whether to include glibc NSS data\n"
1565
               "  -N                         Do not synthesize or include glibc NSS data\n"
1566
               "                             (Same as --synthesize=no --with-nss=no)\n"
1567
               "     --synthesize=BOOL       Synthesize root/nobody user\n"
1568
               "     --with-dropin=BOOL      Control whether to include drop-in records\n"
1569
               "     --with-varlink=BOOL     Control whether to talk to services at all\n"
1570
               "     --multiplexer=BOOL      Control whether to use the multiplexer\n"
1571
               "     --json=pretty|short     JSON output mode\n"
1572
               "     --chain                 Chain another command\n"
1573
               "     --uid-min=ID            Filter by minimum UID/GID (default 0)\n"
1574
               "     --uid-max=ID            Filter by maximum UID/GID (default 4294967294)\n"
1575
               "  -z --fuzzy                 Do a fuzzy name search\n"
1576
               "     --disposition=VALUE     Filter by disposition\n"
1577
               "  -I                         Equivalent to --disposition=intrinsic\n"
1578
               "  -S                         Equivalent to --disposition=system\n"
1579
               "  -R                         Equivalent to --disposition=regular\n"
1580
               "     --boundaries=BOOL       Show/hide UID/GID range boundaries in output\n"
1581
               "  -B                         Equivalent to --boundaries=no\n"
1582
               "  -F --from-file=PATH        Read JSON record from file\n"
1583
               "\nSee the %s for details.\n",
1584
               program_invocation_short_name,
1585
               ansi_highlight(),
1586
               ansi_normal(),
1587
               link);
1588

1589
        return 0;
1590
}
1591

1592
static int parse_argv(int argc, char *argv[]) {
314✔
1593

1594
        enum {
314✔
1595
                ARG_VERSION = 0x100,
1596
                ARG_NO_PAGER,
1597
                ARG_NO_LEGEND,
1598
                ARG_OUTPUT,
1599
                ARG_WITH_NSS,
1600
                ARG_WITH_DROPIN,
1601
                ARG_WITH_VARLINK,
1602
                ARG_SYNTHESIZE,
1603
                ARG_MULTIPLEXER,
1604
                ARG_JSON,
1605
                ARG_CHAIN,
1606
                ARG_UID_MIN,
1607
                ARG_UID_MAX,
1608
                ARG_UUID,
1609
                ARG_DISPOSITION,
1610
                ARG_BOUNDARIES,
1611
        };
1612

1613
        static const struct option options[] = {
314✔
1614
                { "help",         no_argument,       NULL, 'h'              },
1615
                { "version",      no_argument,       NULL, ARG_VERSION      },
1616
                { "no-pager",     no_argument,       NULL, ARG_NO_PAGER     },
1617
                { "no-legend",    no_argument,       NULL, ARG_NO_LEGEND    },
1618
                { "output",       required_argument, NULL, ARG_OUTPUT       },
1619
                { "service",      required_argument, NULL, 's'              },
1620
                { "with-nss",     required_argument, NULL, ARG_WITH_NSS     },
1621
                { "with-dropin",  required_argument, NULL, ARG_WITH_DROPIN  },
1622
                { "with-varlink", required_argument, NULL, ARG_WITH_VARLINK },
1623
                { "synthesize",   required_argument, NULL, ARG_SYNTHESIZE   },
1624
                { "multiplexer",  required_argument, NULL, ARG_MULTIPLEXER  },
1625
                { "json",         required_argument, NULL, ARG_JSON         },
1626
                { "chain",        no_argument,       NULL, ARG_CHAIN        },
1627
                { "uid-min",      required_argument, NULL, ARG_UID_MIN      },
1628
                { "uid-max",      required_argument, NULL, ARG_UID_MAX      },
1629
                { "uuid",         required_argument, NULL, ARG_UUID         },
1630
                { "fuzzy",        no_argument,       NULL, 'z'              },
1631
                { "disposition",  required_argument, NULL, ARG_DISPOSITION  },
1632
                { "boundaries",   required_argument, NULL, ARG_BOUNDARIES   },
1633
                { "from-file",    required_argument, NULL, 'F'              },
1634
                {}
1635
        };
1636

1637
        const char *e;
314✔
1638
        int r;
314✔
1639

1640
        assert(argc >= 0);
314✔
1641
        assert(argv);
314✔
1642

1643
        /* We are going to update this environment variable with our own, hence let's first read what is already set */
1644
        e = getenv("SYSTEMD_ONLY_USERDB");
314✔
1645
        if (e) {
314✔
UNCOV
1646
                char **l;
×
1647

UNCOV
1648
                l = strv_split(e, ":");
×
UNCOV
1649
                if (!l)
×
UNCOV
1650
                        return log_oom();
×
1651

UNCOV
1652
                strv_free(arg_services);
×
UNCOV
1653
                arg_services = l;
×
1654
        }
1655

1656
        /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
1657
         * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
1658
        optind = 0;
314✔
1659

1660
        for (;;) {
411✔
1661
                int c;
411✔
1662

1663
                c = getopt_long(argc, argv,
411✔
1664
                                arg_chain ? "+hjs:NISRzBF:" : "hjs:NISRzBF:", /* When --chain was used disable parsing of further switches */
411✔
1665
                                options, NULL);
1666
                if (c < 0)
411✔
1667
                        break;
1668

1669
                switch (c) {
127✔
1670

1671
                case 'h':
1✔
1672
                        return help(0, NULL, NULL);
1✔
1673

1674
                case ARG_VERSION:
1✔
1675
                        return version();
1✔
1676

1677
                case ARG_NO_PAGER:
2✔
1678
                        arg_pager_flags |= PAGER_DISABLE;
2✔
1679
                        break;
2✔
1680

1681
                case ARG_NO_LEGEND:
3✔
1682
                        arg_legend = false;
3✔
1683
                        break;
3✔
1684

1685
                case ARG_OUTPUT:
8✔
1686
                        if (streq(optarg, "help"))
8✔
UNCOV
1687
                                return DUMP_STRING_TABLE(output, Output, _OUTPUT_MAX);
×
1688

1689
                        arg_output = output_from_string(optarg);
8✔
1690
                        if (arg_output < 0)
8✔
1691
                                return log_error_errno(arg_output, "Invalid --output= mode: %s", optarg);
4✔
1692

1693
                        arg_json_format_flags = arg_output == OUTPUT_JSON ? SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_COLOR_AUTO : SD_JSON_FORMAT_OFF;
4✔
1694
                        break;
4✔
1695

1696
                case ARG_JSON:
6✔
1697
                        r = parse_json_argument(optarg, &arg_json_format_flags);
6✔
1698
                        if (r <= 0)
6✔
1699
                                return r;
1700

1701
                        arg_output = sd_json_format_enabled(arg_json_format_flags) ? OUTPUT_JSON : _OUTPUT_INVALID;
2✔
1702
                        break;
2✔
1703

1704
                case 'j':
26✔
1705
                        arg_json_format_flags = SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_COLOR_AUTO;
26✔
1706
                        arg_output = OUTPUT_JSON;
26✔
1707
                        break;
26✔
1708

UNCOV
1709
                case 's':
×
UNCOV
1710
                        if (isempty(optarg))
×
UNCOV
1711
                                arg_services = strv_free(arg_services);
×
1712
                        else {
UNCOV
1713
                                r = strv_split_and_extend(&arg_services, optarg, ":", /* filter_duplicates = */ true);
×
UNCOV
1714
                                if (r < 0)
×
UNCOV
1715
                                        return log_error_errno(r, "Failed to parse -s/--service= argument: %m");
×
1716
                        }
1717

1718
                        break;
1719

1720
                case 'N':
1✔
1721
                        arg_userdb_flags |= USERDB_EXCLUDE_NSS|USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN;
1✔
1722
                        break;
1✔
1723

1724
                case ARG_WITH_NSS:
14✔
1725
                        r = parse_boolean_argument("--with-nss=", optarg, NULL);
14✔
1726
                        if (r < 0)
14✔
1727
                                return r;
1728

1729
                        SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_NSS, !r);
10✔
1730
                        break;
10✔
1731

1732
                case ARG_WITH_DROPIN:
8✔
1733
                        r = parse_boolean_argument("--with-dropin=", optarg, NULL);
8✔
1734
                        if (r < 0)
8✔
1735
                                return r;
1736

1737
                        SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_DROPIN, !r);
4✔
1738
                        break;
4✔
1739

1740
                case ARG_WITH_VARLINK:
7✔
1741
                        r = parse_boolean_argument("--with-varlink=", optarg, NULL);
7✔
1742
                        if (r < 0)
7✔
1743
                                return r;
1744

1745
                        SET_FLAG(arg_userdb_flags, USERDB_EXCLUDE_VARLINK, !r);
3✔
1746
                        break;
3✔
1747

1748
                case ARG_SYNTHESIZE:
12✔
1749
                        r = parse_boolean_argument("--synthesize=", optarg, NULL);
12✔
1750
                        if (r < 0)
12✔
1751
                                return r;
1752

1753
                        SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE_INTRINSIC|USERDB_DONT_SYNTHESIZE_FOREIGN, !r);
8✔
1754
                        break;
8✔
1755

1756
                case ARG_MULTIPLEXER:
6✔
1757
                        r = parse_boolean_argument("--multiplexer=", optarg, NULL);
6✔
1758
                        if (r < 0)
6✔
1759
                                return r;
1760

1761
                        SET_FLAG(arg_userdb_flags, USERDB_AVOID_MULTIPLEXER, !r);
2✔
1762
                        break;
2✔
1763

1764
                case ARG_CHAIN:
4✔
1765
                        arg_chain = true;
4✔
1766
                        break;
4✔
1767

1768
                case ARG_DISPOSITION: {
4✔
1769
                        UserDisposition d = user_disposition_from_string(optarg);
4✔
1770
                        if (d < 0)
4✔
UNCOV
1771
                                return log_error_errno(d, "Unknown user disposition: %s", optarg);
×
1772

1773
                        if (arg_disposition_mask == UINT64_MAX)
4✔
1774
                                arg_disposition_mask = 0;
2✔
1775

1776
                        arg_disposition_mask |= UINT64_C(1) << d;
4✔
1777
                        break;
4✔
1778
                }
1779

1780
                case 'I':
2✔
1781
                        if (arg_disposition_mask == UINT64_MAX)
2✔
1782
                                arg_disposition_mask = 0;
2✔
1783

1784
                        arg_disposition_mask |= UINT64_C(1) << USER_INTRINSIC;
2✔
1785
                        break;
2✔
1786

1787
                case 'S':
6✔
1788
                        if (arg_disposition_mask == UINT64_MAX)
6✔
1789
                                arg_disposition_mask = 0;
4✔
1790

1791
                        arg_disposition_mask |= UINT64_C(1) << USER_SYSTEM;
6✔
1792
                        break;
6✔
1793

1794
                case 'R':
2✔
1795
                        if (arg_disposition_mask == UINT64_MAX)
2✔
1796
                                arg_disposition_mask = 0;
2✔
1797

1798
                        arg_disposition_mask |= UINT64_C(1) << USER_REGULAR;
2✔
1799
                        break;
2✔
1800

1801
                case ARG_UID_MIN:
2✔
1802
                        r = parse_uid(optarg, &arg_uid_min);
2✔
1803
                        if (r < 0)
2✔
UNCOV
1804
                                return log_error_errno(r, "Failed to parse --uid-min= value: %s", optarg);
×
1805
                        break;
1806

1807
                case ARG_UID_MAX:
2✔
1808
                        r = parse_uid(optarg, &arg_uid_max);
2✔
1809
                        if (r < 0)
2✔
1810
                                return log_error_errno(r, "Failed to parse --uid-max= value: %s", optarg);
×
1811
                        break;
1812

UNCOV
1813
                case ARG_UUID:
×
UNCOV
1814
                        r = sd_id128_from_string(optarg, &arg_uuid);
×
UNCOV
1815
                        if (r < 0)
×
UNCOV
1816
                                return log_error_errno(r, "Failed to parse --uuid= value: %s", optarg);
×
1817
                        break;
1818

1819
                case 'z':
2✔
1820
                        arg_fuzzy = true;
2✔
1821
                        break;
2✔
1822

UNCOV
1823
                case ARG_BOUNDARIES:
×
UNCOV
1824
                        r = parse_boolean_argument("boundaries", optarg, &arg_boundaries);
×
UNCOV
1825
                        if (r < 0)
×
1826
                                return r;
1827
                        break;
1828

1829
                case 'B':
2✔
1830
                        arg_boundaries = false;
2✔
1831
                        break;
2✔
1832

1833
                case 'F': {
6✔
1834
                        if (isempty(optarg)) {
6✔
UNCOV
1835
                                arg_from_file = sd_json_variant_unref(arg_from_file);
×
1836
                                break;
6✔
1837
                        }
1838

1839
                        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
6✔
1840
                        const char *fn = streq(optarg, "-") ? NULL : optarg;
6✔
1841
                        unsigned line = 0;
6✔
1842
                        r = sd_json_parse_file(fn ? NULL : stdin, fn ?: "<stdin>", SD_JSON_PARSE_SENSITIVE, &v, &line, /* reterr_column= */ NULL);
6✔
1843
                        if (r < 0)
6✔
UNCOV
1844
                                return log_syntax(/* unit= */ NULL, LOG_ERR, fn ?: "<stdin>", line, r, "JSON parse failure.");
×
1845

1846
                        sd_json_variant_unref(arg_from_file);
6✔
1847
                        arg_from_file = TAKE_PTR(v);
6✔
1848
                        break;
6✔
1849
                }
1850

1851
                case '?':
1852
                        return -EINVAL;
1853

UNCOV
1854
                default:
×
UNCOV
1855
                        assert_not_reached();
×
1856
                }
1857
        }
1858

1859
        if (arg_uid_min > arg_uid_max)
284✔
UNCOV
1860
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Minimum UID/GID " UID_FMT " is above maximum UID/GID " UID_FMT ", refusing.", arg_uid_min, arg_uid_max);
×
1861

1862
        /* If not mask was specified, use the all bits on mask */
1863
        if (arg_disposition_mask == UINT64_MAX)
284✔
1864
                arg_disposition_mask = USER_DISPOSITION_MASK_ALL;
274✔
1865

1866
        if (arg_from_file)
284✔
1867
                arg_boundaries = false;
6✔
1868

1869
        return 1;
1870
}
1871

1872
static int run(int argc, char *argv[]) {
314✔
1873
        static const Verb verbs[] = {
314✔
1874
                { "help",                VERB_ANY, VERB_ANY, 0,            help                },
1875
                { "user",                VERB_ANY, VERB_ANY, VERB_DEFAULT, display_user        },
1876
                { "group",               VERB_ANY, VERB_ANY, 0,            display_group       },
1877
                { "users-in-group",      VERB_ANY, VERB_ANY, 0,            display_memberships },
1878
                { "groups-of-user",      VERB_ANY, VERB_ANY, 0,            display_memberships },
1879
                { "services",            VERB_ANY, 1,        0,            display_services    },
1880
                { "ssh-authorized-keys", 2,        VERB_ANY, 0,            ssh_authorized_keys },
1881
                { "load-credentials",    VERB_ANY, 1,        0,            load_credentials    },
1882
                {}
1883
        };
1884

1885
        int r;
314✔
1886

1887
        log_setup();
314✔
1888

1889
        r = parse_argv(argc, argv);
314✔
1890
        if (r <= 0)
314✔
1891
                return r;
1892

1893
        if (arg_services) {
284✔
UNCOV
1894
                _cleanup_free_ char *e = NULL;
×
1895

UNCOV
1896
                e = strv_join(arg_services, ":");
×
UNCOV
1897
                if (!e)
×
UNCOV
1898
                        return log_oom();
×
1899

UNCOV
1900
                if (setenv("SYSTEMD_ONLY_USERDB", e, true) < 0)
×
UNCOV
1901
                        return log_error_errno(r, "Failed to set $SYSTEMD_ONLY_USERDB: %m");
×
1902

UNCOV
1903
                log_info("Enabled services: %s", e);
×
1904
        } else
1905
                assert_se(unsetenv("SYSTEMD_ONLY_USERDB") == 0);
284✔
1906

1907
        return dispatch_verb(argc, argv, verbs, NULL);
284✔
1908
}
1909

1910
DEFINE_MAIN_FUNCTION(run);
314✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc