diff options
Diffstat (limited to '')
-rw-r--r-- | plugins/sudoers/cvtsudoers_json.c | 1161 |
1 files changed, 1161 insertions, 0 deletions
diff --git a/plugins/sudoers/cvtsudoers_json.c b/plugins/sudoers/cvtsudoers_json.c new file mode 100644 index 0000000..5fbef1c --- /dev/null +++ b/plugins/sudoers/cvtsudoers_json.c @@ -0,0 +1,1161 @@ +/* + * Copyright (c) 2013-2018 Todd C. Miller <Todd.Miller@sudo.ws> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/types.h> +#include <stdio.h> +#include <stdlib.h> +#ifdef HAVE_STRING_H +# include <string.h> +#endif /* HAVE_STRING_H */ +#ifdef HAVE_STRINGS_H +# include <strings.h> +#endif /* HAVE_STRINGS_H */ +#include <unistd.h> +#include <stdarg.h> +#include <time.h> +#include <ctype.h> + +#include "sudoers.h" +#include "cvtsudoers.h" +#include <gram.h> + +/* + * JSON values may be of the following types. + */ +enum json_value_type { + JSON_STRING, + JSON_ID, + JSON_NUMBER, + JSON_OBJECT, + JSON_ARRAY, + JSON_BOOL, + JSON_NULL +}; + +/* + * JSON value suitable for printing. + * Note: this does not support object or array values. + */ +struct json_value { + enum json_value_type type; + union { + char *string; + int number; + id_t id; + bool boolean; + } u; +}; + +/* + * Closure used to store state when iterating over all aliases. + */ +struct json_alias_closure { + FILE *fp; + const char *title; + unsigned int count; + int alias_type; + int indent; + bool need_comma; +}; + +/* + * Type values used to disambiguate the generic WORD and ALIAS types. + */ +enum word_type { + TYPE_COMMAND, + TYPE_HOSTNAME, + TYPE_RUNASGROUP, + TYPE_RUNASUSER, + TYPE_USERNAME +}; + +/* + * Print "indent" number of blank characters. + */ +static void +print_indent(FILE *fp, int indent) +{ + while (indent--) + putc(' ', fp); +} + +/* + * Print a JSON string, escaping special characters. + * Does not support unicode escapes. + */ +static void +print_string_json_unquoted(FILE *fp, const char *str) +{ + char ch; + + while ((ch = *str++) != '\0') { + switch (ch) { + case '"': + case '\\': + putc('\\', fp); + break; + case '\b': + ch = 'b'; + putc('\\', fp); + break; + case '\f': + ch = 'f'; + putc('\\', fp); + break; + case '\n': + ch = 'n'; + putc('\\', fp); + break; + case '\r': + ch = 'r'; + putc('\\', fp); + break; + case '\t': + ch = 't'; + putc('\\', fp); + break; + } + putc(ch, fp); + } +} + +/* + * Print a quoted JSON string, escaping special characters. + * Does not support unicode escapes. + */ +static void +print_string_json(FILE *fp, const char *str) +{ + putc('\"', fp); + print_string_json_unquoted(fp, str); + putc('\"', fp); +} + +/* + * Print a JSON name: value pair with proper quoting and escaping. + */ +static void +print_pair_json(FILE *fp, const char *pre, const char *name, + const struct json_value *value, const char *post, int indent) +{ + debug_decl(print_pair_json, SUDOERS_DEBUG_UTIL) + + print_indent(fp, indent); + + /* prefix */ + if (pre != NULL) + fputs(pre, fp); + + /* name */ + print_string_json(fp, name); + putc(':', fp); + putc(' ', fp); + + /* value */ + switch (value->type) { + case JSON_STRING: + print_string_json(fp, value->u.string); + break; + case JSON_ID: + fprintf(fp, "%u", (unsigned int)value->u.id); + break; + case JSON_NUMBER: + fprintf(fp, "%d", value->u.number); + break; + case JSON_NULL: + fputs("null", fp); + break; + case JSON_BOOL: + fputs(value->u.boolean ? "true" : "false", fp); + break; + case JSON_OBJECT: + sudo_fatalx("internal error: can't print JSON_OBJECT"); + break; + case JSON_ARRAY: + sudo_fatalx("internal error: can't print JSON_ARRAY"); + break; + } + + /* postfix */ + if (post != NULL) + fputs(post, fp); + + debug_return; +} + +/* + * Print a JSON string with optional prefix and postfix to fp. + * Strings are not quoted but are escaped as per the JSON spec. + */ +static void +printstr_json(FILE *fp, const char *pre, const char *str, const char *post, + int indent) +{ + debug_decl(printstr_json, SUDOERS_DEBUG_UTIL) + + print_indent(fp, indent); + if (pre != NULL) + fputs(pre, fp); + if (str != NULL) { + print_string_json_unquoted(fp, str); + } + if (post != NULL) + fputs(post, fp); + debug_return; +} + +/* + * Print sudo command member in JSON format, with specified indentation. + * If last_one is false, a comma will be printed before the newline + * that closes the object. + */ +static void +print_command_json(FILE *fp, const char *name, int type, bool negated, int indent, bool last_one) +{ + struct sudo_command *c = (struct sudo_command *)name; + struct json_value value; + const char *digest_name; + debug_decl(print_command_json, SUDOERS_DEBUG_UTIL) + + printstr_json(fp, "{", NULL, NULL, indent); + if (negated || c->digest != NULL) { + putc('\n', fp); + indent += 4; + } else { + putc(' ', fp); + indent = 0; + } + + /* Print command with optional command line args. */ + if (c->args != NULL) { + printstr_json(fp, "\"", "command", "\": ", indent); + printstr_json(fp, "\"", c->cmnd, " ", 0); + printstr_json(fp, NULL, c->args, "\"", 0); + } else { + value.type = JSON_STRING; + value.u.string = c->cmnd; + print_pair_json(fp, NULL, "command", &value, NULL, indent); + } + + /* Optional digest. */ + if (c->digest != NULL) { + fputs(",\n", fp); + digest_name = digest_type_to_name(c->digest->digest_type); + value.type = JSON_STRING; + value.u.string = c->digest->digest_str; + print_pair_json(fp, NULL, digest_name, &value, NULL, indent); + } + + /* Command may be negated. */ + if (negated) { + fputs(",\n", fp); + value.type = JSON_BOOL; + value.u.boolean = true; + print_pair_json(fp, NULL, "negated", &value, NULL, indent); + } + + if (indent != 0) { + indent -= 4; + putc('\n', fp); + print_indent(fp, indent); + } else { + putc(' ', fp); + } + putc('}', fp); + if (!last_one) + putc(',', fp); + putc('\n', fp); + + debug_return; +} + +/* + * Map an alias type to enum word_type. + */ +static enum word_type +alias_to_word_type(int alias_type) +{ + switch (alias_type) { + case CMNDALIAS: + return TYPE_COMMAND; + case HOSTALIAS: + return TYPE_HOSTNAME; + case RUNASALIAS: + return TYPE_RUNASUSER; + case USERALIAS: + return TYPE_USERNAME; + default: + sudo_fatalx_nodebug("unexpected alias type %d", alias_type); + } +} + +/* + * Map a Defaults type to enum word_type. + */ +static enum word_type +defaults_to_word_type(int defaults_type) +{ + switch (defaults_type) { + case DEFAULTS_CMND: + return TYPE_COMMAND; + case DEFAULTS_HOST: + return TYPE_HOSTNAME; + case DEFAULTS_RUNAS: + return TYPE_RUNASUSER; + case DEFAULTS_USER: + return TYPE_USERNAME; + default: + sudo_fatalx_nodebug("unexpected defaults type %d", defaults_type); + } +} + +/* + * Print struct member in JSON format, with specified indentation. + * If last_one is false, a comma will be printed before the newline + * that closes the object. + */ +static void +print_member_json_int(FILE *fp, struct sudoers_parse_tree *parse_tree, + char *name, int type, bool negated, enum word_type word_type, + bool last_one, int indent, bool expand_aliases) +{ + struct json_value value; + const char *typestr = NULL; + const char *errstr; + int alias_type = UNSPEC; + id_t id; + debug_decl(print_member_json_int, SUDOERS_DEBUG_UTIL) + + /* Most of the time we print a string. */ + value.type = JSON_STRING; + if (name != NULL) { + value.u.string = name; + } else { + switch (type) { + case ALL: + value.u.string = "ALL"; + break; + case MYSELF: + value.u.string = ""; + break; + default: + sudo_fatalx("missing member name for type %d", type); + } + } + + switch (type) { + case USERGROUP: + value.u.string++; /* skip leading '%' */ + if (*value.u.string == ':') { + value.u.string++; + typestr = "nonunixgroup"; + if (*value.u.string == '#') { + id = sudo_strtoid(value.u.string + 1, NULL, NULL, &errstr); + if (errstr != NULL) { + sudo_warnx("internal error: non-Unix group ID %s: \"%s\"", + errstr, value.u.string + 1); + } else { + value.type = JSON_ID; + value.u.id = id; + typestr = "nonunixgid"; + } + } + } else { + typestr = "usergroup"; + if (*value.u.string == '#') { + id = sudo_strtoid(value.u.string + 1, NULL, NULL, &errstr); + if (errstr != NULL) { + sudo_warnx("internal error: group ID %s: \"%s\"", + errstr, value.u.string + 1); + } else { + value.type = JSON_ID; + value.u.id = id; + typestr = "usergid"; + } + } + } + break; + case NETGROUP: + typestr = "netgroup"; + value.u.string++; /* skip leading '+' */ + break; + case NTWKADDR: + typestr = "networkaddr"; + break; + case COMMAND: + print_command_json(fp, name, type, negated, indent, last_one); + debug_return; + case ALL: + case MYSELF: + case WORD: + switch (word_type) { + case TYPE_COMMAND: + typestr = "command"; + break; + case TYPE_HOSTNAME: + typestr = "hostname"; + break; + case TYPE_RUNASGROUP: + typestr = "usergroup"; + break; + case TYPE_RUNASUSER: + case TYPE_USERNAME: + typestr = "username"; + if (*value.u.string == '#') { + id = sudo_strtoid(value.u.string + 1, NULL, NULL, &errstr); + if (errstr != NULL) { + sudo_warnx("internal error: user ID %s: \"%s\"", + errstr, name); + } else { + value.type = JSON_ID; + value.u.id = id; + typestr = "userid"; + } + } + break; + default: + sudo_fatalx("unexpected word type %d", word_type); + } + break; + case ALIAS: + switch (word_type) { + case TYPE_COMMAND: + if (expand_aliases) { + alias_type = CMNDALIAS; + } else { + typestr = "cmndalias"; + } + break; + case TYPE_HOSTNAME: + if (expand_aliases) { + alias_type = HOSTALIAS; + } else { + typestr = "hostalias"; + } + break; + case TYPE_RUNASGROUP: + case TYPE_RUNASUSER: + if (expand_aliases) { + alias_type = RUNASALIAS; + } else { + typestr = "runasalias"; + } + break; + case TYPE_USERNAME: + if (expand_aliases) { + alias_type = USERALIAS; + } else { + typestr = "useralias"; + } + break; + default: + sudo_fatalx("unexpected word type %d", word_type); + } + break; + default: + sudo_fatalx("unexpected member type %d", type); + } + + if (expand_aliases && type == ALIAS) { + struct alias *a; + struct member *m; + + /* Print each member of the alias. */ + if ((a = alias_get(parse_tree, value.u.string, alias_type)) != NULL) { + TAILQ_FOREACH(m, &a->members, entries) { + print_member_json_int(fp, parse_tree, m->name, m->type, + negated ? !m->negated : m->negated, + alias_to_word_type(alias_type), + last_one && TAILQ_NEXT(m, entries) == NULL, indent, true); + } + alias_put(a); + } + } else { + if (negated) { + print_indent(fp, indent); + fputs("{\n", fp); + indent += 4; + print_pair_json(fp, NULL, typestr, &value, ",\n", indent); + value.type = JSON_BOOL; + value.u.boolean = true; + print_pair_json(fp, NULL, "negated", &value, "\n", indent); + indent -= 4; + print_indent(fp, indent); + putc('}', fp); + } else { + print_pair_json(fp, "{ ", typestr, &value, " }", indent); + } + + if (!last_one) + putc(',', fp); + putc('\n', fp); + } + + debug_return; +} + +static void +print_member_json(FILE *fp, struct sudoers_parse_tree *parse_tree, + struct member *m, enum word_type word_type, bool last_one, + int indent, bool expand_aliases) +{ + print_member_json_int(fp, parse_tree, m->name, m->type, m->negated, + word_type, last_one, indent, expand_aliases); +} + +/* + * Callback for alias_apply() to print an alias entry if it matches + * the type specified in the closure. + */ +static int +print_alias_json(struct sudoers_parse_tree *parse_tree, struct alias *a, void *v) +{ + struct json_alias_closure *closure = v; + struct member *m; + debug_decl(print_alias_json, SUDOERS_DEBUG_UTIL) + + if (a->type != closure->alias_type) + debug_return_int(0); + + /* Open the aliases object or close the last entry, then open new one. */ + if (closure->count++ == 0) { + fprintf(closure->fp, "%s\n%*s\"%s\": {\n", + closure->need_comma ? "," : "", closure->indent, "", + closure->title); + closure->indent += 4; + } else { + fprintf(closure->fp, "%*s],\n", closure->indent, ""); + } + printstr_json(closure->fp, "\"", a->name, "\": [\n", closure->indent); + + closure->indent += 4; + TAILQ_FOREACH(m, &a->members, entries) { + print_member_json(closure->fp, parse_tree, m, + alias_to_word_type(closure->alias_type), + TAILQ_NEXT(m, entries) == NULL, closure->indent, false); + } + closure->indent -= 4; + debug_return_int(0); +} + +/* + * Print the binding for a Defaults entry of the specified type. + */ +static void +print_binding_json(FILE *fp, struct sudoers_parse_tree *parse_tree, + struct member_list *binding, int type, int indent, bool expand_aliases) +{ + struct member *m; + debug_decl(print_binding_json, SUDOERS_DEBUG_UTIL) + + if (TAILQ_EMPTY(binding)) + debug_return; + + fprintf(fp, "%*s\"Binding\": [\n", indent, ""); + indent += 4; + + /* Print each member object in binding. */ + TAILQ_FOREACH(m, binding, entries) { + print_member_json(fp, parse_tree, m, defaults_to_word_type(type), + TAILQ_NEXT(m, entries) == NULL, indent, expand_aliases); + } + + indent -= 4; + fprintf(fp, "%*s],\n", indent, ""); + + debug_return; +} + +/* + * Print a Defaults list JSON format. + */ +static void +print_defaults_list_json(FILE *fp, struct defaults *def, int indent) +{ + char savech, *start, *end = def->val; + struct json_value value; + debug_decl(print_defaults_list_json, SUDOERS_DEBUG_UTIL) + + fprintf(fp, "%*s{\n", indent, ""); + indent += 4; + value.type = JSON_STRING; + switch (def->op) { + case '+': + value.u.string = "list_add"; + break; + case '-': + value.u.string = "list_remove"; + break; + case true: + value.u.string = "list_assign"; + break; + default: + sudo_warnx("internal error: unexpected list op %d", def->op); + value.u.string = "unsupported"; + break; + } + print_pair_json(fp, NULL, "operation", &value, ",\n", indent); + printstr_json(fp, "\"", def->var, "\": [\n", indent); + indent += 4; + print_indent(fp, indent); + /* Split value into multiple space-separated words. */ + do { + /* Remove leading blanks, must have a non-empty string. */ + for (start = end; isblank((unsigned char)*start); start++) + continue; + if (*start == '\0') + break; + + /* Find the end and print it. */ + for (end = start; *end && !isblank((unsigned char)*end); end++) + continue; + savech = *end; + *end = '\0'; + print_string_json(fp, start); + if (savech != '\0') + putc(',', fp); + *end = savech; + } while (*end++ != '\0'); + putc('\n', fp); + indent -= 4; + fprintf(fp, "%*s]\n", indent, ""); + indent -= 4; + fprintf(fp, "%*s}", indent, ""); + + debug_return; +} + +static int +get_defaults_type(struct defaults *def) +{ + struct sudo_defs_types *cur; + + /* Look up def in table to find its type. */ + for (cur = sudo_defs_table; cur->name; cur++) { + if (strcmp(def->var, cur->name) == 0) + return cur->type; + } + return -1; +} + +/* + * Export all Defaults in JSON format. + */ +static bool +print_defaults_json(FILE *fp, struct sudoers_parse_tree *parse_tree, + int indent, bool expand_aliases, bool need_comma) +{ + struct json_value value; + struct defaults *def, *next; + int type; + debug_decl(print_defaults_json, SUDOERS_DEBUG_UTIL) + + if (TAILQ_EMPTY(&parse_tree->defaults)) + debug_return_bool(need_comma); + + fprintf(fp, "%s\n%*s\"Defaults\": [\n", need_comma ? "," : "", indent, ""); + indent += 4; + + TAILQ_FOREACH_SAFE(def, &parse_tree->defaults, entries, next) { + type = get_defaults_type(def); + if (type == -1) { + sudo_warnx(U_("unknown defaults entry \"%s\""), def->var); + /* XXX - just pass it through as a string anyway? */ + continue; + } + + /* Found it, print object container and binding (if any). */ + fprintf(fp, "%*s{\n", indent, ""); + indent += 4; + print_binding_json(fp, parse_tree, def->binding, def->type, + indent, expand_aliases); + + /* Validation checks. */ + /* XXX - validate values in addition to names? */ + + /* Print options, merging ones with the same binding. */ + fprintf(fp, "%*s\"Options\": [\n", indent, ""); + indent += 4; + for (;;) { + next = TAILQ_NEXT(def, entries); + /* XXX - need to update cur too */ + if ((type & T_MASK) == T_FLAG || def->val == NULL) { + value.type = JSON_BOOL; + value.u.boolean = def->op; + print_pair_json(fp, "{ ", def->var, &value, " }", indent); + } else if ((type & T_MASK) == T_LIST) { + print_defaults_list_json(fp, def, indent); + } else { + value.type = JSON_STRING; + value.u.string = def->val; + print_pair_json(fp, "{ ", def->var, &value, " }", indent); + } + if (next == NULL || def->binding != next->binding) + break; + def = next; + type = get_defaults_type(def); + if (type == -1) { + sudo_warnx(U_("unknown defaults entry \"%s\""), def->var); + /* XXX - just pass it through as a string anyway? */ + break; + } + fputs(",\n", fp); + } + putc('\n', fp); + indent -= 4; + print_indent(fp, indent); + fputs("]\n", fp); + indent -= 4; + print_indent(fp, indent); + fprintf(fp, "}%s\n", next != NULL ? "," : ""); + } + + /* Close Defaults array; comma (if any) & newline will be printer later. */ + indent -= 4; + print_indent(fp, indent); + fputs("]", fp); + + debug_return_bool(true); +} + +/* + * Export all aliases of the specified type in JSON format. + * Iterates through the entire aliases tree. + */ +static bool +print_aliases_by_type_json(FILE *fp, struct sudoers_parse_tree *parse_tree, + int alias_type, const char *title, int indent, bool need_comma) +{ + struct json_alias_closure closure; + debug_decl(print_aliases_by_type_json, SUDOERS_DEBUG_UTIL) + + closure.fp = fp; + closure.indent = indent; + closure.count = 0; + closure.alias_type = alias_type; + closure.title = title; + closure.need_comma = need_comma; + alias_apply(parse_tree, print_alias_json, &closure); + if (closure.count != 0) { + print_indent(fp, closure.indent); + fputs("]\n", fp); + closure.indent -= 4; + print_indent(fp, closure.indent); + putc('}', fp); + need_comma = true; + } + + debug_return_bool(need_comma); +} + +/* + * Export all aliases in JSON format. + */ +static bool +print_aliases_json(FILE *fp, struct sudoers_parse_tree *parse_tree, + int indent, bool need_comma) +{ + debug_decl(print_aliases_json, SUDOERS_DEBUG_UTIL) + + need_comma = print_aliases_by_type_json(fp, parse_tree, USERALIAS, + "User_Aliases", indent, need_comma); + need_comma = print_aliases_by_type_json(fp, parse_tree, RUNASALIAS, + "Runas_Aliases", indent, need_comma); + need_comma = print_aliases_by_type_json(fp, parse_tree, HOSTALIAS, + "Host_Aliases", indent, need_comma); + need_comma = print_aliases_by_type_json(fp, parse_tree, CMNDALIAS, + "Command_Aliases", indent, need_comma); + + debug_return_bool(need_comma); +} + +/* + * Print a Cmnd_Spec in JSON format at the specified indent level. + * A pointer to the next Cmnd_Spec is passed in to make it possible to + * merge adjacent entries that are identical in all but the command. + */ +static void +print_cmndspec_json(FILE *fp, struct sudoers_parse_tree *parse_tree, + struct cmndspec *cs, struct cmndspec **nextp, + struct defaults_list *options, bool expand_aliases, int indent) +{ + struct cmndspec *next = *nextp; + struct json_value value; + struct defaults *def; + struct member *m; + struct tm *tp; + bool last_one; + char timebuf[sizeof("20120727121554Z")]; + debug_decl(print_cmndspec_json, SUDOERS_DEBUG_UTIL) + + /* Open Cmnd_Spec object. */ + fprintf(fp, "%*s{\n", indent, ""); + indent += 4; + + /* Print runasuserlist */ + if (cs->runasuserlist != NULL) { + fprintf(fp, "%*s\"runasusers\": [\n", indent, ""); + indent += 4; + TAILQ_FOREACH(m, cs->runasuserlist, entries) { + print_member_json(fp, parse_tree, m, TYPE_RUNASUSER, + TAILQ_NEXT(m, entries) == NULL, indent, expand_aliases); + } + indent -= 4; + fprintf(fp, "%*s],\n", indent, ""); + } + + /* Print runasgrouplist */ + if (cs->runasgrouplist != NULL) { + fprintf(fp, "%*s\"runasgroups\": [\n", indent, ""); + indent += 4; + TAILQ_FOREACH(m, cs->runasgrouplist, entries) { + print_member_json(fp, parse_tree, m, TYPE_RUNASGROUP, + TAILQ_NEXT(m, entries) == NULL, indent, expand_aliases); + } + indent -= 4; + fprintf(fp, "%*s],\n", indent, ""); + } + + /* Print options and tags */ + if (cs->timeout > 0 || cs->notbefore != UNSPEC || cs->notafter != UNSPEC || + TAGS_SET(cs->tags) || !TAILQ_EMPTY(options)) { + struct cmndtag tag = cs->tags; + const char *prefix = "\n"; + + fprintf(fp, "%*s\"Options\": [", indent, ""); + indent += 4; + if (cs->timeout > 0) { + value.type = JSON_NUMBER; + value.u.number = cs->timeout; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "command_timeout", &value, " }", indent); + prefix = ",\n"; + } + if (cs->notbefore != UNSPEC) { + if ((tp = gmtime(&cs->notbefore)) == NULL) { + sudo_warn(U_("unable to get GMT time")); + } else { + if (strftime(timebuf, sizeof(timebuf), "%Y%m%d%H%M%SZ", tp) == 0) { + sudo_warnx(U_("unable to format timestamp")); + } else { + value.type = JSON_STRING; + value.u.string = timebuf; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "notbefore", &value, " }", indent); + prefix = ",\n"; + } + } + } + if (cs->notafter != UNSPEC) { + if ((tp = gmtime(&cs->notafter)) == NULL) { + sudo_warn(U_("unable to get GMT time")); + } else { + if (strftime(timebuf, sizeof(timebuf), "%Y%m%d%H%M%SZ", tp) == 0) { + sudo_warnx(U_("unable to format timestamp")); + } else { + value.type = JSON_STRING; + value.u.string = timebuf; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "notafter", &value, " }", indent); + prefix = ",\n"; + } + } + } + if (tag.nopasswd != UNSPEC) { + value.type = JSON_BOOL; + value.u.boolean = !tag.nopasswd; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "authenticate", &value, " }", indent); + prefix = ",\n"; + } + if (tag.noexec != UNSPEC) { + value.type = JSON_BOOL; + value.u.boolean = tag.noexec; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "noexec", &value, " }", indent); + prefix = ",\n"; + } + if (tag.send_mail != UNSPEC) { + value.type = JSON_BOOL; + value.u.boolean = tag.send_mail; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "send_mail", &value, " }", indent); + prefix = ",\n"; + } + if (tag.setenv != UNSPEC) { + value.type = JSON_BOOL; + value.u.boolean = tag.setenv; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "setenv", &value, " }", indent); + prefix = ",\n"; + } + if (tag.follow != UNSPEC) { + value.type = JSON_BOOL; + value.u.boolean = tag.follow; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "sudoedit_follow", &value, " }", indent); + prefix = ",\n"; + } + if (tag.log_input != UNSPEC) { + value.type = JSON_BOOL; + value.u.boolean = tag.log_input; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "log_input", &value, " }", indent); + prefix = ",\n"; + } + if (tag.log_output != UNSPEC) { + value.type = JSON_BOOL; + value.u.boolean = tag.log_output; + fputs(prefix, fp); + print_pair_json(fp, "{ ", "log_output", &value, " }", indent); + prefix = ",\n"; + } + TAILQ_FOREACH(def, options, entries) { + int type = get_defaults_type(def); + if (type == -1) { + sudo_warnx(U_("unknown defaults entry \"%s\""), def->var); + /* XXX - just pass it through as a string anyway? */ + continue; + } + fputs(prefix, fp); + if ((type & T_MASK) == T_FLAG || def->val == NULL) { + value.type = JSON_BOOL; + value.u.boolean = def->op; + print_pair_json(fp, "{ ", def->var, &value, " }", indent); + } else if ((type & T_MASK) == T_LIST) { + print_defaults_list_json(fp, def, indent); + } else { + value.type = JSON_STRING; + value.u.string = def->val; + print_pair_json(fp, "{ ", def->var, &value, " }", indent); + } + prefix = ",\n"; + } + putc('\n', fp); + indent -= 4; + fprintf(fp, "%*s],\n", indent, ""); + } + +#ifdef HAVE_SELINUX + /* Print SELinux role/type */ + if (cs->role != NULL && cs->type != NULL) { + fprintf(fp, "%*s\"SELinux_Spec\": [\n", indent, ""); + indent += 4; + value.type = JSON_STRING; + value.u.string = cs->role; + print_pair_json(fp, NULL, "role", &value, ",\n", indent); + value.u.string = cs->type; + print_pair_json(fp, NULL, "type", &value, "\n", indent); + indent -= 4; + fprintf(fp, "%*s],\n", indent, ""); + } +#endif /* HAVE_SELINUX */ + +#ifdef HAVE_PRIV_SET + /* Print Solaris privs/limitprivs */ + if (cs->privs != NULL || cs->limitprivs != NULL) { + fprintf(fp, "%*s\"Solaris_Priv_Spec\": [\n", indent, ""); + indent += 4; + value.type = JSON_STRING; + if (cs->privs != NULL) { + value.u.string = cs->privs; + print_pair_json(fp, NULL, "privs", &value, + cs->limitprivs != NULL ? ",\n" : "\n", indent); + } + if (cs->limitprivs != NULL) { + value.u.string = cs->limitprivs; + print_pair_json(fp, NULL, "limitprivs", &value, "\n", indent); + } + indent -= 4; + fprintf(fp, "%*s],\n", indent, ""); + } +#endif /* HAVE_PRIV_SET */ + + /* + * Merge adjacent commands with matching tags, runas, SELinux + * role/type and Solaris priv settings. + */ + fprintf(fp, "%*s\"Commands\": [\n", indent, ""); + indent += 4; + for (;;) { + /* Does the next entry differ only in the command itself? */ + /* XXX - move into a function that returns bool */ + last_one = next == NULL || + RUNAS_CHANGED(cs, next) || TAGS_CHANGED(cs->tags, next->tags) +#ifdef HAVE_PRIV_SET + || cs->privs != next->privs || cs->limitprivs != next->limitprivs +#endif /* HAVE_PRIV_SET */ +#ifdef HAVE_SELINUX + || cs->role != next->role || cs->type != next->type +#endif /* HAVE_SELINUX */ + ; + + print_member_json(fp, parse_tree, cs->cmnd, TYPE_COMMAND, + last_one, indent, expand_aliases); + if (last_one) + break; + cs = next; + next = TAILQ_NEXT(cs, entries); + } + indent -= 4; + fprintf(fp, "%*s]\n", indent, ""); + + /* Close Cmnd_Spec object. */ + indent -= 4; + fprintf(fp, "%*s}%s\n", indent, "", TAILQ_NEXT(cs, entries) != NULL ? "," : ""); + + *nextp = next; + + debug_return; +} + +/* + * Print a User_Spec in JSON format at the specified indent level. + */ +static void +print_userspec_json(FILE *fp, struct sudoers_parse_tree *parse_tree, + struct userspec *us, int indent, bool expand_aliases) +{ + struct privilege *priv; + struct member *m; + struct cmndspec *cs, *next; + debug_decl(print_userspec_json, SUDOERS_DEBUG_UTIL) + + /* + * Each userspec struct may contain multiple privileges for + * a user. We export each privilege as a separate User_Spec + * object for simplicity's sake. + */ + TAILQ_FOREACH(priv, &us->privileges, entries) { + /* Open User_Spec object. */ + fprintf(fp, "%*s{\n", indent, ""); + indent += 4; + + /* Print users list. */ + fprintf(fp, "%*s\"User_List\": [\n", indent, ""); + indent += 4; + TAILQ_FOREACH(m, &us->users, entries) { + print_member_json(fp, parse_tree, m, TYPE_USERNAME, + TAILQ_NEXT(m, entries) == NULL, indent, expand_aliases); + } + indent -= 4; + fprintf(fp, "%*s],\n", indent, ""); + + /* Print hosts list. */ + fprintf(fp, "%*s\"Host_List\": [\n", indent, ""); + indent += 4; + TAILQ_FOREACH(m, &priv->hostlist, entries) { + print_member_json(fp, parse_tree, m, TYPE_HOSTNAME, + TAILQ_NEXT(m, entries) == NULL, indent, expand_aliases); + } + indent -= 4; + fprintf(fp, "%*s],\n", indent, ""); + + /* Print commands. */ + fprintf(fp, "%*s\"Cmnd_Specs\": [\n", indent, ""); + indent += 4; + TAILQ_FOREACH_SAFE(cs, &priv->cmndlist, entries, next) { + print_cmndspec_json(fp, parse_tree, cs, &next, &priv->defaults, + expand_aliases, indent); + } + indent -= 4; + fprintf(fp, "%*s]\n", indent, ""); + + /* Close User_Spec object. */ + indent -= 4; + fprintf(fp, "%*s}%s\n", indent, "", TAILQ_NEXT(priv, entries) != NULL || + TAILQ_NEXT(us, entries) != NULL ? "," : ""); + } + + debug_return; +} + +static bool +print_userspecs_json(FILE *fp, struct sudoers_parse_tree *parse_tree, + int indent, bool expand_aliases, bool need_comma) +{ + struct userspec *us; + debug_decl(print_userspecs_json, SUDOERS_DEBUG_UTIL) + + if (TAILQ_EMPTY(&parse_tree->userspecs)) + debug_return_bool(need_comma); + + fprintf(fp, "%s\n%*s\"User_Specs\": [\n", need_comma ? "," : "", indent, ""); + indent += 4; + TAILQ_FOREACH(us, &parse_tree->userspecs, entries) { + print_userspec_json(fp, parse_tree, us, indent, expand_aliases); + } + indent -= 4; + fprintf(fp, "%*s]", indent, ""); + + debug_return_bool(true); +} + +/* + * Export the parsed sudoers file in JSON format. + */ +bool +convert_sudoers_json(struct sudoers_parse_tree *parse_tree, + const char *output_file, struct cvtsudoers_config *conf) +{ + bool ret = true, need_comma = false; + const int indent = 4; + FILE *output_fp = stdout; + debug_decl(convert_sudoers_json, SUDOERS_DEBUG_UTIL) + + if (strcmp(output_file, "-") != 0) { + if ((output_fp = fopen(output_file, "w")) == NULL) + sudo_fatal(U_("unable to open %s"), output_file); + } + + /* Open JSON output. */ + putc('{', output_fp); + + /* Dump Defaults in JSON format. */ + if (!ISSET(conf->suppress, SUPPRESS_DEFAULTS)) { + need_comma = print_defaults_json(output_fp, parse_tree, indent, + conf->expand_aliases, need_comma); + } + + /* Dump Aliases in JSON format. */ + if (!conf->expand_aliases && !ISSET(conf->suppress, SUPPRESS_ALIASES)) { + need_comma = print_aliases_json(output_fp, parse_tree, indent, + need_comma); + } + + /* Dump User_Specs in JSON format. */ + if (!ISSET(conf->suppress, SUPPRESS_PRIVS)) { + print_userspecs_json(output_fp, parse_tree, indent, + conf->expand_aliases, need_comma); + } + + /* Close JSON output. */ + fputs("\n}\n", output_fp); + (void)fflush(output_fp); + if (ferror(output_fp)) + ret = false; + if (output_fp != stdout) + fclose(output_fp); + + debug_return_bool(ret); +} |