]> granicus.if.org Git - sudo/commitdiff
Add ldif backend to cvtsudoers, to replace sudoers2ldif
authorTodd C. Miller <Todd.Miller@sudo.ws>
Sun, 28 Jan 2018 03:08:02 +0000 (20:08 -0700)
committerTodd C. Miller <Todd.Miller@sudo.ws>
Sun, 28 Jan 2018 03:08:02 +0000 (20:08 -0700)
MANIFEST
doc/cvtsudoers.cat
doc/cvtsudoers.man.in
doc/cvtsudoers.mdoc.in
plugins/sudoers/Makefile.in
plugins/sudoers/cvtsudoers.c
plugins/sudoers/cvtsudoers_ldif.c [new file with mode: 0644]

index 12c77fe8cec54c24008f6525dbee5f7d395f2dc5..022aaa24712fd274e408bc465c00a13e45b2fcb7 100644 (file)
--- 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
index 84364ff78070fd6ff2e81df231890b04b016ce66..526f56a185963a028eb671a47ac1b42af0cfcf51 100644 (file)
@@ -17,12 +17,18 @@ D\bDE\bES\bSC\bCR\bRI\bIP\bPT\bTI\bIO\bON\bN
      The options are as follows:
 
      -\b-f\bf, -\b--\b-f\bfo\bor\brm\bma\bat\bt
-                 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 _\bs_\bu_\bd_\bo_\be_\br_\bs format.  The various values have explicit
-                 types which removes much of the ambiguity of the _\bs_\bu_\bd_\bo_\be_\br_\bs
-                 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 _\bs_\bu_\bd_\bo_\be_\br_\bs format.  The various values
+                           have explicit types which removes much of the
+                           ambiguity of the _\bs_\bu_\bd_\bo_\be_\br_\bs format.
+
+                 LDIF      LDIF (LDAP Data Interchange Format) files can be
+                           imported into an LDAP server for use with
+                           sudoers.ldap(4).
 
      -\b-h\bh, -\b--\b-h\bhe\bel\blp\bp  Display a short help message to the standard output and exit.
 
@@ -35,7 +41,7 @@ D\bDE\bES\bSC\bCR\bRI\bIP\bPT\bTI\bIO\bON\bN
                  Print the c\bcv\bvt\bts\bsu\bud\bdo\boe\ber\brs\bs and _\bs_\bu_\bd_\bo_\be_\br_\bs grammar versions and exit.
 
 S\bSE\bEE\bE A\bAL\bLS\bSO\bO
-     sudoers(4), sudo(1m)
+     sudoers(4), sudoers.ldap(4), sudo(1m)
 
 A\bAU\bUT\bTH\bHO\bOR\bRS\bS
      Many people have worked on s\bsu\bud\bdo\bo over the years; this version consists of
@@ -63,4 +69,4 @@ D\bDI\bIS\bSC\bCL\bLA\bAI\bIM\bME\bER\bR
      file distributed with s\bsu\bud\bdo\bo 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
index 43723e3e794d90e8c3fa842d0c9eb6a830a01716..cfcdafbe6813f08d835eaec818b620a7fee228f7 100644 (file)
@@ -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
index 93e86bba2b8b1f4ffc9214677ba2560057750296..96c2e9c2ba5141ec50b3c369dde11fc5d086ce83 100644 (file)
@@ -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
index 50a5d0d9ee409eaea98aa5257bc83e71176d377f..9c9dc37a4dfb7bfb171be5a992157771ac700fdf 100644 (file)
@@ -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 \
index beb81497ac90238ff3a72a1c2aea70db04359c9d..011e3db83a5f5bd6984f0dcc424957e34f616543 100644 (file)
@@ -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 (file)
index 0000000..7282978
--- /dev/null
@@ -0,0 +1,408 @@
+/*
+ * Copyright (c) 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 <ctype.h>
+
+#include "sudoers.h"
+#include "parse.h"
+#include "redblack.h"
+#include <gram.h>
+
+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);
+}