]> granicus.if.org Git - sudo/commitdiff
Move ldif -> sudoers conversion code into parse_ldif.c
authorTodd C. Miller <Todd.Miller@sudo.ws>
Mon, 27 Aug 2018 02:02:49 +0000 (20:02 -0600)
committerTodd C. Miller <Todd.Miller@sudo.ws>
Mon, 27 Aug 2018 02:02:49 +0000 (20:02 -0600)
MANIFEST
plugins/sudoers/Makefile.in
plugins/sudoers/cvtsudoers.c
plugins/sudoers/cvtsudoers.h
plugins/sudoers/cvtsudoers_ldif.c
plugins/sudoers/parse.h
plugins/sudoers/parse_ldif.c [new file with mode: 0644]

index 105030924a22eea5f1425fc7422878f9beb3db08..6f1cc104c4993d286a574a7711b0f84ecfe1d6f7 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -332,6 +332,7 @@ plugins/sudoers/mkdefaults
 plugins/sudoers/mkdir_parents.c
 plugins/sudoers/parse.c
 plugins/sudoers/parse.h
+plugins/sudoers/parse_ldif.c
 plugins/sudoers/po/README
 plugins/sudoers/po/ca.mo
 plugins/sudoers/po/ca.po
index 9a2e1e1847a6aecb3ba05979905c260c4c9a3e63..e23ccb3b8787f43761d3cb1c458170a26181f079 100644 (file)
@@ -164,7 +164,7 @@ VISUDO_OBJS = editor.lo find_path.lo goodpath.lo locale.lo stubs.o \
              sudo_printf.o visudo.o
 
 CVTSUDOERS_OBJS = cvtsudoers.o cvtsudoers_json.o cvtsudoers_ldif.o \
-                 cvtsudoers_pwutil.o fmtsudoers.lo locale.lo \
+                 cvtsudoers_pwutil.o fmtsudoers.lo locale.lo parse_ldif.o \
                  strlist.o stubs.o sudo_printf.o ldap_util.lo
 
 REPLAY_OBJS = getdate.o sudoreplay.o iolog_util.lo
@@ -1133,6 +1133,17 @@ parse.lo: $(srcdir)/parse.c $(devdir)/def_data.h $(devdir)/gram.h \
           $(srcdir)/sudo_nss.h $(srcdir)/sudoers.h $(srcdir)/sudoers_debug.h \
           $(top_builddir)/config.h $(top_builddir)/pathnames.h
        $(LIBTOOL) $(LTFLAGS) --mode=compile $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/parse.c
+parse_ldif.o: $(srcdir)/parse_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)/redblack.h $(srcdir)/strlist.h \
+              $(srcdir)/sudo_ldap.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)/parse_ldif.c
 passwd.lo: $(authdir)/passwd.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 \
index 3adf5215302f19ef791d2b4af7353a638148f2d4..0a320496fc32633c35d4b8bfe7da3cc4f36a4417 100644 (file)
@@ -80,6 +80,7 @@ static void help(void) __attribute__((__noreturn__));
 static void usage(int);
 static bool convert_sudoers_sudoers(struct sudoers_parse_tree *parse_tree, const char *output_file, struct cvtsudoers_config *conf);
 static bool parse_sudoers(const char *input_file, struct cvtsudoers_config *conf);
+static bool parse_ldif(struct sudoers_parse_tree *parse_tree, const char *input_file, struct cvtsudoers_config *conf);
 static bool cvtsudoers_parse_filter(char *expression);
 static struct cvtsudoers_config *cvtsudoers_conf_read(const char *conf_file);
 static void cvtsudoers_conf_free(struct cvtsudoers_config *conf);
@@ -581,6 +582,24 @@ cvtsudoers_parse_filter(char *expression)
     debug_return_bool(true);
 }
 
+static bool
+parse_ldif(struct sudoers_parse_tree *parse_tree, const char *input_file,
+    struct cvtsudoers_config *conf)
+{
+    FILE *fp;
+    debug_decl(parse_ldif, SUDOERS_DEBUG_UTIL)
+
+    /* Open LDIF file and parse it. */
+    if (strcmp(input_file, "-") == 0) {
+       fp = stdin;
+       input_file = "stdin";
+    } else if ((fp = fopen(input_file, "r")) == NULL)
+       sudo_fatal(U_("unable to open %s"), input_file);
+
+    debug_return_bool(sudoers_parse_ldif(parse_tree, fp, conf->sudoers_base,
+     conf->store_options));
+}
+
 static bool
 parse_sudoers(const char *input_file, struct cvtsudoers_config *conf)
 {
index 8d6d48407b0f4bce2b4197a3537044268a4bf01e..562da5c965b99d445b1c5e12dd4603b4f1ee96a4 100644 (file)
@@ -83,8 +83,6 @@ bool convert_sudoers_json(struct sudoers_parse_tree *parse_tree, const char *out
 
 /* cvtsudoers_ldif.c */
 bool convert_sudoers_ldif(struct sudoers_parse_tree *parse_tree, const char *output_file, struct cvtsudoers_config *conf);
-bool parse_ldif(struct sudoers_parse_tree *parse_tree, const char *input_file, struct cvtsudoers_config *conf);
-void get_hostname(void);
 
 /* cvtsudoers_pwutil.c */
 struct cache_item *cvtsudoers_make_pwitem(uid_t uid, const char *name);
@@ -92,4 +90,7 @@ struct cache_item *cvtsudoers_make_gritem(gid_t gid, const char *name);
 struct cache_item *cvtsudoers_make_gidlist_item(const struct passwd *pw, char * const *unused1, unsigned int type);
 struct cache_item *cvtsudoers_make_grlist_item(const struct passwd *pw, char * const *unused1);
 
+/* stubs.c */
+void get_hostname(void);
+
 #endif /* SUDOERS_CVTSUDOERS_H */
index 2d5d2edaeb35002c32c2e4f9abb71be5ed83fb81..75064467eab7d5c16e83b2a8077684deceb00af6 100644 (file)
@@ -654,746 +654,3 @@ convert_sudoers_ldif(struct sudoers_parse_tree *parse_tree,
 
     debug_return_bool(ret);
 }
-
-struct sudo_role {
-    STAILQ_ENTRY(sudo_role) entries;
-    char *cn;
-    char *notbefore;
-    char *notafter;
-    double order;
-    struct sudoers_str_list *cmnds;
-    struct sudoers_str_list *hosts;
-    struct sudoers_str_list *users;
-    struct sudoers_str_list *runasusers;
-    struct sudoers_str_list *runasgroups;
-    struct sudoers_str_list *options;
-};
-STAILQ_HEAD(sudo_role_list, sudo_role);
-
-static void
-sudo_role_free(struct sudo_role *role)
-{
-    debug_decl(sudo_role_free, SUDOERS_DEBUG_UTIL)
-
-    if (role != NULL) {
-       free(role->cn);
-       free(role->notbefore);
-       free(role->notafter);
-       str_list_free(role->cmnds);
-       str_list_free(role->hosts);
-       str_list_free(role->users);
-       str_list_free(role->runasusers);
-       str_list_free(role->runasgroups);
-       str_list_free(role->options);
-       free(role);
-    }
-
-    debug_return;
-}
-
-static struct sudo_role *
-sudo_role_alloc(void)
-{
-    struct sudo_role *role;
-    debug_decl(sudo_role_alloc, SUDOERS_DEBUG_UTIL)
-
-    role = calloc(1, sizeof(*role));
-    if (role != NULL) {
-       role->cmnds = str_list_alloc();
-       role->hosts = str_list_alloc();
-       role->users = str_list_alloc();
-       role->runasusers = str_list_alloc();
-       role->runasgroups = str_list_alloc();
-       role->options = str_list_alloc();
-       if (role->cmnds == NULL || role->hosts == NULL ||
-           role->users == NULL || role->runasusers == NULL ||
-           role->runasgroups == NULL || role->options == NULL) {
-           sudo_role_free(role);
-           role = NULL;
-       }
-    }
-
-    debug_return_ptr(role);
-}
-
-/*
- * Parse an LDIF attribute, including base64 support.
- * See http://www.faqs.org/rfcs/rfc2849.html
- */
-static char *
-ldif_parse_attribute(char *str)
-{
-    bool encoded = false;
-    char *attr, *ep;
-    size_t len;
-    debug_decl(ldif_parse_attribute, SUDOERS_DEBUG_UTIL)
-
-    /* Check for foo:: base64str. */
-    if (*str == ':') {
-       encoded = true;
-       str++;
-    }
-
-    /* Trim leading and trailing space. */
-    while (*str == ' ')
-       str++;
-
-    ep = str + strlen(str);
-    while (ep > str && ep[-1] == ' ') {
-       /* Don't trim ecaped trailing space if not base64. */
-       if (!encoded && ep != str && ep[-2] == '\\')
-           break;
-       *--ep = '\0';
-    }
-
-    attr = str;
-    if (encoded) {
-       /*
-        * Decode base64 inline and add NUL-terminator.
-        * The copy allows us to provide a useful message on error.
-        */
-       char *copy = strdup(str);
-       if (copy == NULL) {
-           sudo_fatalx(U_("%s: %s"), __func__,
-               U_("unable to allocate memory"));
-       }
-       len = base64_decode(copy, (unsigned char *)attr, strlen(attr));
-       if (len == (size_t)-1) {
-           sudo_warnx(U_("ignoring invalid attribute value: %s"), copy);
-           free(copy);
-           debug_return_str(NULL);
-       }
-       attr[len] = '\0';
-       free(copy);
-    }
-
-    debug_return_str(attr);
-}
-
-/*
- * Allocate a struct sudoers_string, store str in it and
- * insert into the specified strlist.
- */
-static void
-ldif_store_string(const char *str, struct sudoers_str_list *strlist, bool sorted)
-{
-    struct sudoers_string *ls;
-    debug_decl(ldif_store_string, SUDOERS_DEBUG_UTIL)
-
-    if ((ls = sudoers_string_alloc(str)) == NULL) {
-       sudo_fatalx(U_("%s: %s"), __func__,
-           U_("unable to allocate memory"));
-    }
-    if (!sorted) {
-       STAILQ_INSERT_TAIL(strlist, ls, entries);
-    } else {
-       struct sudoers_string *prev, *next;
-
-       /* Insertion sort, list is small. */
-       prev = STAILQ_FIRST(strlist);
-       if (prev == NULL || strcasecmp(str, prev->str) <= 0) {
-           STAILQ_INSERT_HEAD(strlist, ls, entries);
-       } else {
-           while ((next = STAILQ_NEXT(prev, entries)) != NULL) {
-               if (strcasecmp(str, next->str) <= 0)
-                   break;
-               prev = next;
-           }
-           STAILQ_INSERT_AFTER(strlist, prev, ls, entries);
-       }
-    }
-
-    debug_return;
-}
-
-/*
- * Iterator for sudo_ldap_role_to_priv().
- * Takes a pointer to a struct sudoers_string *.
- * Returns the string or NULL if we've reached the end.
- */
-static char *
-sudoers_string_iter(void **vp)
-{
-    struct sudoers_string *ls = *vp;
-
-    if (ls == NULL)
-       return NULL;
-
-    *vp = STAILQ_NEXT(ls, entries);
-
-    return ls->str;
-}
-
-static int
-role_order_cmp(const void *va, const void *vb)
-{
-    const struct sudo_role *a = *(const struct sudo_role **)va;
-    const struct sudo_role *b = *(const struct sudo_role **)vb;
-    debug_decl(role_order_cmp, SUDOERS_DEBUG_LDAP)
-
-    debug_return_int(a->order < b->order ? -1 :
-        (a->order > b->order ? 1 : 0));
-}
-
-/*
- * Parse list of sudoOption and store in global defaults list.
- */
-static void
-ldif_store_options(struct sudoers_parse_tree *parse_tree,
-    struct sudoers_str_list *options)
-{
-    struct defaults *d;
-    struct sudoers_string *ls;
-    char *var, *val;
-    debug_decl(ldif_store_options, SUDOERS_DEBUG_UTIL)
-
-    STAILQ_FOREACH(ls, options, entries) {
-       if ((d = calloc(1, sizeof(*d))) == NULL ||
-           (d->binding = malloc(sizeof(*d->binding))) == NULL) {
-           sudo_fatalx(U_("%s: %s"), __func__,
-               U_("unable to allocate memory"));
-       }
-       TAILQ_INIT(d->binding);
-       d->type = DEFAULTS;
-       d->op = sudo_ldap_parse_option(ls->str, &var, &val);
-       if ((d->var = strdup(var)) == NULL) {
-           sudo_fatalx(U_("%s: %s"), __func__,
-               U_("unable to allocate memory"));
-       }
-       if (val != NULL) {
-           if ((d->val = strdup(val)) == NULL) {
-               sudo_fatalx(U_("%s: %s"), __func__,
-                   U_("unable to allocate memory"));
-           }
-       }
-       TAILQ_INSERT_TAIL(&parse_tree->defaults, d, entries);
-    }
-    debug_return;
-}
-
-static int
-str_list_cmp(const void *aa, const void *bb)
-{
-    const struct sudoers_str_list *a = aa;
-    const struct sudoers_str_list *b = bb;
-    const struct sudoers_string *lsa = STAILQ_FIRST(a);
-    const struct sudoers_string *lsb = STAILQ_FIRST(b);
-    int ret;
-
-    while (lsa != NULL && lsb != NULL) {
-       if ((ret = strcmp(lsa->str, lsb->str)) != 0)
-           return ret;
-       lsa = STAILQ_NEXT(lsa, entries);
-       lsb = STAILQ_NEXT(lsb, entries);
-    }
-    return lsa == lsb ? 0 : (lsa == NULL ? -1 : 1);
-}
-
-static int
-str_list_cache(struct rbtree *cache, struct sudoers_str_list **strlistp)
-{
-    struct sudoers_str_list *strlist = *strlistp;
-    struct rbnode *node;
-    int ret;
-    debug_decl(str_list_cache, SUDOERS_DEBUG_UTIL)
-
-    ret = rbinsert(cache, strlist, &node);
-    switch (ret) {
-    case 0:
-       /* new entry, take a ref for the cache */
-       strlist->refcnt++;
-       break;
-    case 1:
-       /* already exists, use existing and take a ref. */
-       str_list_free(strlist);
-       strlist = node->data;
-       strlist->refcnt++;
-       *strlistp = strlist;
-       break;
-    }
-    debug_return_int(ret);
-}
-
-/*
- * Convert a sudoRole to sudoers format and store in the global sudoers
- * data structures.
- */
-static void
-role_to_sudoers(struct sudoers_parse_tree *parse_tree, struct sudo_role *role,
-    bool store_options, bool reuse_userspec, bool reuse_privilege,
-    bool reuse_runas)
-{
-    struct privilege *priv;
-    struct sudoers_string *ls;
-    struct userspec *us;
-    struct member *m;
-    debug_decl(role_to_sudoers, SUDOERS_DEBUG_UTIL)
-
-    /*
-     * TODO: use cn to create a UserAlias if multiple users in it?
-     */
-
-    if (reuse_userspec) {
-       /* Re-use the previous userspec */
-       us = TAILQ_LAST(&parse_tree->userspecs, userspec_list);
-    } else {
-       /* Allocate a new userspec and fill in the user list. */
-       if ((us = calloc(1, sizeof(*us))) == NULL) {
-           sudo_fatalx(U_("%s: %s"), __func__,
-               U_("unable to allocate memory"));
-       }
-       TAILQ_INIT(&us->privileges);
-       TAILQ_INIT(&us->users);
-       STAILQ_INIT(&us->comments);
-
-       STAILQ_FOREACH(ls, role->users, entries) {
-           char *user = ls->str;
-
-           if ((m = calloc(1, sizeof(*m))) == NULL) {
-               sudo_fatalx(U_("%s: %s"), __func__,
-                   U_("unable to allocate memory"));
-           }
-           m->negated = sudo_ldap_is_negated(&user);
-           m->name = strdup(user);
-           if (m->name == NULL) {
-               sudo_fatalx(U_("%s: %s"), __func__,
-                   U_("unable to allocate memory"));
-           }
-           if (strcmp(user, "ALL") == 0) {
-               m->type = ALL;
-           } else if (*user == '+') {
-               m->type = NETGROUP;
-           } else if (*user == '%') {
-               m->type = USERGROUP;
-           } else {
-               m->type = WORD;
-           }
-           TAILQ_INSERT_TAIL(&us->users, m, entries);
-       }
-    }
-
-    /* Add source role as a comment. */
-    if (role->cn != NULL) {
-       struct sudoers_comment *comment = NULL;
-       if (reuse_userspec) {
-           /* Try to re-use comment too. */
-           STAILQ_FOREACH(comment, &us->comments, entries) {
-               if (strncmp(comment->str, "sudoRole ", 9) == 0) {
-                   char *tmpstr;
-                   if (asprintf(&tmpstr, "%s, %s", comment->str, role->cn) == -1) {
-                       sudo_fatalx(U_("%s: %s"), __func__,
-                           U_("unable to allocate memory"));
-                   }
-                   free(comment->str);
-                   comment->str = tmpstr;
-                   break;
-               }
-           }
-       }
-       if (comment == NULL) {
-           /* Create a new comment. */
-           if ((comment = malloc(sizeof(*comment))) == NULL) {
-               sudo_fatalx(U_("%s: %s"), __func__,
-                   U_("unable to allocate memory"));
-           }
-           if (asprintf(&comment->str, "sudoRole %s", role->cn) == -1) {
-               sudo_fatalx(U_("%s: %s"), __func__,
-                   U_("unable to allocate memory"));
-           }
-           STAILQ_INSERT_TAIL(&us->comments, comment, entries);
-       }
-    }
-
-    /* Convert role to sudoers privilege. */
-    priv = sudo_ldap_role_to_priv(role->cn, STAILQ_FIRST(role->hosts),
-       STAILQ_FIRST(role->runasusers), STAILQ_FIRST(role->runasgroups),
-       STAILQ_FIRST(role->cmnds), STAILQ_FIRST(role->options),
-       role->notbefore, role->notafter, true, store_options,
-       sudoers_string_iter);
-    if (priv == NULL) {
-       sudo_fatalx(U_("%s: %s"), __func__,
-           U_("unable to allocate memory"));
-    }
-
-    if (reuse_privilege) {
-       /* Hostspec unchanged, append cmndlist to previous privilege. */
-       struct privilege *prev_priv = TAILQ_LAST(&us->privileges, privilege_list);
-       if (reuse_runas) {
-           /* Runas users and groups same if as in previous privilege. */
-           struct member_list *runasuserlist =
-               TAILQ_FIRST(&prev_priv->cmndlist)->runasuserlist;
-           struct member_list *runasgrouplist =
-               TAILQ_FIRST(&prev_priv->cmndlist)->runasgrouplist;
-           struct cmndspec *cmndspec = TAILQ_FIRST(&priv->cmndlist);
-
-           /* Free duplicate runas lists. */
-           if (cmndspec->runasuserlist != NULL)
-               free_members(cmndspec->runasuserlist);
-           if (cmndspec->runasgrouplist != NULL)
-               free_members(cmndspec->runasgrouplist);
-
-           /* Update cmndspec with previous runas lists. */
-           TAILQ_FOREACH(cmndspec, &priv->cmndlist, entries) {
-               cmndspec->runasuserlist = runasuserlist;
-               cmndspec->runasgrouplist = runasgrouplist;
-           }
-       }
-       TAILQ_CONCAT(&prev_priv->cmndlist, &priv->cmndlist, entries);
-       free_privilege(priv);
-    } else {
-       TAILQ_INSERT_TAIL(&us->privileges, priv, entries);
-    }
-
-    /* Add finished userspec to the list if new. */
-    if (!reuse_userspec)
-       TAILQ_INSERT_TAIL(&parse_tree->userspecs, us, entries);
-
-    debug_return;
-}
-
-/*
- * Convert the list of sudoRoles to sudoers format and
- * store in the global sudoers data structures.
- */
-static void
-ldif_to_sudoers(struct sudoers_parse_tree *parse_tree,
-    struct sudo_role_list *roles, unsigned int numroles, bool store_options)
-{
-    struct sudo_role **role_array, *role = NULL;
-    unsigned int n;
-    debug_decl(ldif_to_sudoers, SUDOERS_DEBUG_UTIL)
-
-    /* Convert from list of roles to array and sort by order. */
-    role_array = reallocarray(NULL, numroles + 1, sizeof(*role_array));
-    for (n = 0; n < numroles; n++) {
-       if ((role = STAILQ_FIRST(roles)) == NULL)
-           break;      /* cannot happen */
-       STAILQ_REMOVE_HEAD(roles, entries);
-       role_array[n] = role;
-    }
-    role_array[n] = NULL;
-    qsort(role_array, numroles, sizeof(*role_array), role_order_cmp);
-
-    /*
-     * Iterate over roles in sorted order, converting to sudoers.
-     */
-    for (n = 0; n < numroles; n++) {
-       bool reuse_userspec = false;
-       bool reuse_privilege = false;
-       bool reuse_runas = false;
-
-       role = role_array[n];
-
-       /* Check whether we can reuse the previous user and host specs */
-       if (n > 0 && role->users == role_array[n - 1]->users) {
-           reuse_userspec = true;
-
-           /*
-            * Since options are stored per-privilege we can't
-            * append to the previous privilege's cmndlist if
-            * we are storing options.
-            */
-           if (!store_options) {
-               if (role->hosts == role_array[n - 1]->hosts) {
-                   reuse_privilege = true;
-
-                   /* Reuse runasusers and runasgroups if possible. */
-                   if (role->runasusers == role_array[n - 1]->runasusers &&
-                       role->runasgroups == role_array[n - 1]->runasgroups)
-                       reuse_runas = true;
-               }
-           }
-       }
-
-       role_to_sudoers(parse_tree, role, store_options, reuse_userspec,
-           reuse_privilege, reuse_runas);
-    }
-
-    /* Clean up. */
-    for (n = 0; n < numroles; n++)
-       sudo_role_free(role_array[n]);
-    free(role_array);
-
-    debug_return;
-}
-
-/*
- * Given a cn with possible quoted characters, return a copy of
- * the cn with quote characters ('\\') removed.
- * The caller is responsible for freeing the returned string.
- */
-static
-char *unquote_cn(const char *src)
-{
-    char *dst, *new_cn;
-    size_t len;
-    debug_decl(unquote_cn, SUDOERS_DEBUG_UTIL)
-
-    len = strlen(src);
-    if ((new_cn = malloc(len + 1)) == NULL)
-       debug_return_str(NULL);
-
-    for (dst = new_cn; *src != '\0';) {
-       if (src[0] == '\\' && src[1] != '\0')
-           src++;
-       *dst++ = *src++;
-    }
-    *dst = '\0';
-
-    debug_return_str(new_cn);
-}
-
-/*
- * Parse a sudoers file in LDIF format, https://tools.ietf.org/html/rfc2849
- * Parsed sudoRole objects are stored in the specified parse_tree which
- * must already be initialized.
- */
-bool
-parse_ldif(struct sudoers_parse_tree *parse_tree, const char *input_file,
-    struct cvtsudoers_config *conf)
-{
-    struct sudo_role_list roles = STAILQ_HEAD_INITIALIZER(roles);
-    struct sudo_role *role = NULL;
-    struct rbtree *usercache, *groupcache, *hostcache;
-    unsigned numroles = 0;
-    bool in_role = false;
-    size_t linesize = 0;
-    char *attr, *line = NULL, *savedline = NULL;
-    ssize_t savedlen = 0;
-    bool mismatch = false;
-    FILE *fp;
-    debug_decl(parse_ldif, SUDOERS_DEBUG_UTIL)
-
-    /* Open LDIF file and parse it. */
-    if (strcmp(input_file, "-") == 0) {
-        fp = stdin;
-        input_file = "stdin";
-    } else if ((fp = fopen(input_file, "r")) == NULL)
-        sudo_fatal(U_("unable to open %s"), input_file);
-
-    /* Free old contents of the parse tree (if any). */
-    free_parse_tree(parse_tree);
-
-    /*
-     * We cache user, group and host lists to make it eay to detect when there
-     * are identical lists (simple pointer compare).  This makes it possible
-     * to merge multiplpe sudoRole objects into a single UserSpec and/or
-     * Privilege.  The lists are sorted since LDAP order is arbitrary.
-     */
-    usercache = rbcreate(str_list_cmp);
-    groupcache = rbcreate(str_list_cmp);
-    hostcache = rbcreate(str_list_cmp);
-    if (usercache == NULL || groupcache == NULL || hostcache == NULL)
-       sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
-
-    /* Read through input, parsing into sudo_roles and global defaults. */
-    for (;;) {
-       int ch;
-       ssize_t len = getline(&line, &linesize, fp);
-
-       /* Trim trailing return or newline. */
-       while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n'))
-           line[--len] = '\0';
-
-       /* Blank line or EOF terminates an entry. */
-       if (len <= 0) {
-           if (in_role) {
-               if (role->cn != NULL && strcmp(role->cn, "defaults") == 0) {
-                   ldif_store_options(parse_tree, role->options);
-                   sudo_role_free(role);
-                   role = NULL;
-               } else if (STAILQ_EMPTY(role->users) ||
-                   STAILQ_EMPTY(role->hosts) || STAILQ_EMPTY(role->cmnds)) {
-                   /* Incomplete role. */
-                   sudo_warnx(U_("ignoring incomplete sudoRole: cn: %s"),
-                       role->cn ? role->cn : "UNKNOWN");
-                   sudo_role_free(role);
-                   role = NULL;
-               } else {
-                   /* Cache users, hosts, runasusers and runasgroups. */
-                   if (str_list_cache(usercache, &role->users) == -1 ||
-                       str_list_cache(hostcache, &role->hosts) == -1 ||
-                       str_list_cache(usercache, &role->runasusers) == -1 ||
-                       str_list_cache(groupcache, &role->runasgroups) == -1) {
-                       sudo_fatalx(U_("%s: %s"), __func__,
-                           U_("unable to allocate memory"));
-                   }
-
-                   /* Store finished role. */
-                   STAILQ_INSERT_TAIL(&roles, role, entries);
-                   numroles++;
-               }
-               role = NULL;
-               in_role = false;
-           }
-           if (len == -1) {
-               /* EOF */
-               break;
-           }
-           mismatch = false;
-           continue;
-       }
-
-       if (savedline != NULL) {
-           char *tmp;
-
-           /* Append to saved line. */
-           linesize = savedlen + len + 1;
-           if ((tmp = realloc(savedline, linesize)) == NULL) {
-               sudo_fatalx(U_("%s: %s"), __func__,
-                   U_("unable to allocate memory"));
-           }
-           memcpy(tmp + savedlen, line, len + 1);
-           free(line);
-           line = tmp;
-           savedline = NULL;
-       } else {
-           /* Skip comment lines or records that don't match the base. */
-           if (*line == '#' || mismatch)
-               continue;
-       }
-
-       /* Check for folded line */
-       if ((ch = getc(fp)) == ' ') {
-           /* folded line, append to the saved portion. */
-           savedlen = len;
-           savedline = line;
-           line = NULL;
-           linesize = 0;
-           continue;
-       } else {
-           /* not folded, push back ch */
-           ungetc(ch, fp);
-       }
-
-       /* Allocate new role as needed. */
-       if (role == NULL) {
-           if ((role = sudo_role_alloc()) == NULL) {
-               sudo_fatalx(U_("%s: %s"), __func__,
-                   U_("unable to allocate memory"));
-           }
-       }
-
-       /* Parse dn and objectClass. */
-       if (strncasecmp(line, "dn:", 3) == 0) {
-           /* Compare dn to base, if specified. */
-           if (conf->sudoers_base != NULL) {
-               attr = ldif_parse_attribute(line + 3);
-               if (attr == NULL) {
-                   /* invalid attribute */
-                   mismatch = true;
-                   continue;
-               }
-               /* Skip over cn if present. */
-               if (strncasecmp(attr, "cn=", 3) == 0) {
-                   for (attr += 3; *attr != '\0'; attr++) {
-                       /* Handle escaped ',' chars. */
-                       if (*attr == '\\')
-                           attr++;
-                       if (*attr == ',') {
-                           attr++;
-                           break;
-                       }
-                   }
-               }
-               if (strcasecmp(attr, conf->sudoers_base) != 0) {
-                   /* Doesn't match base, skip the rest of it. */
-                   mismatch = true;
-                   continue;
-               }
-           }
-       } else if (strncmp(line, "objectClass:", 12) == 0) {
-           attr = ldif_parse_attribute(line + 12);
-           if (attr != NULL && strcmp(attr, "sudoRole") == 0)
-               in_role = true;
-       }
-
-       /* Not in a sudoRole, keep reading. */
-       if (!in_role)
-           continue;
-
-       /* Part of a sudoRole, parse it. */
-       if (strncmp(line, "cn:", 3) == 0) {
-           attr = ldif_parse_attribute(line + 3);
-           if (attr != NULL) {
-               free(role->cn);
-               role->cn = unquote_cn(attr);
-               if (role->cn == NULL) {
-                   sudo_fatalx(U_("%s: %s"), __func__,
-                       U_("unable to allocate memory"));
-               }
-           }
-       } else if (strncmp(line, "sudoUser:", 9) == 0) {
-           attr = ldif_parse_attribute(line + 9);
-           if (attr != NULL)
-               ldif_store_string(attr, role->users, true);
-       } else if (strncmp(line, "sudoHost:", 9) == 0) {
-           attr = ldif_parse_attribute(line + 9);
-           if (attr != NULL)
-               ldif_store_string(attr, role->hosts, true);
-       } else if (strncmp(line, "sudoRunAs:", 10) == 0) {
-           attr = ldif_parse_attribute(line + 10);
-           if (attr != NULL)
-               ldif_store_string(attr, role->runasusers, true);
-       } else if (strncmp(line, "sudoRunAsUser:", 14) == 0) {
-           attr = ldif_parse_attribute(line + 14);
-           if (attr != NULL)
-               ldif_store_string(attr, role->runasusers, true);
-       } else if (strncmp(line, "sudoRunAsGroup:", 15) == 0) {
-           attr = ldif_parse_attribute(line + 15);
-           if (attr != NULL)
-               ldif_store_string(attr, role->runasgroups, true);
-       } else if (strncmp(line, "sudoCommand:", 12) == 0) {
-           attr = ldif_parse_attribute(line + 12);
-           if (attr != NULL)
-               ldif_store_string(attr, role->cmnds, false);
-       } else if (strncmp(line, "sudoOption:", 11) == 0) {
-           attr = ldif_parse_attribute(line + 11);
-           if (attr != NULL)
-               ldif_store_string(attr, role->options, false);
-       } else if (strncmp(line, "sudoOrder:", 10) == 0) {
-           char *ep;
-           attr = ldif_parse_attribute(line + 10);
-           if (attr != NULL) {
-               role->order = strtod(attr, &ep);
-               if (ep == attr || *ep != '\0')
-                   sudo_warnx(U_("invalid sudoOrder attribute: %s"), attr);
-           }
-       } else if (strncmp(line, "sudoNotBefore:", 14) == 0) {
-           attr = ldif_parse_attribute(line + 14);
-           if (attr != NULL) {
-               free(role->notbefore);
-               role->notbefore = strdup(attr);
-               if (role->notbefore == NULL) {
-                   sudo_fatalx(U_("%s: %s"), __func__,
-                       U_("unable to allocate memory"));
-               }
-           }
-       } else if (strncmp(line, "sudoNotAfter:", 13) == 0) {
-           attr = ldif_parse_attribute(line + 13);
-           if (attr != NULL) {
-               free(role->notafter);
-               role->notafter = strdup(attr);
-               if (role->notafter == NULL) {
-                   sudo_fatalx(U_("%s: %s"), __func__,
-                       U_("unable to allocate memory"));
-               }
-           }
-       }
-    }
-    sudo_role_free(role);
-    free(line);
-
-    /* Convert from roles to sudoers data structures. */
-    ldif_to_sudoers(parse_tree, &roles, numroles, conf->store_options);
-
-    /* Clean up. */
-    rbdestroy(usercache, str_list_free);
-    rbdestroy(groupcache, str_list_free);
-    rbdestroy(hostcache, str_list_free);
-
-    if (fp != stdin)
-       fclose(fp);
-
-    debug_return_bool(true);
-}
index b8b2497ad300c4651cec3cc4ea457fda28771ae0..426144c533cde022fdfdc96c1a13dc3489c46d11 100644 (file)
@@ -346,6 +346,9 @@ int sudoers_lookup(struct sudo_nss_list *snl, struct passwd *pw, int validated,
 int display_privs(struct sudo_nss_list *snl, struct passwd *pw, bool verbose);
 int display_cmnd(struct sudo_nss_list *snl, struct passwd *pw);
 
+/* parse_ldif.c */
+bool sudoers_parse_ldif(struct sudoers_parse_tree *parse_tree, FILE *fp, const char *sudoers_base, bool store_options);
+
 /* fmtsudoers.c */
 struct sudo_lbuf;
 bool sudoers_format_cmndspec(struct sudo_lbuf *lbuf, struct sudoers_parse_tree *parse_tree, struct cmndspec *cs, struct cmndspec *prev_cs, struct cmndtag tags, bool expand_aliases);
diff --git a/plugins/sudoers/parse_ldif.c b/plugins/sudoers/parse_ldif.c
new file mode 100644 (file)
index 0000000..0b07b7c
--- /dev/null
@@ -0,0 +1,766 @@
+/*
+ * 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 "sudoers.h"
+#include "sudo_ldap.h"
+#include "redblack.h"
+#include "strlist.h"
+#include <gram.h>
+
+struct sudo_role {
+    STAILQ_ENTRY(sudo_role) entries;
+    char *cn;
+    char *notbefore;
+    char *notafter;
+    double order;
+    struct sudoers_str_list *cmnds;
+    struct sudoers_str_list *hosts;
+    struct sudoers_str_list *users;
+    struct sudoers_str_list *runasusers;
+    struct sudoers_str_list *runasgroups;
+    struct sudoers_str_list *options;
+};
+STAILQ_HEAD(sudo_role_list, sudo_role);
+
+static void
+sudo_role_free(struct sudo_role *role)
+{
+    debug_decl(sudo_role_free, SUDOERS_DEBUG_UTIL)
+
+    if (role != NULL) {
+       free(role->cn);
+       free(role->notbefore);
+       free(role->notafter);
+       str_list_free(role->cmnds);
+       str_list_free(role->hosts);
+       str_list_free(role->users);
+       str_list_free(role->runasusers);
+       str_list_free(role->runasgroups);
+       str_list_free(role->options);
+       free(role);
+    }
+
+    debug_return;
+}
+
+static struct sudo_role *
+sudo_role_alloc(void)
+{
+    struct sudo_role *role;
+    debug_decl(sudo_role_alloc, SUDOERS_DEBUG_UTIL)
+
+    role = calloc(1, sizeof(*role));
+    if (role != NULL) {
+       role->cmnds = str_list_alloc();
+       role->hosts = str_list_alloc();
+       role->users = str_list_alloc();
+       role->runasusers = str_list_alloc();
+       role->runasgroups = str_list_alloc();
+       role->options = str_list_alloc();
+       if (role->cmnds == NULL || role->hosts == NULL ||
+           role->users == NULL || role->runasusers == NULL ||
+           role->runasgroups == NULL || role->options == NULL) {
+           sudo_role_free(role);
+           role = NULL;
+       }
+    }
+
+    debug_return_ptr(role);
+}
+
+/*
+ * Parse an LDIF attribute, including base64 support.
+ * See http://www.faqs.org/rfcs/rfc2849.html
+ */
+static char *
+ldif_parse_attribute(char *str)
+{
+    bool encoded = false;
+    char *attr, *ep;
+    size_t len;
+    debug_decl(ldif_parse_attribute, SUDOERS_DEBUG_UTIL)
+
+    /* Check for foo:: base64str. */
+    if (*str == ':') {
+       encoded = true;
+       str++;
+    }
+
+    /* Trim leading and trailing space. */
+    while (*str == ' ')
+       str++;
+
+    ep = str + strlen(str);
+    while (ep > str && ep[-1] == ' ') {
+       /* Don't trim ecaped trailing space if not base64. */
+       if (!encoded && ep != str && ep[-2] == '\\')
+           break;
+       *--ep = '\0';
+    }
+
+    attr = str;
+    if (encoded) {
+       /*
+        * Decode base64 inline and add NUL-terminator.
+        * The copy allows us to provide a useful message on error.
+        */
+       char *copy = strdup(str);
+       if (copy == NULL) {
+           sudo_fatalx(U_("%s: %s"), __func__,
+               U_("unable to allocate memory"));
+       }
+       len = base64_decode(copy, (unsigned char *)attr, strlen(attr));
+       if (len == (size_t)-1) {
+           sudo_warnx(U_("ignoring invalid attribute value: %s"), copy);
+           free(copy);
+           debug_return_str(NULL);
+       }
+       attr[len] = '\0';
+       free(copy);
+    }
+
+    debug_return_str(attr);
+}
+
+/*
+ * Allocate a struct sudoers_string, store str in it and
+ * insert into the specified strlist.
+ */
+static void
+ldif_store_string(const char *str, struct sudoers_str_list *strlist, bool sorted)
+{
+    struct sudoers_string *ls;
+    debug_decl(ldif_store_string, SUDOERS_DEBUG_UTIL)
+
+    if ((ls = sudoers_string_alloc(str)) == NULL) {
+       sudo_fatalx(U_("%s: %s"), __func__,
+           U_("unable to allocate memory"));
+    }
+    if (!sorted) {
+       STAILQ_INSERT_TAIL(strlist, ls, entries);
+    } else {
+       struct sudoers_string *prev, *next;
+
+       /* Insertion sort, list is small. */
+       prev = STAILQ_FIRST(strlist);
+       if (prev == NULL || strcasecmp(str, prev->str) <= 0) {
+           STAILQ_INSERT_HEAD(strlist, ls, entries);
+       } else {
+           while ((next = STAILQ_NEXT(prev, entries)) != NULL) {
+               if (strcasecmp(str, next->str) <= 0)
+                   break;
+               prev = next;
+           }
+           STAILQ_INSERT_AFTER(strlist, prev, ls, entries);
+       }
+    }
+
+    debug_return;
+}
+
+/*
+ * Iterator for sudo_ldap_role_to_priv().
+ * Takes a pointer to a struct sudoers_string *.
+ * Returns the string or NULL if we've reached the end.
+ */
+static char *
+sudoers_string_iter(void **vp)
+{
+    struct sudoers_string *ls = *vp;
+
+    if (ls == NULL)
+       return NULL;
+
+    *vp = STAILQ_NEXT(ls, entries);
+
+    return ls->str;
+}
+
+static int
+role_order_cmp(const void *va, const void *vb)
+{
+    const struct sudo_role *a = *(const struct sudo_role **)va;
+    const struct sudo_role *b = *(const struct sudo_role **)vb;
+    debug_decl(role_order_cmp, SUDOERS_DEBUG_LDAP)
+
+    debug_return_int(a->order < b->order ? -1 :
+        (a->order > b->order ? 1 : 0));
+}
+
+/*
+ * Parse list of sudoOption and store in the parse tree's defaults list.
+ */
+static void
+ldif_store_options(struct sudoers_parse_tree *parse_tree,
+    struct sudoers_str_list *options)
+{
+    struct defaults *d;
+    struct sudoers_string *ls;
+    char *var, *val;
+    debug_decl(ldif_store_options, SUDOERS_DEBUG_UTIL)
+
+    STAILQ_FOREACH(ls, options, entries) {
+       if ((d = calloc(1, sizeof(*d))) == NULL ||
+           (d->binding = malloc(sizeof(*d->binding))) == NULL) {
+           sudo_fatalx(U_("%s: %s"), __func__,
+               U_("unable to allocate memory"));
+       }
+       TAILQ_INIT(d->binding);
+       d->type = DEFAULTS;
+       d->op = sudo_ldap_parse_option(ls->str, &var, &val);
+       if ((d->var = strdup(var)) == NULL) {
+           sudo_fatalx(U_("%s: %s"), __func__,
+               U_("unable to allocate memory"));
+       }
+       if (val != NULL) {
+           if ((d->val = strdup(val)) == NULL) {
+               sudo_fatalx(U_("%s: %s"), __func__,
+                   U_("unable to allocate memory"));
+           }
+       }
+       TAILQ_INSERT_TAIL(&parse_tree->defaults, d, entries);
+    }
+    debug_return;
+}
+
+static int
+str_list_cmp(const void *aa, const void *bb)
+{
+    const struct sudoers_str_list *a = aa;
+    const struct sudoers_str_list *b = bb;
+    const struct sudoers_string *lsa = STAILQ_FIRST(a);
+    const struct sudoers_string *lsb = STAILQ_FIRST(b);
+    int ret;
+
+    while (lsa != NULL && lsb != NULL) {
+       if ((ret = strcmp(lsa->str, lsb->str)) != 0)
+           return ret;
+       lsa = STAILQ_NEXT(lsa, entries);
+       lsb = STAILQ_NEXT(lsb, entries);
+    }
+    return lsa == lsb ? 0 : (lsa == NULL ? -1 : 1);
+}
+
+static int
+str_list_cache(struct rbtree *cache, struct sudoers_str_list **strlistp)
+{
+    struct sudoers_str_list *strlist = *strlistp;
+    struct rbnode *node;
+    int ret;
+    debug_decl(str_list_cache, SUDOERS_DEBUG_UTIL)
+
+    ret = rbinsert(cache, strlist, &node);
+    switch (ret) {
+    case 0:
+       /* new entry, take a ref for the cache */
+       strlist->refcnt++;
+       break;
+    case 1:
+       /* already exists, use existing and take a ref. */
+       str_list_free(strlist);
+       strlist = node->data;
+       strlist->refcnt++;
+       *strlistp = strlist;
+       break;
+    }
+    debug_return_int(ret);
+}
+
+/*
+ * Convert a sudoRole to sudoers format and store in the parse tree.
+ */
+static void
+role_to_sudoers(struct sudoers_parse_tree *parse_tree, struct sudo_role *role,
+    bool store_options, bool reuse_userspec, bool reuse_privilege,
+    bool reuse_runas)
+{
+    struct privilege *priv;
+    struct sudoers_string *ls;
+    struct userspec *us;
+    struct member *m;
+    debug_decl(role_to_sudoers, SUDOERS_DEBUG_UTIL)
+
+    /*
+     * TODO: use cn to create a UserAlias if multiple users in it?
+     */
+
+    if (reuse_userspec) {
+       /* Re-use the previous userspec */
+       us = TAILQ_LAST(&parse_tree->userspecs, userspec_list);
+    } else {
+       /* Allocate a new userspec and fill in the user list. */
+       if ((us = calloc(1, sizeof(*us))) == NULL) {
+           sudo_fatalx(U_("%s: %s"), __func__,
+               U_("unable to allocate memory"));
+       }
+       TAILQ_INIT(&us->privileges);
+       TAILQ_INIT(&us->users);
+       STAILQ_INIT(&us->comments);
+
+       STAILQ_FOREACH(ls, role->users, entries) {
+           char *user = ls->str;
+
+           if ((m = calloc(1, sizeof(*m))) == NULL) {
+               sudo_fatalx(U_("%s: %s"), __func__,
+                   U_("unable to allocate memory"));
+           }
+           m->negated = sudo_ldap_is_negated(&user);
+           m->name = strdup(user);
+           if (m->name == NULL) {
+               sudo_fatalx(U_("%s: %s"), __func__,
+                   U_("unable to allocate memory"));
+           }
+           if (strcmp(user, "ALL") == 0) {
+               m->type = ALL;
+           } else if (*user == '+') {
+               m->type = NETGROUP;
+           } else if (*user == '%') {
+               m->type = USERGROUP;
+           } else {
+               m->type = WORD;
+           }
+           TAILQ_INSERT_TAIL(&us->users, m, entries);
+       }
+    }
+
+    /* Add source role as a comment. */
+    if (role->cn != NULL) {
+       struct sudoers_comment *comment = NULL;
+       if (reuse_userspec) {
+           /* Try to re-use comment too. */
+           STAILQ_FOREACH(comment, &us->comments, entries) {
+               if (strncmp(comment->str, "sudoRole ", 9) == 0) {
+                   char *tmpstr;
+                   if (asprintf(&tmpstr, "%s, %s", comment->str, role->cn) == -1) {
+                       sudo_fatalx(U_("%s: %s"), __func__,
+                           U_("unable to allocate memory"));
+                   }
+                   free(comment->str);
+                   comment->str = tmpstr;
+                   break;
+               }
+           }
+       }
+       if (comment == NULL) {
+           /* Create a new comment. */
+           if ((comment = malloc(sizeof(*comment))) == NULL) {
+               sudo_fatalx(U_("%s: %s"), __func__,
+                   U_("unable to allocate memory"));
+           }
+           if (asprintf(&comment->str, "sudoRole %s", role->cn) == -1) {
+               sudo_fatalx(U_("%s: %s"), __func__,
+                   U_("unable to allocate memory"));
+           }
+           STAILQ_INSERT_TAIL(&us->comments, comment, entries);
+       }
+    }
+
+    /* Convert role to sudoers privilege. */
+    priv = sudo_ldap_role_to_priv(role->cn, STAILQ_FIRST(role->hosts),
+       STAILQ_FIRST(role->runasusers), STAILQ_FIRST(role->runasgroups),
+       STAILQ_FIRST(role->cmnds), STAILQ_FIRST(role->options),
+       role->notbefore, role->notafter, true, store_options,
+       sudoers_string_iter);
+    if (priv == NULL) {
+       sudo_fatalx(U_("%s: %s"), __func__,
+           U_("unable to allocate memory"));
+    }
+
+    if (reuse_privilege) {
+       /* Hostspec unchanged, append cmndlist to previous privilege. */
+       struct privilege *prev_priv = TAILQ_LAST(&us->privileges, privilege_list);
+       if (reuse_runas) {
+           /* Runas users and groups same if as in previous privilege. */
+           struct member_list *runasuserlist =
+               TAILQ_FIRST(&prev_priv->cmndlist)->runasuserlist;
+           struct member_list *runasgrouplist =
+               TAILQ_FIRST(&prev_priv->cmndlist)->runasgrouplist;
+           struct cmndspec *cmndspec = TAILQ_FIRST(&priv->cmndlist);
+
+           /* Free duplicate runas lists. */
+           if (cmndspec->runasuserlist != NULL)
+               free_members(cmndspec->runasuserlist);
+           if (cmndspec->runasgrouplist != NULL)
+               free_members(cmndspec->runasgrouplist);
+
+           /* Update cmndspec with previous runas lists. */
+           TAILQ_FOREACH(cmndspec, &priv->cmndlist, entries) {
+               cmndspec->runasuserlist = runasuserlist;
+               cmndspec->runasgrouplist = runasgrouplist;
+           }
+       }
+       TAILQ_CONCAT(&prev_priv->cmndlist, &priv->cmndlist, entries);
+       free_privilege(priv);
+    } else {
+       TAILQ_INSERT_TAIL(&us->privileges, priv, entries);
+    }
+
+    /* Add finished userspec to the list if new. */
+    if (!reuse_userspec)
+       TAILQ_INSERT_TAIL(&parse_tree->userspecs, us, entries);
+
+    debug_return;
+}
+
+/*
+ * Convert the list of sudoRoles to sudoers format and store in the parse tree.
+ */
+static void
+ldif_to_sudoers(struct sudoers_parse_tree *parse_tree,
+    struct sudo_role_list *roles, unsigned int numroles, bool store_options)
+{
+    struct sudo_role **role_array, *role = NULL;
+    unsigned int n;
+    debug_decl(ldif_to_sudoers, SUDOERS_DEBUG_UTIL)
+
+    /* Convert from list of roles to array and sort by order. */
+    role_array = reallocarray(NULL, numroles + 1, sizeof(*role_array));
+    for (n = 0; n < numroles; n++) {
+       if ((role = STAILQ_FIRST(roles)) == NULL)
+           break;      /* cannot happen */
+       STAILQ_REMOVE_HEAD(roles, entries);
+       role_array[n] = role;
+    }
+    role_array[n] = NULL;
+    qsort(role_array, numroles, sizeof(*role_array), role_order_cmp);
+
+    /*
+     * Iterate over roles in sorted order, converting to sudoers.
+     */
+    for (n = 0; n < numroles; n++) {
+       bool reuse_userspec = false;
+       bool reuse_privilege = false;
+       bool reuse_runas = false;
+
+       role = role_array[n];
+
+       /* Check whether we can reuse the previous user and host specs */
+       if (n > 0 && role->users == role_array[n - 1]->users) {
+           reuse_userspec = true;
+
+           /*
+            * Since options are stored per-privilege we can't
+            * append to the previous privilege's cmndlist if
+            * we are storing options.
+            */
+           if (!store_options) {
+               if (role->hosts == role_array[n - 1]->hosts) {
+                   reuse_privilege = true;
+
+                   /* Reuse runasusers and runasgroups if possible. */
+                   if (role->runasusers == role_array[n - 1]->runasusers &&
+                       role->runasgroups == role_array[n - 1]->runasgroups)
+                       reuse_runas = true;
+               }
+           }
+       }
+
+       role_to_sudoers(parse_tree, role, store_options, reuse_userspec,
+           reuse_privilege, reuse_runas);
+    }
+
+    /* Clean up. */
+    for (n = 0; n < numroles; n++)
+       sudo_role_free(role_array[n]);
+    free(role_array);
+
+    debug_return;
+}
+
+/*
+ * Given a cn with possible quoted characters, return a copy of
+ * the cn with quote characters ('\\') removed.
+ * The caller is responsible for freeing the returned string.
+ */
+static
+char *unquote_cn(const char *src)
+{
+    char *dst, *new_cn;
+    size_t len;
+    debug_decl(unquote_cn, SUDOERS_DEBUG_UTIL)
+
+    len = strlen(src);
+    if ((new_cn = malloc(len + 1)) == NULL)
+       debug_return_str(NULL);
+
+    for (dst = new_cn; *src != '\0';) {
+       if (src[0] == '\\' && src[1] != '\0')
+           src++;
+       *dst++ = *src++;
+    }
+    *dst = '\0';
+
+    debug_return_str(new_cn);
+}
+
+/*
+ * Parse a sudoers file in LDIF format, https://tools.ietf.org/html/rfc2849
+ * Parsed sudoRole objects are stored in the specified parse_tree which
+ * must already be initialized.
+ */
+bool
+sudoers_parse_ldif(struct sudoers_parse_tree *parse_tree,
+    FILE *fp, const char *sudoers_base, bool store_options)
+{
+    struct sudo_role_list roles = STAILQ_HEAD_INITIALIZER(roles);
+    struct sudo_role *role = NULL;
+    struct rbtree *usercache, *groupcache, *hostcache;
+    unsigned numroles = 0;
+    bool in_role = false;
+    size_t linesize = 0;
+    char *attr, *line = NULL, *savedline = NULL;
+    ssize_t savedlen = 0;
+    bool mismatch = false;
+    debug_decl(sudoers_parse_ldif, SUDOERS_DEBUG_UTIL)
+
+    /* Free old contents of the parse tree (if any). */
+    free_parse_tree(parse_tree);
+
+    /*
+     * We cache user, group and host lists to make it eay to detect when there
+     * are identical lists (simple pointer compare).  This makes it possible
+     * to merge multiplpe sudoRole objects into a single UserSpec and/or
+     * Privilege.  The lists are sorted since LDAP order is arbitrary.
+     */
+    usercache = rbcreate(str_list_cmp);
+    groupcache = rbcreate(str_list_cmp);
+    hostcache = rbcreate(str_list_cmp);
+    if (usercache == NULL || groupcache == NULL || hostcache == NULL)
+       sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
+
+    /* Read through input, parsing into sudo_roles and global defaults. */
+    for (;;) {
+       int ch;
+       ssize_t len = getline(&line, &linesize, fp);
+
+       /* Trim trailing return or newline. */
+       while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n'))
+           line[--len] = '\0';
+
+       /* Blank line or EOF terminates an entry. */
+       if (len <= 0) {
+           if (in_role) {
+               if (role->cn != NULL && strcmp(role->cn, "defaults") == 0) {
+                   ldif_store_options(parse_tree, role->options);
+                   sudo_role_free(role);
+                   role = NULL;
+               } else if (STAILQ_EMPTY(role->users) ||
+                   STAILQ_EMPTY(role->hosts) || STAILQ_EMPTY(role->cmnds)) {
+                   /* Incomplete role. */
+                   sudo_warnx(U_("ignoring incomplete sudoRole: cn: %s"),
+                       role->cn ? role->cn : "UNKNOWN");
+                   sudo_role_free(role);
+                   role = NULL;
+               } else {
+                   /* Cache users, hosts, runasusers and runasgroups. */
+                   if (str_list_cache(usercache, &role->users) == -1 ||
+                       str_list_cache(hostcache, &role->hosts) == -1 ||
+                       str_list_cache(usercache, &role->runasusers) == -1 ||
+                       str_list_cache(groupcache, &role->runasgroups) == -1) {
+                       sudo_fatalx(U_("%s: %s"), __func__,
+                           U_("unable to allocate memory"));
+                   }
+
+                   /* Store finished role. */
+                   STAILQ_INSERT_TAIL(&roles, role, entries);
+                   numroles++;
+               }
+               role = NULL;
+               in_role = false;
+           }
+           if (len == -1) {
+               /* EOF */
+               break;
+           }
+           mismatch = false;
+           continue;
+       }
+
+       if (savedline != NULL) {
+           char *tmp;
+
+           /* Append to saved line. */
+           linesize = savedlen + len + 1;
+           if ((tmp = realloc(savedline, linesize)) == NULL) {
+               sudo_fatalx(U_("%s: %s"), __func__,
+                   U_("unable to allocate memory"));
+           }
+           memcpy(tmp + savedlen, line, len + 1);
+           free(line);
+           line = tmp;
+           savedline = NULL;
+       } else {
+           /* Skip comment lines or records that don't match the base. */
+           if (*line == '#' || mismatch)
+               continue;
+       }
+
+       /* Check for folded line */
+       if ((ch = getc(fp)) == ' ') {
+           /* folded line, append to the saved portion. */
+           savedlen = len;
+           savedline = line;
+           line = NULL;
+           linesize = 0;
+           continue;
+       } else {
+           /* not folded, push back ch */
+           ungetc(ch, fp);
+       }
+
+       /* Allocate new role as needed. */
+       if (role == NULL) {
+           if ((role = sudo_role_alloc()) == NULL) {
+               sudo_fatalx(U_("%s: %s"), __func__,
+                   U_("unable to allocate memory"));
+           }
+       }
+
+       /* Parse dn and objectClass. */
+       if (strncasecmp(line, "dn:", 3) == 0) {
+           /* Compare dn to base, if specified. */
+           if (sudoers_base != NULL) {
+               attr = ldif_parse_attribute(line + 3);
+               if (attr == NULL) {
+                   /* invalid attribute */
+                   mismatch = true;
+                   continue;
+               }
+               /* Skip over cn if present. */
+               if (strncasecmp(attr, "cn=", 3) == 0) {
+                   for (attr += 3; *attr != '\0'; attr++) {
+                       /* Handle escaped ',' chars. */
+                       if (*attr == '\\')
+                           attr++;
+                       if (*attr == ',') {
+                           attr++;
+                           break;
+                       }
+                   }
+               }
+               if (strcasecmp(attr, sudoers_base) != 0) {
+                   /* Doesn't match base, skip the rest of it. */
+                   mismatch = true;
+                   continue;
+               }
+           }
+       } else if (strncmp(line, "objectClass:", 12) == 0) {
+           attr = ldif_parse_attribute(line + 12);
+           if (attr != NULL && strcmp(attr, "sudoRole") == 0)
+               in_role = true;
+       }
+
+       /* Not in a sudoRole, keep reading. */
+       if (!in_role)
+           continue;
+
+       /* Part of a sudoRole, parse it. */
+       if (strncmp(line, "cn:", 3) == 0) {
+           attr = ldif_parse_attribute(line + 3);
+           if (attr != NULL) {
+               free(role->cn);
+               role->cn = unquote_cn(attr);
+               if (role->cn == NULL) {
+                   sudo_fatalx(U_("%s: %s"), __func__,
+                       U_("unable to allocate memory"));
+               }
+           }
+       } else if (strncmp(line, "sudoUser:", 9) == 0) {
+           attr = ldif_parse_attribute(line + 9);
+           if (attr != NULL)
+               ldif_store_string(attr, role->users, true);
+       } else if (strncmp(line, "sudoHost:", 9) == 0) {
+           attr = ldif_parse_attribute(line + 9);
+           if (attr != NULL)
+               ldif_store_string(attr, role->hosts, true);
+       } else if (strncmp(line, "sudoRunAs:", 10) == 0) {
+           attr = ldif_parse_attribute(line + 10);
+           if (attr != NULL)
+               ldif_store_string(attr, role->runasusers, true);
+       } else if (strncmp(line, "sudoRunAsUser:", 14) == 0) {
+           attr = ldif_parse_attribute(line + 14);
+           if (attr != NULL)
+               ldif_store_string(attr, role->runasusers, true);
+       } else if (strncmp(line, "sudoRunAsGroup:", 15) == 0) {
+           attr = ldif_parse_attribute(line + 15);
+           if (attr != NULL)
+               ldif_store_string(attr, role->runasgroups, true);
+       } else if (strncmp(line, "sudoCommand:", 12) == 0) {
+           attr = ldif_parse_attribute(line + 12);
+           if (attr != NULL)
+               ldif_store_string(attr, role->cmnds, false);
+       } else if (strncmp(line, "sudoOption:", 11) == 0) {
+           attr = ldif_parse_attribute(line + 11);
+           if (attr != NULL)
+               ldif_store_string(attr, role->options, false);
+       } else if (strncmp(line, "sudoOrder:", 10) == 0) {
+           char *ep;
+           attr = ldif_parse_attribute(line + 10);
+           if (attr != NULL) {
+               role->order = strtod(attr, &ep);
+               if (ep == attr || *ep != '\0')
+                   sudo_warnx(U_("invalid sudoOrder attribute: %s"), attr);
+           }
+       } else if (strncmp(line, "sudoNotBefore:", 14) == 0) {
+           attr = ldif_parse_attribute(line + 14);
+           if (attr != NULL) {
+               free(role->notbefore);
+               role->notbefore = strdup(attr);
+               if (role->notbefore == NULL) {
+                   sudo_fatalx(U_("%s: %s"), __func__,
+                       U_("unable to allocate memory"));
+               }
+           }
+       } else if (strncmp(line, "sudoNotAfter:", 13) == 0) {
+           attr = ldif_parse_attribute(line + 13);
+           if (attr != NULL) {
+               free(role->notafter);
+               role->notafter = strdup(attr);
+               if (role->notafter == NULL) {
+                   sudo_fatalx(U_("%s: %s"), __func__,
+                       U_("unable to allocate memory"));
+               }
+           }
+       }
+    }
+    sudo_role_free(role);
+    free(line);
+
+    /* Convert from roles to sudoers data structures. */
+    ldif_to_sudoers(parse_tree, &roles, numroles, store_options);
+
+    /* Clean up. */
+    rbdestroy(usercache, str_list_free);
+    rbdestroy(groupcache, str_list_free);
+    rbdestroy(hostcache, str_list_free);
+
+    if (fp != stdin)
+       fclose(fp);
+
+    debug_return_bool(true);
+}