From: Todd C. Miller Date: Sun, 28 Jan 2018 03:08:02 +0000 (-0700) Subject: Add ldif backend to cvtsudoers, to replace sudoers2ldif X-Git-Tag: SUDO_1_8_23^2~177 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=681fb2e76e8d060aaac60c087d1966cf5405d505;p=sudo Add ldif backend to cvtsudoers, to replace sudoers2ldif --- diff --git a/MANIFEST b/MANIFEST index 12c77fe8c..022aaa247 100644 --- a/MANIFEST +++ b/MANIFEST @@ -266,6 +266,7 @@ plugins/sudoers/check.c plugins/sudoers/check.h plugins/sudoers/cvtsudoers.c plugins/sudoers/cvtsudoers_json.c +plugins/sudoers/cvtsudoers_ldif.c plugins/sudoers/def_data.c plugins/sudoers/def_data.h plugins/sudoers/def_data.in diff --git a/doc/cvtsudoers.cat b/doc/cvtsudoers.cat index 84364ff78..526f56a18 100644 --- a/doc/cvtsudoers.cat +++ b/doc/cvtsudoers.cat @@ -17,12 +17,18 @@ DDEESSCCRRIIPPTTIIOONN The options are as follows: --ff, ----ffoorrmmaatt - Specify the output format. Currently, JSON is the only - supported output format. The JSON format is intended to be - easier for third-party applications to parse than the - traditional _s_u_d_o_e_r_s format. The various values have explicit - types which removes much of the ambiguity of the _s_u_d_o_e_r_s - format. + Specify the output format. The following formats are + supported: + + JSON JSON (JavaScript Object Notation) files are usually + easier for third-party applications to consume than + the traditional _s_u_d_o_e_r_s format. The various values + have explicit types which removes much of the + ambiguity of the _s_u_d_o_e_r_s format. + + LDIF LDIF (LDAP Data Interchange Format) files can be + imported into an LDAP server for use with + sudoers.ldap(4). --hh, ----hheellpp Display a short help message to the standard output and exit. @@ -35,7 +41,7 @@ DDEESSCCRRIIPPTTIIOONN Print the ccvvttssuuddooeerrss and _s_u_d_o_e_r_s grammar versions and exit. SSEEEE AALLSSOO - sudoers(4), sudo(1m) + sudoers(4), sudoers.ldap(4), sudo(1m) AAUUTTHHOORRSS Many people have worked on ssuuddoo over the years; this version consists of @@ -63,4 +69,4 @@ DDIISSCCLLAAIIMMEERR file distributed with ssuuddoo or https://www.sudo.ws/license.html for complete details. -Sudo 1.8.22 January 25, 2018 Sudo 1.8.22 +Sudo 1.8.22 January 27, 2018 Sudo 1.8.22 diff --git a/doc/cvtsudoers.man.in b/doc/cvtsudoers.man.in index 43723e3e7..cfcdafbe6 100644 --- a/doc/cvtsudoers.man.in +++ b/doc/cvtsudoers.man.in @@ -16,7 +16,7 @@ .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" -.TH "CVTSUDOERS" "8" "January 25, 2018" "Sudo @PACKAGE_VERSION@" "System Manager's Manual" +.TH "CVTSUDOERS" "8" "January 27, 2018" "Sudo @PACKAGE_VERSION@" "System Manager's Manual" .nh .if n .ad l .SH "NAME" @@ -52,15 +52,30 @@ The options are as follows: .TP 12n \fB\-f\fR, \fB\--format\fR Specify the output format. -Currently, JSON is the only supported output format. -The JSON format is intended to be easier for third-party -applications to parse than the traditional +The following formats are supported: +.PP +.RS 12n +.PD 0 +.TP 10n +JSON +JSON (JavaScript Object Notation) files are usually easier for +third-party applications to consume than the traditional \fIsudoers\fR format. The various values have explicit types which removes much of the ambiguity of the \fIsudoers\fR format. +.PD +.TP 10n +LDIF +LDIF (LDAP Data Interchange Format) files can be imported into an LDAP +server for use with +sudoers.ldap(@mansectform@). +.PD 0 +.PP +.RE +.PD .TP 12n \fB\-h\fR, \fB\--help\fR Display a short help message to the standard output and exit. @@ -84,6 +99,7 @@ and grammar versions and exit. .SH "SEE ALSO" sudoers(@mansectform@), +sudoers.ldap(@mansectform@), sudo(@mansectsu@) .SH "AUTHORS" Many people have worked on diff --git a/doc/cvtsudoers.mdoc.in b/doc/cvtsudoers.mdoc.in index 93e86bba2..96c2e9c2b 100644 --- a/doc/cvtsudoers.mdoc.in +++ b/doc/cvtsudoers.mdoc.in @@ -14,7 +14,7 @@ .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. .\" -.Dd January 25, 2018 +.Dd January 27, 2018 .Dt CVTSUDOERS @mansectsu@ .Os Sudo @PACKAGE_VERSION@ .Sh NAME @@ -49,15 +49,22 @@ The options are as follows: .Bl -tag -width Fl .It Fl f , -format Specify the output format. -Currently, JSON is the only supported output format. -The JSON format is intended to be easier for third-party -applications to parse than the traditional +The following formats are supported: +.Bl -tag -width 8n +.It JSON +JSON (JavaScript Object Notation) files are usually easier for +third-party applications to consume than the traditional .Em sudoers format. The various values have explicit types which removes much of the ambiguity of the .Em sudoers format. +.It LDIF +LDIF (LDAP Data Interchange Format) files can be imported into an LDAP +server for use with +.Xr sudoers.ldap @mansectform@ . +.El .It Fl h , -help Display a short help message to the standard output and exit. .It Fl o Ar output_file , Fl -output Ns = Ns Ar output_file @@ -80,6 +87,7 @@ grammar versions and exit. .El .Sh SEE ALSO .Xr sudoers @mansectform@ , +.Xr sudoers.ldap @mansectform@ , .Xr sudo @mansectsu@ .Sh AUTHORS Many people have worked on diff --git a/plugins/sudoers/Makefile.in b/plugins/sudoers/Makefile.in index 50a5d0d9e..9c9dc37a4 100644 --- a/plugins/sudoers/Makefile.in +++ b/plugins/sudoers/Makefile.in @@ -161,7 +161,8 @@ SUDOERS_OBJS = $(AUTH_OBJS) boottime.lo check.lo editor.lo env.lo \ VISUDO_OBJS = editor.o find_path.o goodpath.o locale.o stubs.o sudo_printf.o visudo.o -CVTSUDOERS_OBJS = cvtsudoers.o cvtsudoers_json.o locale.o stubs.o sudo_printf.o +CVTSUDOERS_OBJS = cvtsudoers.o cvtsudoers_json.o cvtsudoers_ldif.o \ + locale.o stubs.o sudo_printf.o REPLAY_OBJS = getdate.o sudoreplay.o @@ -705,6 +706,17 @@ cvtsudoers_json.o: $(srcdir)/cvtsudoers_json.c $(devdir)/def_data.h \ $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \ $(top_builddir)/pathnames.h $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/cvtsudoers_json.c +cvtsudoers_ldif.o: $(srcdir)/cvtsudoers_ldif.c $(devdir)/def_data.h \ + $(devdir)/gram.h $(incdir)/compat/stdbool.h \ + $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h \ + $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ + $(incdir)/sudo_gettext.h $(incdir)/sudo_plugin.h \ + $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ + $(srcdir)/defaults.h $(srcdir)/logging.h $(srcdir)/parse.h \ + $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h \ + $(srcdir)/sudoers_debug.h $(top_builddir)/config.h \ + $(top_builddir)/pathnames.h + $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/cvtsudoers_ldif.c dce.lo: $(authdir)/dce.c $(devdir)/def_data.h $(incdir)/compat/stdbool.h \ $(incdir)/sudo_compat.h $(incdir)/sudo_conf.h $(incdir)/sudo_debug.h \ $(incdir)/sudo_fatal.h $(incdir)/sudo_gettext.h \ diff --git a/plugins/sudoers/cvtsudoers.c b/plugins/sudoers/cvtsudoers.c index beb81497a..011e3db83 100644 --- a/plugins/sudoers/cvtsudoers.c +++ b/plugins/sudoers/cvtsudoers.c @@ -50,6 +50,7 @@ #endif /* HAVE_GETOPT_LONG */ extern bool convert_sudoers_json(const char *output_file); +extern bool convert_sudoers_ldif(const char *output_file, const char *base); extern void parse_sudoers_options(void); extern void get_hostname(void); @@ -74,13 +75,19 @@ __dso_public int main(int argc, char *argv[]); static void help(void) __attribute__((__noreturn__)); static void usage(int); +enum output_formats { + output_invalid, + output_json, + output_ldif +}; + int main(int argc, char *argv[]) { int ch, exitcode = EXIT_FAILURE; + enum output_formats output_format = output_json; const char *input_file = NULL; const char *output_file = "-"; - const char *output_format = "JSON"; debug_decl(main, SUDOERS_DEBUG_MAIN) #if defined(SUDO_DEVEL) && defined(__OpenBSD__) @@ -119,11 +126,14 @@ main(int argc, char *argv[]) while ((ch = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { switch (ch) { case 'f': - if (strcasecmp(optarg, "json") != 0) { + if (strcasecmp(optarg, "json") == 0) { + output_format = output_json; + } else if (strcasecmp(optarg, "ldif") == 0) { + output_format = output_ldif; + } else { sudo_warnx("unsupported output format %s", optarg); usage(1); } - output_format = optarg; break; case 'h': help(); @@ -203,7 +213,16 @@ main(int argc, char *argv[]) goto done; } - exitcode = convert_sudoers_json(output_file) ? EXIT_SUCCESS : EXIT_FAILURE; + switch (output_format) { + case output_json: + exitcode = !convert_sudoers_json(output_file); + break; + case output_ldif: + exitcode = !convert_sudoers_ldif(output_file, NULL); + break; + default: + sudo_fatalx("error: unhandled output format %d", output_format); + } done: sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, exitcode); diff --git a/plugins/sudoers/cvtsudoers_ldif.c b/plugins/sudoers/cvtsudoers_ldif.c new file mode 100644 index 000000000..7282978cc --- /dev/null +++ b/plugins/sudoers/cvtsudoers_ldif.c @@ -0,0 +1,408 @@ +/* + * Copyright (c) 2018 Todd C. Miller + * + * 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 + +#include +#include +#include +#ifdef HAVE_STRING_H +# include +#endif /* HAVE_STRING_H */ +#ifdef HAVE_STRINGS_H +# include +#endif /* HAVE_STRINGS_H */ +#include +#include +#include + +#include "sudoers.h" +#include "parse.h" +#include "redblack.h" +#include + +struct seen_user { + const char *name; + long count; +}; + +static int sudo_order; +static struct rbtree *seen_users; + +static int +seen_user_compare(const void *aa, const void *bb) +{ + const struct seen_user *a = aa; + const struct seen_user *b = bb; + + return strcasecmp(a->name, b->name); +} + +/* + * Print global Defaults in a single sudoRole object. + */ +static bool +print_global_defaults_ldif(FILE *fp, const char *base) +{ + struct defaults *def; + debug_decl(print_global_defaults_ldif, SUDOERS_DEBUG_UTIL) + + if (TAILQ_EMPTY(&defaults)) + debug_return_bool(true); + + fprintf(fp, "dn: cn=defaults,%s\n", base); + fputs("objectClass: top\n", fp); + fputs("objectClass: sudoRole\n", fp); + fputs("cn: defaults\n", fp); + fputs("description: Default sudoOption's go here\n", fp); + + TAILQ_FOREACH(def, &defaults, entries) { + if (def->type != DEFAULTS) + continue; /* only want global defaults */ + + if (def->val != NULL) { + /* There is no need to double quote values here. */ + fprintf(fp, "sudoOption: %s%s%s\n", def->var, + def->op == '+' ? "+=" : def->op == '-' ? "-=" : "=", def->val); + } else { + /* Boolean flag. */ + fprintf(fp, "sudoOption: %s%s\n", def->op == false ? "!" : "", + def->var); + } + } + putc('\n', fp); + + debug_return_bool(!ferror(fp)); +} + +static void +warn_bound_defaults_ldif(FILE *fp) +{ + struct defaults *def; + debug_decl(warn_bound_defaults_ldif, SUDOERS_DEBUG_UTIL) + + TAILQ_FOREACH(def, &defaults, entries) { + if (def->type == DEFAULTS) + continue; /* only want bound defaults */ + + /* XXX - print Defaults line */ + sudo_warnx(U_("%s:%d unable to translate Defaults line"), + def->file, def->lineno); + } + + debug_return; +} + +/* + * Print struct member in LDIF format, with specified prefix. + * See print_member_int() in parse.c. + */ +static void +print_member_ldif(FILE *fp, char *name, int type, int negated, + int alias_type, const char *prefix) +{ + struct alias *a; + struct member *m; + struct sudo_command *c; + debug_decl(print_member_ldif, SUDOERS_DEBUG_UTIL) + + switch (type) { + case ALL: + fprintf(fp, "%s: %sALL\n", prefix, negated ? "!" : ""); + break; + case MYSELF: + /* Only valid for sudoRunasUser */ + fprintf(fp, "%s:\n", prefix); + break; + case COMMAND: + c = (struct sudo_command *)name; + fprintf(fp, "%s: ", prefix); + if (c->digest != NULL) + fprintf(fp, "%s:", digest_type_to_name(c->digest->digest_type)); + fprintf(fp, "%s%s", negated ? "!" : "", c->cmnd); + if (c->args != NULL) + fprintf(fp, " %s", c->args); + putc('\n', fp); + break; + case ALIAS: + if ((a = alias_get(name, alias_type)) != NULL) { + TAILQ_FOREACH(m, &a->members, entries) { + print_member_ldif(fp, m->name, m->type, + negated ? !m->negated : m->negated, alias_type, prefix); + } + alias_put(a); + break; + } + /* FALLTHROUGH */ + default: + fprintf(fp, "%s: %s%s\n", prefix, negated ? "!" : "", name); + break; + } + + debug_return; +} + +/* + * Print a Cmnd_Spec in LDIF format. + * 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_ldif(FILE *fp, struct cmndspec *cs, struct cmndspec **nextp) +{ + struct cmndspec *next = *nextp; + struct member *m; + bool last_one; + debug_decl(print_cmndspec_ldif, SUDOERS_DEBUG_UTIL) + + /* Print runasuserlist as sudoRunAsUser attributes */ + if (cs->runasuserlist != NULL) { + TAILQ_FOREACH(m, cs->runasuserlist, entries) { + print_member_ldif(fp, m->name, m->type, m->negated, + RUNASALIAS, "sudoRunAsUser"); + } + } + + /* Print runasgrouplist as sudoRunAsGroup attributes */ + if (cs->runasgrouplist != NULL) { + TAILQ_FOREACH(m, cs->runasgrouplist, entries) { + print_member_ldif(fp, m->name, m->type, m->negated, + RUNASALIAS, "sudoRunAsGroup"); + } + } + + /* Print tags as sudoOption attributes */ + if (cs->timeout > 0 || TAGS_SET(cs->tags)) { + struct cmndtag tag = cs->tags; + + if (cs->timeout > 0) { + fprintf(fp, "sudoOption: command_timeout=%d\n", cs->timeout); + } + if (tag.nopasswd != UNSPEC) { + fprintf(fp, "sudoOption: %sauthenticate\n", tag.nopasswd ? "!" : ""); + } + if (tag.noexec != UNSPEC) { + fprintf(fp, "sudoOption: %snoexec\n", tag.noexec ? "" : "!"); + } + if (tag.send_mail != UNSPEC) { + if (tag.send_mail) { + fprintf(fp, "sudoOption: mail_all_cmnds\n"); + } else { + fprintf(fp, "sudoOption: !mail_all_cmnds\n"); + fprintf(fp, "sudoOption: !mail_always\n"); + fprintf(fp, "sudoOption: !mail_no_perms\n"); + } + } + if (tag.setenv != UNSPEC && tag.setenv != IMPLIED) { + fprintf(fp, "sudoOption: %ssetenv\n", tag.setenv ? "" : "!"); + } + if (tag.follow != UNSPEC) { + fprintf(fp, "sudoOption: %ssudoedit_follow\n", tag.follow ? "" : "!"); + } + if (tag.log_input != UNSPEC) { + fprintf(fp, "sudoOption: %slog_input\n", tag.log_input ? "" : "!"); + } + if (tag.log_output != UNSPEC) { + fprintf(fp, "sudoOption: %slog_output\n", tag.log_output ? "" : "!"); + } + } + +#ifdef HAVE_SELINUX + /* Print SELinux role/type */ + if (cs->role != NULL && cs->type != NULL) { + fprintf(fp, "sudoOption: role=%s\n", cs->role); + fprintf(fp, "sudoOption: type=%s\n", cs->type); + } +#endif /* HAVE_SELINUX */ + +#ifdef HAVE_PRIV_SET + /* Print Solaris privs/limitprivs */ + if (cs->privs != NULL || cs->limitprivs != NULL) { + if (cs->privs != NULL) + fprintf(fp, "sudoOption: privs=%s\n", cs->privs); + if (cs->limitprivs != NULL) + fprintf(fp, "sudoOption: limitprivs=%s\n", cs->limitprivs); + } +#endif /* HAVE_PRIV_SET */ + + /* + * Merge adjacent commands with matching tags, runas, SELinux + * role/type and Solaris priv settings. + */ + for (;;) { + /* Does the next entry differ only in the command itself? */ + /* XXX - move into a function that returns bool */ + /* XXX - TAG_SET does not account for implied SETENV */ + 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_ldif(fp, cs->cmnd->name, cs->cmnd->type, cs->cmnd->negated, + CMNDALIAS, "sudoCommand"); + if (last_one) + break; + cs = next; + next = TAILQ_NEXT(cs, entries); + } + + *nextp = next; + + debug_return; +} + +/* + * Print a single User_Spec. + */ +static bool +print_userspec_ldif(FILE *fp, struct userspec *us, const char *base) +{ + struct privilege *priv; + struct member *m; + struct cmndspec *cs, *next; + struct rbnode *node; + debug_decl(print_userspec_ldif, SUDOERS_DEBUG_UTIL) + + /* + * Each userspec struct may contain multiple privileges for + * the user. We export each privilege as a separate sudoRole + * object for simplicity's sake. + */ + TAILQ_FOREACH(priv, &us->privileges, entries) { + TAILQ_FOREACH_SAFE(cs, &priv->cmndlist, entries, next) { + char *cn; + struct seen_user *su, key; + + /* + * Increment the number of times we have seen this user. + * If more than one user is listed, just use the first one. + */ + m = TAILQ_FIRST(&us->users); + key.name = m->name ? m->name : "ALL"; + node = rbfind(seen_users, &key); + if (node != NULL) { + su = node->data; + if (asprintf(&cn, "%s_%ld", key.name, su->count) == -1) + cn = NULL; + su->count++; + } else { + if ((su = malloc(sizeof(*su))) == NULL) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + su->name = key.name; + su->count = 1; + if (rbinsert(seen_users, su, NULL) != 0) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + cn = strdup(key.name); + } + if (cn == NULL) { + sudo_fatalx(U_("%s: %s"), __func__, + U_("unable to allocate memory")); + } + + fprintf(fp, "dn: cn=%s,%s\n", cn, base); + fprintf(fp, "objectClass: top\n"); + fprintf(fp, "objectClass: sudoRole\n"); + fprintf(fp, "cn: %s\n", cn); + free(cn); + + TAILQ_FOREACH(m, &us->users, entries) { + print_member_ldif(fp, m->name, m->type, m->negated, + USERALIAS, "sudoUser"); + } + + TAILQ_FOREACH(m, &priv->hostlist, entries) { + print_member_ldif(fp, m->name, m->type, m->negated, + HOSTALIAS, "sudoHost"); + } + + print_cmndspec_ldif(fp, cs, &next); + + fprintf(fp, "sudoOrder: %d\n\n", ++sudo_order); + } + } + + debug_return_bool(!ferror(fp)); +} + +/* + * Print User_Specs. + */ +static bool +print_userspecs_ldif(FILE *fp, const char *base) +{ + struct userspec *us; + debug_decl(print_userspecs_ldif, SUDOERS_DEBUG_UTIL) + + TAILQ_FOREACH(us, &userspecs, entries) { + if (!print_userspec_ldif(fp, us, base)) + debug_return_bool(false); + } + debug_return_bool(true); +} + +/* + * Export the parsed sudoers file in LDIF format. + */ +bool +convert_sudoers_ldif(const char *output_file, const char *base) +{ + bool ret = true; + FILE *output_fp = stdout; + debug_decl(convert_sudoers_ldif, SUDOERS_DEBUG_UTIL) + + if (base == NULL) { + base = getenv("SUDOERS_BASE"); + if (base == NULL) + sudo_fatalx(U_("The SUDOERS_BASE environment variable is not set")); + } + + if (strcmp(output_file, "-") != 0) { + if ((output_fp = fopen(output_file, "w")) == NULL) + sudo_fatal(U_("unable to open %s"), output_file); + } + + /* Create a dictionary of already-seen users. */ + seen_users = rbcreate(seen_user_compare); + + /* Dump global Defaults in LDIF format. */ + print_global_defaults_ldif(output_fp, base); + + /* Dump User_Specs in LDIF format, expanding Aliases. */ + print_userspecs_ldif(output_fp, base); + + /* Warn about non-translatable Defaults entries. */ + warn_bound_defaults_ldif(output_fp); + + /* Clean up. */ + rbdestroy(seen_users, free); + + (void)fflush(output_fp); + if (ferror(output_fp)) + ret = false; + if (output_fp != stdout) + fclose(output_fp); + + debug_return_bool(ret); +}