
This makes it possible to convert from LDAP sudoers to a traditional sudoers file. Semantic differences between file sudoers and LDAP sudoers mean that LDIF -> sudoers is not completely equivalent.
1127 lines
28 KiB
C
1127 lines
28 KiB
C
/*
|
|
* 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.
|
|
*/
|
|
|
|
#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 "parse.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, 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;
|
|
bool need_newline = true;
|
|
id_t id;
|
|
debug_decl(print_member_json, SUDOERS_DEBUG_UTIL)
|
|
|
|
/* Most of the time we print a string. */
|
|
value.type = JSON_STRING;
|
|
value.u.string = name;
|
|
|
|
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(name + 3, NULL, NULL, &errstr);
|
|
if (errstr != NULL) {
|
|
sudo_warnx("internal error: non-Unix group ID %s: \"%s\"",
|
|
errstr, name);
|
|
} else {
|
|
value.type = JSON_ID;
|
|
value.u.id = id;
|
|
typestr = "nonunixgid";
|
|
}
|
|
}
|
|
} else {
|
|
typestr = "usergroup";
|
|
if (*value.u.string == '#') {
|
|
id = sudo_strtoid(name + 2, NULL, NULL, &errstr);
|
|
if (errstr != NULL) {
|
|
sudo_warnx("internal error: group ID %s: \"%s\"",
|
|
errstr, name);
|
|
} 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:
|
|
value.u.string = "ALL";
|
|
/* FALLTHROUGH */
|
|
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(name + 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;
|
|
|
|
if ((a = alias_get(name, alias_type)) != NULL) {
|
|
TAILQ_FOREACH(m, &a->members, entries) {
|
|
print_member_json_int(fp, 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);
|
|
need_newline = false;
|
|
}
|
|
} 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);
|
|
if (need_newline)
|
|
putc('\n', fp);
|
|
|
|
debug_return;
|
|
}
|
|
|
|
static void
|
|
print_member_json(FILE *fp, struct member *m, enum word_type word_type,
|
|
bool last_one, int indent, bool expand_aliases)
|
|
{
|
|
return print_member_json_int(fp, 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.
|
|
*/
|
|
int
|
|
print_alias_json(void *v1, void *v2)
|
|
{
|
|
struct alias *a = v1;
|
|
struct json_alias_closure *closure = v2;
|
|
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, 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 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, 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, 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(&defaults))
|
|
debug_return_bool(need_comma);
|
|
|
|
fprintf(fp, "%s\n%*s\"Defaults\": [\n", need_comma ? "," : "", indent, "");
|
|
indent += 4;
|
|
|
|
TAILQ_FOREACH_SAFE(def, &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, 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, 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(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, int indent, bool need_comma)
|
|
{
|
|
debug_decl(print_aliases_json, SUDOERS_DEBUG_UTIL)
|
|
|
|
need_comma = print_aliases_by_type_json(fp, USERALIAS, "User_Aliases",
|
|
indent, need_comma);
|
|
need_comma = print_aliases_by_type_json(fp, RUNASALIAS, "Runas_Aliases",
|
|
indent, need_comma);
|
|
need_comma = print_aliases_by_type_json(fp, HOSTALIAS, "Host_Aliases",
|
|
indent, need_comma);
|
|
need_comma = print_aliases_by_type_json(fp, 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 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, 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, 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, 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 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, 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, 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, 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, int indent, bool expand_aliases, bool need_comma)
|
|
{
|
|
struct userspec *us;
|
|
debug_decl(print_userspecs_json, SUDOERS_DEBUG_UTIL)
|
|
|
|
if (TAILQ_EMPTY(&userspecs))
|
|
debug_return_bool(need_comma);
|
|
|
|
fprintf(fp, "%s\n%*s\"User_Specs\": [\n", need_comma ? "," : "", indent, "");
|
|
indent += 4;
|
|
TAILQ_FOREACH(us, &userspecs, entries) {
|
|
print_userspec_json(fp, 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(const char *output_file, bool expand_aliases)
|
|
{
|
|
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. */
|
|
need_comma = print_defaults_json(output_fp, indent, expand_aliases, need_comma);
|
|
|
|
/* Dump Aliases in JSON format. */
|
|
if (!expand_aliases)
|
|
need_comma = print_aliases_json(output_fp, indent, need_comma);
|
|
|
|
/* Dump User_Specs in JSON format. */
|
|
print_userspecs_json(output_fp, indent, 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);
|
|
}
|