Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sudo-project
GitHub Repository: sudo-project/sudo
Path: blob/main/plugins/sudoers/cvtsudoers_json.c
1532 views
1
/*
2
* SPDX-License-Identifier: ISC
3
*
4
* Copyright (c) 2013-2024 Todd C. Miller <[email protected]>
5
*
6
* Permission to use, copy, modify, and distribute this software for any
7
* purpose with or without fee is hereby granted, provided that the above
8
* copyright notice and this permission notice appear in all copies.
9
*
10
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
*/
18
19
#include <config.h>
20
21
#include <stdio.h>
22
#include <stdlib.h>
23
#include <string.h>
24
#include <unistd.h>
25
#include <stdarg.h>
26
#include <ctype.h>
27
28
#include <sudoers.h>
29
#include <sudo_digest.h>
30
#include <sudo_json.h>
31
#include <cvtsudoers.h>
32
#include <gram.h>
33
34
/*
35
* Closure used to store state when iterating over all aliases.
36
*/
37
struct json_alias_closure {
38
struct json_container *jsonc;
39
const char *title;
40
unsigned int count;
41
short alias_type;
42
};
43
44
/*
45
* Type values used to disambiguate the generic WORD and ALIAS types.
46
*/
47
enum word_type {
48
TYPE_COMMAND,
49
TYPE_HOSTNAME,
50
TYPE_RUNASGROUP,
51
TYPE_RUNASUSER,
52
TYPE_USERNAME
53
};
54
55
/*
56
* Print sudo command member in JSON format, with correct indentation.
57
* Returns true on success, false on a memory allocation failure.
58
*/
59
static bool
60
print_command_json(struct json_container *jsonc, const char *name, bool negated)
61
{
62
struct sudo_command *c = (struct sudo_command *)name;
63
struct command_digest *digest;
64
struct json_value value;
65
char *cmnd = c->cmnd;
66
unsigned int digest_type;
67
const char *digest_name;
68
debug_decl(print_command_json, SUDOERS_DEBUG_UTIL);
69
70
/* Print command with optional command line args. */
71
if (c->args != NULL) {
72
if (asprintf(&cmnd, "%s %s", c->cmnd, c->args) == -1)
73
debug_return_bool(false);
74
}
75
value.type = JSON_STRING;
76
value.u.string = cmnd ? cmnd : (char *)"ALL";
77
78
if (!negated && TAILQ_EMPTY(&c->digests)) {
79
/* Print as { "command": "command and args" } */
80
if (!sudo_json_add_value_as_object(jsonc, "command", &value))
81
debug_return_bool(false);
82
} else {
83
/* Print as multi-line object. */
84
if (!sudo_json_open_object(jsonc, NULL))
85
debug_return_bool(false);
86
if (!sudo_json_add_value(jsonc, "command", &value))
87
debug_return_bool(false);
88
89
/* Optional digest list, ordered by digest type. */
90
for (digest_type = 0; digest_type < SUDO_DIGEST_INVALID; digest_type++) {
91
unsigned int ndigests = 0;
92
93
TAILQ_FOREACH(digest, &c->digests, entries) {
94
if (digest->digest_type == digest_type)
95
ndigests++;
96
}
97
if (ndigests == 0)
98
continue;
99
100
digest_name = digest_type_to_name(digest_type);
101
if (ndigests > 1) {
102
if (!sudo_json_open_array(jsonc, digest_name))
103
debug_return_bool(false);
104
/* Only use digest_name for the array key, not value. */
105
digest_name = NULL;
106
}
107
TAILQ_FOREACH(digest, &c->digests, entries) {
108
if (digest->digest_type != digest_type)
109
continue;
110
value.type = JSON_STRING;
111
value.u.string = digest->digest_str;
112
if (!sudo_json_add_value(jsonc, digest_name, &value))
113
debug_return_bool(false);
114
}
115
if (ndigests > 1) {
116
if (!sudo_json_close_array(jsonc))
117
debug_return_bool(false);
118
}
119
}
120
121
/* Command may be negated. */
122
if (negated) {
123
value.type = JSON_BOOL;
124
value.u.boolean = true;
125
if (!sudo_json_add_value(jsonc, "negated", &value))
126
debug_return_bool(false);
127
}
128
129
if (!sudo_json_close_object(jsonc))
130
debug_return_bool(false);
131
}
132
133
if (cmnd != c->cmnd)
134
free(cmnd);
135
136
debug_return_bool(true);
137
}
138
139
/*
140
* Map an alias type to enum word_type.
141
*/
142
static enum word_type
143
alias_to_word_type(short alias_type)
144
{
145
switch (alias_type) {
146
case CMNDALIAS:
147
return TYPE_COMMAND;
148
case HOSTALIAS:
149
return TYPE_HOSTNAME;
150
case RUNASALIAS:
151
return TYPE_RUNASUSER;
152
case USERALIAS:
153
return TYPE_USERNAME;
154
default:
155
sudo_fatalx_nodebug("unexpected alias type %d", alias_type);
156
}
157
}
158
159
/*
160
* Map a Defaults type to enum word_type.
161
*/
162
static enum word_type
163
defaults_to_word_type(int defaults_type)
164
{
165
switch (defaults_type) {
166
case DEFAULTS_CMND:
167
return TYPE_COMMAND;
168
case DEFAULTS_HOST:
169
return TYPE_HOSTNAME;
170
case DEFAULTS_RUNAS:
171
return TYPE_RUNASUSER;
172
case DEFAULTS_USER:
173
return TYPE_USERNAME;
174
default:
175
sudo_fatalx_nodebug("unexpected defaults type %d", defaults_type);
176
}
177
}
178
179
/*
180
* Print struct member in JSON format, with correct indentation.
181
* Returns true on success, false on a memory allocation failure.
182
*/
183
static bool
184
print_member_json_int(struct json_container *jsonc,
185
const struct sudoers_parse_tree *parse_tree, char *name, int type,
186
bool negated, enum word_type word_type, bool expand_aliases)
187
{
188
struct json_value value;
189
const char *typestr = NULL;
190
short alias_type = UNSPEC;
191
struct alias *a = NULL;
192
const char *errstr;
193
id_t id;
194
debug_decl(print_member_json_int, SUDOERS_DEBUG_UTIL);
195
196
/* Most of the time we print a string. */
197
value.type = JSON_STRING;
198
switch (type) {
199
case ALL:
200
if (name == NULL) {
201
value.u.string = "ALL";
202
} else {
203
/* ALL used with digest, print as a command. */
204
type = COMMAND;
205
}
206
break;
207
case MYSELF:
208
value.u.string = "";
209
break;
210
default:
211
if (name == NULL)
212
sudo_fatalx("missing member name for type %d", type);
213
value.u.string = name;
214
}
215
216
/* Special handling for ALIAS, which might actually be a WORD. */
217
if (type == ALIAS) {
218
switch (word_type) {
219
case TYPE_COMMAND:
220
alias_type = CMNDALIAS;
221
typestr = "cmndalias";
222
break;
223
case TYPE_HOSTNAME:
224
alias_type = HOSTALIAS;
225
typestr = "hostalias";
226
break;
227
case TYPE_RUNASGROUP:
228
case TYPE_RUNASUSER:
229
alias_type = RUNASALIAS;
230
typestr = "runasalias";
231
break;
232
case TYPE_USERNAME:
233
alias_type = USERALIAS;
234
typestr = "useralias";
235
break;
236
default:
237
sudo_fatalx("unexpected word type %d", word_type);
238
}
239
240
a = alias_get(parse_tree, value.u.string, alias_type);
241
if (a == NULL && alias_type != CMNDALIAS) {
242
/* Alias does not resolve, treat as WORD instead. */
243
type = WORD;
244
}
245
}
246
247
switch (type) {
248
case USERGROUP:
249
value.u.string++; /* skip leading '%' */
250
if (*value.u.string == ':') {
251
value.u.string++;
252
typestr = "nonunixgroup";
253
if (*value.u.string == '#') {
254
id = sudo_strtoid(value.u.string + 1, &errstr);
255
if (errstr != NULL) {
256
sudo_warnx("internal error: non-Unix group-ID %s: \"%s\"",
257
errstr, value.u.string + 1);
258
} else {
259
value.type = JSON_ID;
260
value.u.id = id;
261
typestr = "nonunixgid";
262
}
263
}
264
} else {
265
typestr = "usergroup";
266
if (*value.u.string == '#') {
267
id = sudo_strtoid(value.u.string + 1, &errstr);
268
if (errstr != NULL) {
269
sudo_warnx("internal error: group-ID %s: \"%s\"",
270
errstr, value.u.string + 1);
271
} else {
272
value.type = JSON_ID;
273
value.u.id = id;
274
typestr = "usergid";
275
}
276
}
277
}
278
break;
279
case NETGROUP:
280
typestr = "netgroup";
281
value.u.string++; /* skip leading '+' */
282
break;
283
case NTWKADDR:
284
typestr = "networkaddr";
285
break;
286
case COMMAND:
287
if (!print_command_json(jsonc, name, negated))
288
goto oom;
289
debug_return_bool(true);
290
case ALL:
291
case MYSELF:
292
case WORD:
293
switch (word_type) {
294
case TYPE_COMMAND:
295
typestr = "command";
296
break;
297
case TYPE_HOSTNAME:
298
typestr = "hostname";
299
break;
300
case TYPE_RUNASGROUP:
301
typestr = "usergroup";
302
break;
303
case TYPE_RUNASUSER:
304
case TYPE_USERNAME:
305
typestr = "username";
306
if (*value.u.string == '#') {
307
id = sudo_strtoid(value.u.string + 1, &errstr);
308
if (errstr != NULL) {
309
sudo_warnx("internal error: user-ID %s: \"%s\"",
310
errstr, name);
311
} else {
312
value.type = JSON_ID;
313
value.u.id = id;
314
typestr = "userid";
315
}
316
}
317
break;
318
default:
319
sudo_fatalx("unexpected word type %d", word_type);
320
}
321
break;
322
case ALIAS:
323
/* handled earlier */
324
break;
325
default:
326
sudo_fatalx("unexpected member type %d", type);
327
}
328
329
if (expand_aliases && type == ALIAS) {
330
/* Print each member of the alias. */
331
if (a != NULL) {
332
struct member *m;
333
334
TAILQ_FOREACH(m, &a->members, entries) {
335
if (!print_member_json_int(jsonc, parse_tree, m->name, m->type,
336
negated ? !m->negated : m->negated, word_type, true))
337
goto oom;
338
}
339
}
340
} else {
341
if (negated) {
342
if (!sudo_json_open_object(jsonc, NULL))
343
goto oom;
344
if (!sudo_json_add_value(jsonc, typestr, &value))
345
goto oom;
346
value.type = JSON_BOOL;
347
value.u.boolean = true;
348
if (!sudo_json_add_value(jsonc, "negated", &value))
349
goto oom;
350
if (!sudo_json_close_object(jsonc))
351
goto oom;
352
} else {
353
if (!sudo_json_add_value_as_object(jsonc, typestr, &value))
354
goto oom;
355
}
356
}
357
358
if (a != NULL)
359
alias_put(a);
360
debug_return_bool(true);
361
oom:
362
/* warning printed by caller */
363
if (a != NULL)
364
alias_put(a);
365
debug_return_bool(false);
366
}
367
368
static bool
369
print_member_json(struct json_container *jsonc,
370
const struct sudoers_parse_tree *parse_tree, struct member *m,
371
enum word_type word_type, bool expand_aliases)
372
{
373
return print_member_json_int(jsonc, parse_tree, m->name, m->type,
374
m->negated, word_type, expand_aliases);
375
}
376
377
/*
378
* Callback for alias_apply() to print an alias entry if it matches
379
* the type specified in the closure.
380
* Returns 0 on success and -1 on memory allocation failure.
381
*/
382
static int
383
print_alias_json(struct sudoers_parse_tree *parse_tree, struct alias *a,
384
void *v)
385
{
386
struct json_alias_closure *closure = v;
387
struct member *m;
388
debug_decl(print_alias_json, SUDOERS_DEBUG_UTIL);
389
390
if (a->type != closure->alias_type)
391
debug_return_int(0);
392
393
/* Open the aliases object or close the last entry, then open new one. */
394
if (closure->count++ == 0) {
395
if (!sudo_json_open_object(closure->jsonc, closure->title))
396
debug_return_int(-1);
397
} else {
398
if (!sudo_json_close_array(closure->jsonc))
399
debug_return_int(-1);
400
}
401
if (!sudo_json_open_array(closure->jsonc, a->name))
402
debug_return_int(-1);
403
404
TAILQ_FOREACH(m, &a->members, entries) {
405
if (!print_member_json(closure->jsonc, parse_tree, m,
406
alias_to_word_type(closure->alias_type), false))
407
debug_return_int(-1);
408
}
409
debug_return_int(0);
410
}
411
412
/*
413
* Print the binding for a Defaults entry of the specified type.
414
* Returns true on success, false on a memory allocation failure.
415
*/
416
static bool
417
print_binding_json(struct json_container *jsonc,
418
const struct sudoers_parse_tree *parse_tree,
419
struct defaults_binding *binding, int type, bool expand_aliases)
420
{
421
struct member *m;
422
debug_decl(print_binding_json, SUDOERS_DEBUG_UTIL);
423
424
if (TAILQ_EMPTY(&binding->members))
425
debug_return_bool(true);
426
427
/* Print each member object in binding. */
428
if (!sudo_json_open_array(jsonc, "Binding"))
429
goto oom;
430
TAILQ_FOREACH(m, &binding->members, entries) {
431
if (!print_member_json(jsonc, parse_tree, m,
432
defaults_to_word_type(type), expand_aliases))
433
goto oom;
434
}
435
if (!sudo_json_close_array(jsonc))
436
goto oom;
437
438
debug_return_bool(true);
439
oom:
440
/* warning printed by caller */
441
debug_return_bool(false);
442
}
443
444
/*
445
* Print a Defaults list JSON format.
446
* Returns true on success, false on a memory allocation failure.
447
*/
448
static bool
449
print_defaults_list_json(struct json_container *jsonc, struct defaults *def)
450
{
451
char savech, *start, *end = def->val;
452
struct json_value value;
453
debug_decl(print_defaults_list_json, SUDOERS_DEBUG_UTIL);
454
455
if (!sudo_json_open_object(jsonc, NULL))
456
goto oom;
457
value.type = JSON_STRING;
458
switch (def->op) {
459
case '+':
460
value.u.string = "list_add";
461
break;
462
case '-':
463
value.u.string = "list_remove";
464
break;
465
case true:
466
value.u.string = "list_assign";
467
break;
468
default:
469
sudo_warnx("internal error: unexpected list op %d", def->op);
470
value.u.string = "unsupported";
471
break;
472
}
473
if (!sudo_json_add_value(jsonc, "operation", &value))
474
goto oom;
475
if (!sudo_json_open_array(jsonc, def->var))
476
goto oom;
477
/* Split value into multiple space-separated words. */
478
do {
479
/* Remove leading blanks, must have a non-empty string. */
480
for (start = end; isblank((unsigned char)*start); start++)
481
continue;
482
if (*start == '\0')
483
break;
484
485
/* Find the end and print it. */
486
for (end = start; *end && !isblank((unsigned char)*end); end++)
487
continue;
488
savech = *end;
489
*end = '\0';
490
value.type = JSON_STRING;
491
value.u.string = start;
492
if (!sudo_json_add_value(jsonc, NULL, &value))
493
goto oom;
494
*end = savech;
495
} while (*end++ != '\0');
496
if (!sudo_json_close_array(jsonc))
497
goto oom;
498
if (!sudo_json_close_object(jsonc))
499
goto oom;
500
501
debug_return_bool(true);
502
oom:
503
/* warning printed by caller */
504
debug_return_bool(false);
505
}
506
507
static int
508
get_defaults_type(struct defaults *def)
509
{
510
struct sudo_defs_types *cur;
511
512
/* Look up def in table to find its type. */
513
for (cur = sudo_defs_table; cur->name; cur++) {
514
if (strcmp(def->var, cur->name) == 0)
515
return cur->type;
516
}
517
return -1;
518
}
519
520
/*
521
* Export all Defaults in JSON format.
522
* Returns true on success, else false, displaying a warning.
523
*/
524
static bool
525
print_defaults_json(struct json_container *jsonc,
526
const struct sudoers_parse_tree *parse_tree, bool expand_aliases)
527
{
528
struct json_value value;
529
struct defaults *def, *next;
530
int type;
531
debug_decl(print_defaults_json, SUDOERS_DEBUG_UTIL);
532
533
if (TAILQ_EMPTY(&parse_tree->defaults))
534
debug_return_bool(true);
535
536
if (!sudo_json_open_array(jsonc, "Defaults"))
537
goto oom;
538
539
TAILQ_FOREACH_SAFE(def, &parse_tree->defaults, entries, next) {
540
type = get_defaults_type(def);
541
if (type == -1) {
542
log_warnx(U_("%s:%d:%d: unknown defaults entry \"%s\""),
543
def->file, def->line, def->column, def->var);
544
/* XXX - just pass it through as a string anyway? */
545
continue;
546
}
547
548
/* Found it, print object container and binding (if any). */
549
if (!sudo_json_open_object(jsonc, NULL))
550
goto oom;
551
if (!print_binding_json(jsonc, parse_tree, def->binding, def->type,
552
expand_aliases))
553
goto oom;
554
555
/* Validation checks. */
556
/* XXX - validate values in addition to names? */
557
558
/* Print options, merging ones with the same binding. */
559
if (!sudo_json_open_array(jsonc, "Options"))
560
goto oom;
561
for (;;) {
562
next = TAILQ_NEXT(def, entries);
563
/* XXX - need to update cur too */
564
if ((type & T_MASK) == T_FLAG || def->val == NULL) {
565
value.type = JSON_BOOL;
566
value.u.boolean = def->op;
567
if (!sudo_json_add_value_as_object(jsonc, def->var, &value))
568
goto oom;
569
} else if ((type & T_MASK) == T_LIST) {
570
if (!print_defaults_list_json(jsonc, def))
571
goto oom;
572
} else {
573
value.type = JSON_STRING;
574
value.u.string = def->val;
575
if (!sudo_json_add_value_as_object(jsonc, def->var, &value))
576
goto oom;
577
}
578
if (next == NULL || def->binding != next->binding)
579
break;
580
def = next;
581
type = get_defaults_type(def);
582
if (type == -1) {
583
log_warnx(U_("%s:%d:%d: unknown defaults entry \"%s\""),
584
def->file, def->line, def->column, def->var);
585
/* XXX - just pass it through as a string anyway? */
586
break;
587
}
588
}
589
if (!sudo_json_close_array(jsonc) || !sudo_json_close_object(jsonc))
590
goto oom;
591
}
592
593
/* Close Defaults array; comma (if any) & newline will be printer later. */
594
if (!sudo_json_close_array(jsonc))
595
goto oom;
596
597
debug_return_bool(true);
598
oom:
599
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
600
debug_return_bool(false);
601
}
602
603
/*
604
* Export all aliases of the specified type in JSON format.
605
* Iterates through the entire aliases tree.
606
* Returns true on success, else false, displaying a warning.
607
*/
608
static bool
609
print_aliases_by_type(struct json_container *jsonc,
610
const struct sudoers_parse_tree *parse_tree, short alias_type,
611
const char *title)
612
{
613
struct json_alias_closure closure;
614
debug_decl(print_aliases_by_type, SUDOERS_DEBUG_UTIL);
615
616
/* print_alias_json() does not modify parse_tree. */
617
closure.jsonc = jsonc;
618
closure.count = 0;
619
closure.alias_type = alias_type;
620
closure.title = title;
621
if (!alias_apply((struct sudoers_parse_tree *)parse_tree, print_alias_json,
622
&closure))
623
goto oom;
624
if (closure.count != 0) {
625
if (!sudo_json_close_array(jsonc))
626
goto oom;
627
if (!sudo_json_close_object(jsonc)) {
628
goto oom;
629
}
630
}
631
632
debug_return_bool(true);
633
oom:
634
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
635
debug_return_bool(false);
636
}
637
638
/*
639
* Export all aliases in JSON format.
640
* Returns true on success, false on a memory allocation failure.
641
*/
642
static bool
643
print_aliases_json(struct json_container *jsonc,
644
const struct sudoers_parse_tree *parse_tree)
645
{
646
debug_decl(print_aliases_json, SUDOERS_DEBUG_UTIL);
647
648
if (!print_aliases_by_type(jsonc, parse_tree, USERALIAS, "User_Aliases"))
649
debug_return_bool(false);
650
if (!print_aliases_by_type(jsonc, parse_tree, RUNASALIAS, "Runas_Aliases"))
651
debug_return_bool(false);
652
if (!print_aliases_by_type(jsonc, parse_tree, HOSTALIAS, "Host_Aliases"))
653
debug_return_bool(false);
654
if (!print_aliases_by_type(jsonc, parse_tree, CMNDALIAS, "Command_Aliases"))
655
debug_return_bool(false);
656
657
debug_return_bool(true);
658
}
659
660
/* Does the next entry differ only in the command itself? */
661
static bool
662
cmndspec_continues(struct cmndspec *cs, struct cmndspec *next)
663
{
664
bool ret = next != NULL &&
665
!RUNAS_CHANGED(cs, next) && !TAGS_CHANGED(cs->tags, next->tags)
666
&& cs->privs == next->privs && cs->limitprivs == next->limitprivs
667
&& cs->role == next->role && cs->type == next->type
668
&& cs->apparmor_profile == next->apparmor_profile
669
&& cs->runchroot == next->runchroot && cs->runcwd == next->runcwd;
670
return ret;
671
}
672
673
/*
674
* Print a Cmnd_Spec in JSON format at the correct indent level.
675
* A pointer to the next Cmnd_Spec is passed in to make it possible to
676
* merge adjacent entries that are identical in all but the command.
677
* Returns true on success, false on a memory allocation failure.
678
*/
679
static bool
680
print_cmndspec_json(struct json_container *jsonc,
681
const struct sudoers_parse_tree *parse_tree, struct cmndspec *cs,
682
struct cmndspec **nextp, struct defaults_list *options, bool expand_aliases)
683
{
684
char timebuf[sizeof("20120727121554Z")];
685
struct cmndspec *next = *nextp;
686
bool has_options = false;
687
struct json_value value;
688
struct defaults *def;
689
struct member *m;
690
struct tm gmt;
691
size_t len;
692
debug_decl(print_cmndspec_json, SUDOERS_DEBUG_UTIL);
693
694
/* Open Cmnd_Spec object. */
695
if (!sudo_json_open_object(jsonc, NULL))
696
goto oom;
697
698
/* Print runasuserlist */
699
if (cs->runasuserlist != NULL) {
700
if (!sudo_json_open_array(jsonc, "runasusers"))
701
goto oom;
702
TAILQ_FOREACH(m, cs->runasuserlist, entries) {
703
if (!print_member_json(jsonc, parse_tree, m, TYPE_RUNASUSER,
704
expand_aliases))
705
goto oom;
706
}
707
if (!sudo_json_close_array(jsonc))
708
goto oom;
709
}
710
711
/* Print runasgrouplist */
712
if (cs->runasgrouplist != NULL) {
713
if (!sudo_json_open_array(jsonc, "runasgroups"))
714
goto oom;
715
TAILQ_FOREACH(m, cs->runasgrouplist, entries) {
716
if (!print_member_json(jsonc, parse_tree, m, TYPE_RUNASGROUP,
717
expand_aliases))
718
goto oom;
719
}
720
if (!sudo_json_close_array(jsonc))
721
goto oom;
722
}
723
724
/* Print options and tags */
725
has_options = TAGS_SET(cs->tags) || !TAILQ_EMPTY(options) ||
726
cs->timeout > 0 || cs->notbefore != UNSPEC || cs->notafter != UNSPEC ||
727
cs->runchroot != NULL || cs->runcwd != NULL ||
728
(cs->role != NULL && cs->type != NULL) || cs->apparmor_profile != NULL
729
|| cs->privs != NULL || cs->limitprivs != NULL;
730
if (has_options) {
731
struct cmndtag tag = cs->tags;
732
733
if (!sudo_json_open_array(jsonc, "Options"))
734
goto oom;
735
if (cs->runchroot != NULL) {
736
value.type = JSON_STRING;
737
value.u.string = cs->runchroot;
738
if (!sudo_json_add_value_as_object(jsonc, "runchroot", &value))
739
goto oom;
740
}
741
if (cs->runcwd != NULL) {
742
value.type = JSON_STRING;
743
value.u.string = cs->runcwd;
744
if (!sudo_json_add_value_as_object(jsonc, "runcwd", &value))
745
goto oom;
746
}
747
if (cs->timeout > 0) {
748
value.type = JSON_NUMBER;
749
value.u.number = cs->timeout;
750
if (!sudo_json_add_value_as_object(jsonc, "command_timeout", &value))
751
goto oom;
752
}
753
if (cs->notbefore != UNSPEC) {
754
if (gmtime_r(&cs->notbefore, &gmt) == NULL) {
755
sudo_warn("%s", U_("unable to get GMT time"));
756
} else {
757
timebuf[sizeof(timebuf) - 1] = '\0';
758
len = strftime(timebuf, sizeof(timebuf), "%Y%m%d%H%M%SZ", &gmt);
759
if (len == 0 || timebuf[sizeof(timebuf) - 1] != '\0') {
760
sudo_warnx("%s", U_("unable to format timestamp"));
761
} else {
762
value.type = JSON_STRING;
763
value.u.string = timebuf;
764
if (!sudo_json_add_value_as_object(jsonc, "notbefore", &value))
765
goto oom;
766
}
767
}
768
}
769
if (cs->notafter != UNSPEC) {
770
if (gmtime_r(&cs->notafter, &gmt) == NULL) {
771
sudo_warn("%s", U_("unable to get GMT time"));
772
} else {
773
timebuf[sizeof(timebuf) - 1] = '\0';
774
len = strftime(timebuf, sizeof(timebuf), "%Y%m%d%H%M%SZ", &gmt);
775
if (len == 0 || timebuf[sizeof(timebuf) - 1] != '\0') {
776
sudo_warnx("%s", U_("unable to format timestamp"));
777
} else {
778
value.type = JSON_STRING;
779
value.u.string = timebuf;
780
if (!sudo_json_add_value_as_object(jsonc, "notafter", &value))
781
goto oom;
782
}
783
}
784
}
785
if (tag.nopasswd != UNSPEC) {
786
value.type = JSON_BOOL;
787
value.u.boolean = !tag.nopasswd;
788
if (!sudo_json_add_value_as_object(jsonc, "authenticate", &value))
789
goto oom;
790
}
791
if (tag.noexec != UNSPEC) {
792
value.type = JSON_BOOL;
793
value.u.boolean = tag.noexec;
794
if (!sudo_json_add_value_as_object(jsonc, "noexec", &value))
795
goto oom;
796
}
797
if (tag.intercept != UNSPEC) {
798
value.type = JSON_BOOL;
799
value.u.boolean = tag.intercept;
800
if (!sudo_json_add_value_as_object(jsonc, "intercept", &value))
801
goto oom;
802
}
803
if (tag.send_mail != UNSPEC) {
804
value.type = JSON_BOOL;
805
value.u.boolean = tag.send_mail;
806
if (!sudo_json_add_value_as_object(jsonc, "send_mail", &value))
807
goto oom;
808
}
809
if (tag.setenv != UNSPEC) {
810
value.type = JSON_BOOL;
811
value.u.boolean = tag.setenv;
812
if (!sudo_json_add_value_as_object(jsonc, "setenv", &value))
813
goto oom;
814
}
815
if (tag.follow != UNSPEC) {
816
value.type = JSON_BOOL;
817
value.u.boolean = tag.follow;
818
if (!sudo_json_add_value_as_object(jsonc, "sudoedit_follow", &value))
819
goto oom;
820
}
821
if (tag.log_input != UNSPEC) {
822
value.type = JSON_BOOL;
823
value.u.boolean = tag.log_input;
824
if (!sudo_json_add_value_as_object(jsonc, "log_input", &value))
825
goto oom;
826
}
827
if (tag.log_output != UNSPEC) {
828
value.type = JSON_BOOL;
829
value.u.boolean = tag.log_output;
830
if (!sudo_json_add_value_as_object(jsonc, "log_output", &value))
831
goto oom;
832
}
833
TAILQ_FOREACH(def, options, entries) {
834
int type = get_defaults_type(def);
835
if (type == -1) {
836
log_warnx(U_("%s:%d:%d: unknown defaults entry \"%s\""),
837
def->file, def->line, def->column, def->var);
838
/* XXX - just pass it through as a string anyway? */
839
continue;
840
}
841
if ((type & T_MASK) == T_FLAG || def->val == NULL) {
842
value.type = JSON_BOOL;
843
value.u.boolean = def->op;
844
if (!sudo_json_add_value_as_object(jsonc, def->var, &value))
845
goto oom;
846
} else if ((type & T_MASK) == T_LIST) {
847
if (!print_defaults_list_json(jsonc, def))
848
goto oom;
849
} else {
850
value.type = JSON_STRING;
851
value.u.string = def->val;
852
if (!sudo_json_add_value_as_object(jsonc, def->var, &value))
853
goto oom;
854
}
855
}
856
if (cs->role != NULL && cs->type != NULL) {
857
value.type = JSON_STRING;
858
value.u.string = cs->role;
859
if (!sudo_json_add_value_as_object(jsonc, "role", &value))
860
goto oom;
861
value.u.string = cs->type;
862
if (!sudo_json_add_value_as_object(jsonc, "type", &value))
863
goto oom;
864
}
865
if (cs->apparmor_profile != NULL) {
866
value.type = JSON_STRING;
867
value.u.string = cs->apparmor_profile;
868
if (!sudo_json_add_value_as_object(jsonc, "apparmor_profile", &value))
869
goto oom;
870
}
871
if (cs->privs != NULL) {
872
value.type = JSON_STRING;
873
value.u.string = cs->privs;
874
if (!sudo_json_add_value_as_object(jsonc, "privs", &value))
875
goto oom;
876
}
877
if (cs->limitprivs != NULL) {
878
value.type = JSON_STRING;
879
value.u.string = cs->limitprivs;
880
if (!sudo_json_add_value_as_object(jsonc, "limitprivs", &value))
881
goto oom;
882
}
883
if (!sudo_json_close_array(jsonc))
884
goto oom;
885
}
886
887
/*
888
* Merge adjacent commands with matching tags, runas, SELinux
889
* role/type and Solaris priv settings.
890
*/
891
if (!sudo_json_open_array(jsonc, "Commands"))
892
goto oom;
893
for (;;) {
894
if (!print_member_json(jsonc, parse_tree, cs->cmnd, TYPE_COMMAND,
895
expand_aliases))
896
goto oom;
897
/* Does the next entry differ only in the command itself? */
898
if (!cmndspec_continues(cs, next))
899
break;
900
cs = next;
901
next = TAILQ_NEXT(cs, entries);
902
}
903
if (!sudo_json_close_array(jsonc))
904
goto oom;
905
906
/* Close Cmnd_Spec object. */
907
if (!sudo_json_close_object(jsonc))
908
goto oom;
909
910
*nextp = next;
911
912
debug_return_bool(true);
913
oom:
914
/* warning printed by caller */
915
debug_return_bool(false);
916
}
917
918
/*
919
* Print a User_Spec in JSON format at the correct indent level.
920
* Returns true on success, false on a memory allocation failure.
921
*/
922
static bool
923
print_userspec_json(struct json_container *jsonc,
924
const struct sudoers_parse_tree *parse_tree, struct userspec *us,
925
bool expand_aliases)
926
{
927
struct privilege *priv;
928
struct member *m;
929
struct cmndspec *cs, *next;
930
debug_decl(print_userspec_json, SUDOERS_DEBUG_UTIL);
931
932
/*
933
* Each userspec struct may contain multiple privileges for
934
* a user. We export each privilege as a separate User_Spec
935
* object for simplicity's sake.
936
*/
937
TAILQ_FOREACH(priv, &us->privileges, entries) {
938
/* Open User_Spec object. */
939
if (!sudo_json_open_object(jsonc, NULL))
940
goto oom;
941
942
/* Print users list. */
943
if (!sudo_json_open_array(jsonc, "User_List"))
944
goto oom;
945
TAILQ_FOREACH(m, &us->users, entries) {
946
if (!print_member_json(jsonc, parse_tree, m, TYPE_USERNAME,
947
expand_aliases))
948
goto oom;
949
}
950
if (!sudo_json_close_array(jsonc))
951
goto oom;
952
953
/* Print hosts list. */
954
if (!sudo_json_open_array(jsonc, "Host_List"))
955
goto oom;
956
TAILQ_FOREACH(m, &priv->hostlist, entries) {
957
if (!print_member_json(jsonc, parse_tree, m, TYPE_HOSTNAME,
958
expand_aliases))
959
goto oom;
960
}
961
if (!sudo_json_close_array(jsonc))
962
goto oom;
963
964
/* Print commands. */
965
if (!sudo_json_open_array(jsonc, "Cmnd_Specs"))
966
goto oom;
967
TAILQ_FOREACH_SAFE(cs, &priv->cmndlist, entries, next) {
968
if (!print_cmndspec_json(jsonc, parse_tree, cs, &next,
969
&priv->defaults, expand_aliases))
970
goto oom;
971
}
972
if (!sudo_json_close_array(jsonc))
973
goto oom;
974
975
/* Close User_Spec object. */
976
if (!sudo_json_close_object(jsonc))
977
goto oom;
978
}
979
980
debug_return_bool(true);
981
oom:
982
/* warning printed by caller */
983
debug_return_bool(false);
984
}
985
986
/*
987
* Print an array of User_Spec in JSON format.
988
* Returns true on success, else false, displaying a warning.
989
*/
990
static bool
991
print_userspecs_json(struct json_container *jsonc,
992
const struct sudoers_parse_tree *parse_tree, bool expand_aliases)
993
{
994
struct userspec *us;
995
debug_decl(print_userspecs_json, SUDOERS_DEBUG_UTIL);
996
997
if (TAILQ_EMPTY(&parse_tree->userspecs))
998
debug_return_bool(true);
999
1000
if (!sudo_json_open_array(jsonc, "User_Specs"))
1001
goto oom;
1002
TAILQ_FOREACH(us, &parse_tree->userspecs, entries) {
1003
if (!print_userspec_json(jsonc, parse_tree, us, expand_aliases))
1004
goto oom;
1005
}
1006
if (!sudo_json_close_array(jsonc))
1007
goto oom;
1008
1009
debug_return_bool(true);
1010
oom:
1011
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
1012
debug_return_bool(false);
1013
}
1014
1015
/*
1016
* Export the parsed sudoers file in JSON format.
1017
*/
1018
bool
1019
convert_sudoers_json(const struct sudoers_parse_tree *parse_tree,
1020
const char *output_file, struct cvtsudoers_config *conf)
1021
{
1022
struct json_container jsonc;
1023
bool ret = false;
1024
FILE *output_fp = stdout;
1025
debug_decl(convert_sudoers_json, SUDOERS_DEBUG_UTIL);
1026
1027
if (strcmp(output_file, "-") != 0) {
1028
if ((output_fp = fopen(output_file, "w")) == NULL) {
1029
sudo_warn(U_("unable to open %s"), output_file);
1030
debug_return_bool(false);
1031
}
1032
}
1033
1034
/* 4 space indent, non-compact, exit on memory allocation failure. */
1035
if (!sudo_json_init(&jsonc, 4, false, false, false)) {
1036
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
1037
goto cleanup;
1038
}
1039
1040
/* Dump Defaults in JSON format. */
1041
if (!ISSET(conf->suppress, SUPPRESS_DEFAULTS)) {
1042
if (!print_defaults_json(&jsonc, parse_tree, conf->expand_aliases))
1043
goto cleanup;
1044
}
1045
1046
/* Dump Aliases in JSON format. */
1047
if (!conf->expand_aliases && !ISSET(conf->suppress, SUPPRESS_ALIASES)) {
1048
if (!print_aliases_json(&jsonc, parse_tree))
1049
goto cleanup;
1050
}
1051
1052
/* Dump User_Specs in JSON format. */
1053
if (!ISSET(conf->suppress, SUPPRESS_PRIVS)) {
1054
if (!print_userspecs_json(&jsonc, parse_tree, conf->expand_aliases))
1055
goto cleanup;
1056
}
1057
1058
/* Write JSON output. */
1059
if (sudo_json_get_len(&jsonc) != 0) {
1060
putc('{', output_fp);
1061
fputs(sudo_json_get_buf(&jsonc), output_fp);
1062
fputs("\n}\n", output_fp);
1063
(void)fflush(output_fp);
1064
}
1065
if (!ferror(output_fp))
1066
ret = true;
1067
1068
cleanup:
1069
sudo_json_free(&jsonc);
1070
if (output_fp != stdout)
1071
fclose(output_fp);
1072
1073
debug_return_bool(ret);
1074
}
1075
1076