2 * Copyright (c) 2018 Todd C. Miller <Todd.Miller@sudo.ws>
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18 * This is an open source non-commercial project. Dear PVS-Studio, please check it.
19 * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
24 #include <sys/types.h>
29 #endif /* HAVE_STRING_H */
32 #endif /* HAVE_STRINGS_H */
35 #include "sudo_ldap.h"
41 STAILQ_ENTRY(sudo_role) entries;
46 struct sudoers_str_list *cmnds;
47 struct sudoers_str_list *hosts;
48 struct sudoers_str_list *users;
49 struct sudoers_str_list *runasusers;
50 struct sudoers_str_list *runasgroups;
51 struct sudoers_str_list *options;
53 STAILQ_HEAD(sudo_role_list, sudo_role);
56 sudo_role_free(struct sudo_role *role)
58 debug_decl(sudo_role_free, SUDOERS_DEBUG_UTIL)
62 free(role->notbefore);
64 str_list_free(role->cmnds);
65 str_list_free(role->hosts);
66 str_list_free(role->users);
67 str_list_free(role->runasusers);
68 str_list_free(role->runasgroups);
69 str_list_free(role->options);
76 static struct sudo_role *
79 struct sudo_role *role;
80 debug_decl(sudo_role_alloc, SUDOERS_DEBUG_UTIL)
82 role = calloc(1, sizeof(*role));
84 role->cmnds = str_list_alloc();
85 role->hosts = str_list_alloc();
86 role->users = str_list_alloc();
87 role->runasusers = str_list_alloc();
88 role->runasgroups = str_list_alloc();
89 role->options = str_list_alloc();
90 if (role->cmnds == NULL || role->hosts == NULL ||
91 role->users == NULL || role->runasusers == NULL ||
92 role->runasgroups == NULL || role->options == NULL) {
98 debug_return_ptr(role);
102 * Parse an LDIF attribute, including base64 support.
103 * See http://www.faqs.org/rfcs/rfc2849.html
106 ldif_parse_attribute(char *str)
108 bool encoded = false;
111 debug_decl(ldif_parse_attribute, SUDOERS_DEBUG_UTIL)
113 /* Check for foo:: base64str. */
119 /* Trim leading and trailing space. */
123 ep = str + strlen(str);
124 while (ep > str && ep[-1] == ' ') {
126 /* Don't trim escaped trailing space if not base64. */
127 if (!encoded && ep != str && ep[-1] == '\\')
135 * Decode base64 inline and add NUL-terminator.
136 * The copy allows us to provide a useful message on error.
138 char *copy = strdup(str);
140 sudo_fatalx(U_("%s: %s"), __func__,
141 U_("unable to allocate memory"));
143 len = base64_decode(copy, (unsigned char *)attr, strlen(attr));
144 if (len == (size_t)-1) {
145 sudo_warnx(U_("ignoring invalid attribute value: %s"), copy);
147 debug_return_str(NULL);
153 debug_return_str(attr);
157 * Allocate a struct sudoers_string, store str in it and
158 * insert into the specified strlist.
161 ldif_store_string(const char *str, struct sudoers_str_list *strlist, bool sorted)
163 struct sudoers_string *ls;
164 debug_decl(ldif_store_string, SUDOERS_DEBUG_UTIL)
166 if ((ls = sudoers_string_alloc(str)) == NULL) {
167 sudo_fatalx(U_("%s: %s"), __func__,
168 U_("unable to allocate memory"));
171 STAILQ_INSERT_TAIL(strlist, ls, entries);
173 struct sudoers_string *prev, *next;
175 /* Insertion sort, list is small. */
176 prev = STAILQ_FIRST(strlist);
177 if (prev == NULL || strcasecmp(str, prev->str) <= 0) {
178 STAILQ_INSERT_HEAD(strlist, ls, entries);
180 while ((next = STAILQ_NEXT(prev, entries)) != NULL) {
181 if (strcasecmp(str, next->str) <= 0)
185 STAILQ_INSERT_AFTER(strlist, prev, ls, entries);
193 * Iterator for sudo_ldap_role_to_priv().
194 * Takes a pointer to a struct sudoers_string *.
195 * Returns the string or NULL if we've reached the end.
198 sudoers_string_iter(void **vp)
200 struct sudoers_string *ls = *vp;
205 *vp = STAILQ_NEXT(ls, entries);
211 role_order_cmp(const void *va, const void *vb)
213 const struct sudo_role *a = *(const struct sudo_role **)va;
214 const struct sudo_role *b = *(const struct sudo_role **)vb;
215 debug_decl(role_order_cmp, SUDOERS_DEBUG_LDAP)
217 debug_return_int(a->order < b->order ? -1 :
218 (a->order > b->order ? 1 : 0));
222 * Parse list of sudoOption and store in the parse tree's defaults list.
225 ldif_store_options(struct sudoers_parse_tree *parse_tree,
226 struct sudoers_str_list *options)
229 struct sudoers_string *ls;
231 debug_decl(ldif_store_options, SUDOERS_DEBUG_UTIL)
233 STAILQ_FOREACH(ls, options, entries) {
234 if ((d = calloc(1, sizeof(*d))) == NULL ||
235 (d->binding = malloc(sizeof(*d->binding))) == NULL) {
236 sudo_fatalx(U_("%s: %s"), __func__,
237 U_("unable to allocate memory"));
239 TAILQ_INIT(d->binding);
241 d->op = sudo_ldap_parse_option(ls->str, &var, &val);
242 if ((d->var = strdup(var)) == NULL) {
243 sudo_fatalx(U_("%s: %s"), __func__,
244 U_("unable to allocate memory"));
247 if ((d->val = strdup(val)) == NULL) {
248 sudo_fatalx(U_("%s: %s"), __func__,
249 U_("unable to allocate memory"));
252 TAILQ_INSERT_TAIL(&parse_tree->defaults, d, entries);
258 str_list_cmp(const void *aa, const void *bb)
260 const struct sudoers_str_list *a = aa;
261 const struct sudoers_str_list *b = bb;
262 const struct sudoers_string *lsa = STAILQ_FIRST(a);
263 const struct sudoers_string *lsb = STAILQ_FIRST(b);
266 while (lsa != NULL && lsb != NULL) {
267 if ((ret = strcmp(lsa->str, lsb->str)) != 0)
269 lsa = STAILQ_NEXT(lsa, entries);
270 lsb = STAILQ_NEXT(lsb, entries);
272 return lsa == lsb ? 0 : (lsa == NULL ? -1 : 1);
276 str_list_cache(struct rbtree *cache, struct sudoers_str_list **strlistp)
278 struct sudoers_str_list *strlist = *strlistp;
281 debug_decl(str_list_cache, SUDOERS_DEBUG_UTIL)
283 ret = rbinsert(cache, strlist, &node);
286 /* new entry, take a ref for the cache */
290 /* already exists, use existing and take a ref. */
291 str_list_free(strlist);
292 strlist = node->data;
297 debug_return_int(ret);
301 * Convert a sudoRole to sudoers format and store in the parse tree.
304 role_to_sudoers(struct sudoers_parse_tree *parse_tree, struct sudo_role *role,
305 bool store_options, bool reuse_userspec, bool reuse_privilege,
308 struct privilege *priv;
309 struct sudoers_string *ls;
312 debug_decl(role_to_sudoers, SUDOERS_DEBUG_UTIL)
315 * TODO: use cn to create a UserAlias if multiple users in it?
318 if (reuse_userspec) {
319 /* Re-use the previous userspec */
320 us = TAILQ_LAST(&parse_tree->userspecs, userspec_list);
322 /* Allocate a new userspec and fill in the user list. */
323 if ((us = calloc(1, sizeof(*us))) == NULL) {
324 sudo_fatalx(U_("%s: %s"), __func__,
325 U_("unable to allocate memory"));
327 TAILQ_INIT(&us->privileges);
328 TAILQ_INIT(&us->users);
329 STAILQ_INIT(&us->comments);
331 STAILQ_FOREACH(ls, role->users, entries) {
332 char *user = ls->str;
334 if ((m = calloc(1, sizeof(*m))) == NULL) {
335 sudo_fatalx(U_("%s: %s"), __func__,
336 U_("unable to allocate memory"));
338 m->negated = sudo_ldap_is_negated(&user);
339 m->name = strdup(user);
340 if (m->name == NULL) {
341 sudo_fatalx(U_("%s: %s"), __func__,
342 U_("unable to allocate memory"));
344 if (strcmp(user, "ALL") == 0) {
346 } else if (*user == '+') {
348 } else if (*user == '%') {
353 TAILQ_INSERT_TAIL(&us->users, m, entries);
357 /* Add source role as a comment. */
358 if (role->cn != NULL) {
359 struct sudoers_comment *comment = NULL;
360 if (reuse_userspec) {
361 /* Try to re-use comment too. */
362 STAILQ_FOREACH(comment, &us->comments, entries) {
363 if (strncmp(comment->str, "sudoRole ", 9) == 0) {
365 if (asprintf(&tmpstr, "%s, %s", comment->str, role->cn) == -1) {
366 sudo_fatalx(U_("%s: %s"), __func__,
367 U_("unable to allocate memory"));
370 comment->str = tmpstr;
375 if (comment == NULL) {
376 /* Create a new comment. */
377 if ((comment = malloc(sizeof(*comment))) == NULL) {
378 sudo_fatalx(U_("%s: %s"), __func__,
379 U_("unable to allocate memory"));
381 if (asprintf(&comment->str, "sudoRole %s", role->cn) == -1) {
382 sudo_fatalx(U_("%s: %s"), __func__,
383 U_("unable to allocate memory"));
385 STAILQ_INSERT_TAIL(&us->comments, comment, entries);
389 /* Convert role to sudoers privilege. */
390 priv = sudo_ldap_role_to_priv(role->cn, STAILQ_FIRST(role->hosts),
391 STAILQ_FIRST(role->runasusers), STAILQ_FIRST(role->runasgroups),
392 STAILQ_FIRST(role->cmnds), STAILQ_FIRST(role->options),
393 role->notbefore, role->notafter, true, store_options,
394 sudoers_string_iter);
396 sudo_fatalx(U_("%s: %s"), __func__,
397 U_("unable to allocate memory"));
400 if (reuse_privilege) {
401 /* Hostspec unchanged, append cmndlist to previous privilege. */
402 struct privilege *prev_priv = TAILQ_LAST(&us->privileges, privilege_list);
404 /* Runas users and groups same if as in previous privilege. */
405 struct member_list *runasuserlist =
406 TAILQ_FIRST(&prev_priv->cmndlist)->runasuserlist;
407 struct member_list *runasgrouplist =
408 TAILQ_FIRST(&prev_priv->cmndlist)->runasgrouplist;
409 struct cmndspec *cmndspec = TAILQ_FIRST(&priv->cmndlist);
411 /* Free duplicate runas lists. */
412 if (cmndspec->runasuserlist != NULL) {
413 free_members(cmndspec->runasuserlist);
414 free(cmndspec->runasuserlist);
416 if (cmndspec->runasgrouplist != NULL) {
417 free_members(cmndspec->runasgrouplist);
418 free(cmndspec->runasgrouplist);
421 /* Update cmndspec with previous runas lists. */
422 TAILQ_FOREACH(cmndspec, &priv->cmndlist, entries) {
423 cmndspec->runasuserlist = runasuserlist;
424 cmndspec->runasgrouplist = runasgrouplist;
427 TAILQ_CONCAT(&prev_priv->cmndlist, &priv->cmndlist, entries);
428 free_privilege(priv);
430 TAILQ_INSERT_TAIL(&us->privileges, priv, entries);
433 /* Add finished userspec to the list if new. */
435 TAILQ_INSERT_TAIL(&parse_tree->userspecs, us, entries);
441 * Convert the list of sudoRoles to sudoers format and store in the parse tree.
444 ldif_to_sudoers(struct sudoers_parse_tree *parse_tree,
445 struct sudo_role_list *roles, unsigned int numroles, bool store_options)
447 struct sudo_role **role_array, *role = NULL;
449 debug_decl(ldif_to_sudoers, SUDOERS_DEBUG_UTIL)
451 /* Convert from list of roles to array and sort by order. */
452 role_array = reallocarray(NULL, numroles + 1, sizeof(*role_array));
453 for (n = 0; n < numroles; n++) {
454 if ((role = STAILQ_FIRST(roles)) == NULL)
455 break; /* cannot happen */
456 STAILQ_REMOVE_HEAD(roles, entries);
457 role_array[n] = role;
459 role_array[n] = NULL;
460 qsort(role_array, numroles, sizeof(*role_array), role_order_cmp);
463 * Iterate over roles in sorted order, converting to sudoers.
465 for (n = 0; n < numroles; n++) {
466 bool reuse_userspec = false;
467 bool reuse_privilege = false;
468 bool reuse_runas = false;
470 role = role_array[n];
472 /* Check whether we can reuse the previous user and host specs */
473 if (n > 0 && role->users == role_array[n - 1]->users) {
474 reuse_userspec = true;
477 * Since options are stored per-privilege we can't
478 * append to the previous privilege's cmndlist if
479 * we are storing options.
481 if (!store_options) {
482 if (role->hosts == role_array[n - 1]->hosts) {
483 reuse_privilege = true;
485 /* Reuse runasusers and runasgroups if possible. */
486 if (role->runasusers == role_array[n - 1]->runasusers &&
487 role->runasgroups == role_array[n - 1]->runasgroups)
493 role_to_sudoers(parse_tree, role, store_options, reuse_userspec,
494 reuse_privilege, reuse_runas);
498 for (n = 0; n < numroles; n++)
499 sudo_role_free(role_array[n]);
506 * Given a cn with possible quoted characters, return a copy of
507 * the cn with quote characters ('\\') removed.
508 * The caller is responsible for freeing the returned string.
511 char *unquote_cn(const char *src)
515 debug_decl(unquote_cn, SUDOERS_DEBUG_UTIL)
518 if ((new_cn = malloc(len + 1)) == NULL)
519 debug_return_str(NULL);
521 for (dst = new_cn; *src != '\0';) {
522 if (src[0] == '\\' && src[1] != '\0')
528 debug_return_str(new_cn);
532 * Parse a sudoers file in LDIF format, https://tools.ietf.org/html/rfc2849
533 * Parsed sudoRole objects are stored in the specified parse_tree which
534 * must already be initialized.
537 sudoers_parse_ldif(struct sudoers_parse_tree *parse_tree,
538 FILE *fp, const char *sudoers_base, bool store_options)
540 struct sudo_role_list roles = STAILQ_HEAD_INITIALIZER(roles);
541 struct sudo_role *role = NULL;
542 struct rbtree *usercache, *groupcache, *hostcache;
543 unsigned numroles = 0;
544 bool in_role = false;
546 char *attr, *line = NULL, *savedline = NULL;
547 ssize_t savedlen = 0;
548 bool mismatch = false;
549 debug_decl(sudoers_parse_ldif, SUDOERS_DEBUG_UTIL)
551 /* Free old contents of the parse tree (if any). */
552 free_parse_tree(parse_tree);
555 * We cache user, group and host lists to make it eay to detect when there
556 * are identical lists (simple pointer compare). This makes it possible
557 * to merge multiplpe sudoRole objects into a single UserSpec and/or
558 * Privilege. The lists are sorted since LDAP order is arbitrary.
560 usercache = rbcreate(str_list_cmp);
561 groupcache = rbcreate(str_list_cmp);
562 hostcache = rbcreate(str_list_cmp);
563 if (usercache == NULL || groupcache == NULL || hostcache == NULL)
564 sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
566 /* Read through input, parsing into sudo_roles and global defaults. */
569 ssize_t len = getdelim(&line, &linesize, '\n', fp);
571 /* Trim trailing return or newline. */
572 while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n'))
575 /* Blank line or EOF terminates an entry. */
578 if (role->cn != NULL && strcmp(role->cn, "defaults") == 0) {
579 ldif_store_options(parse_tree, role->options);
580 sudo_role_free(role);
581 } else if (STAILQ_EMPTY(role->users) ||
582 STAILQ_EMPTY(role->hosts) || STAILQ_EMPTY(role->cmnds)) {
583 /* Incomplete role. */
584 sudo_warnx(U_("ignoring incomplete sudoRole: cn: %s"),
585 role->cn ? role->cn : "UNKNOWN");
586 sudo_role_free(role);
588 /* Cache users, hosts, runasusers and runasgroups. */
589 if (str_list_cache(usercache, &role->users) == -1 ||
590 str_list_cache(hostcache, &role->hosts) == -1 ||
591 str_list_cache(usercache, &role->runasusers) == -1 ||
592 str_list_cache(groupcache, &role->runasgroups) == -1) {
593 sudo_fatalx(U_("%s: %s"), __func__,
594 U_("unable to allocate memory"));
597 /* Store finished role. */
598 STAILQ_INSERT_TAIL(&roles, role, entries);
612 if (savedline != NULL) {
615 /* Append to saved line. */
616 linesize = savedlen + len + 1;
617 if ((tmp = realloc(savedline, linesize)) == NULL) {
618 sudo_fatalx(U_("%s: %s"), __func__,
619 U_("unable to allocate memory"));
621 memcpy(tmp + savedlen, line, len + 1);
626 /* Skip comment lines or records that don't match the base. */
627 if (*line == '#' || mismatch)
631 /* Check for folded line */
632 if ((ch = getc(fp)) == ' ') {
633 /* folded line, append to the saved portion. */
640 /* not folded, push back ch */
644 /* Parse dn and objectClass. */
645 if (strncasecmp(line, "dn:", 3) == 0) {
646 /* Compare dn to base, if specified. */
647 if (sudoers_base != NULL) {
648 attr = ldif_parse_attribute(line + 3);
650 /* invalid attribute */
654 /* Skip over cn if present. */
655 if (strncasecmp(attr, "cn=", 3) == 0) {
656 for (attr += 3; *attr != '\0'; attr++) {
657 /* Handle escaped ',' chars. */
666 if (strcasecmp(attr, sudoers_base) != 0) {
667 /* Doesn't match base, skip the rest of it. */
672 } else if (strncmp(line, "objectClass:", 12) == 0) {
673 attr = ldif_parse_attribute(line + 12);
674 if (attr != NULL && strcmp(attr, "sudoRole") == 0) {
675 /* Allocate new role as needed. */
677 if ((role = sudo_role_alloc()) == NULL) {
678 sudo_fatalx(U_("%s: %s"), __func__,
679 U_("unable to allocate memory"));
686 /* Not in a sudoRole, keep reading. */
690 /* Part of a sudoRole, parse it. */
691 if (strncmp(line, "cn:", 3) == 0) {
692 attr = ldif_parse_attribute(line + 3);
695 role->cn = unquote_cn(attr);
696 if (role->cn == NULL) {
697 sudo_fatalx(U_("%s: %s"), __func__,
698 U_("unable to allocate memory"));
701 } else if (strncmp(line, "sudoUser:", 9) == 0) {
702 attr = ldif_parse_attribute(line + 9);
704 ldif_store_string(attr, role->users, true);
705 } else if (strncmp(line, "sudoHost:", 9) == 0) {
706 attr = ldif_parse_attribute(line + 9);
708 ldif_store_string(attr, role->hosts, true);
709 } else if (strncmp(line, "sudoRunAs:", 10) == 0) {
710 attr = ldif_parse_attribute(line + 10);
712 ldif_store_string(attr, role->runasusers, true);
713 } else if (strncmp(line, "sudoRunAsUser:", 14) == 0) {
714 attr = ldif_parse_attribute(line + 14);
716 ldif_store_string(attr, role->runasusers, true);
717 } else if (strncmp(line, "sudoRunAsGroup:", 15) == 0) {
718 attr = ldif_parse_attribute(line + 15);
720 ldif_store_string(attr, role->runasgroups, true);
721 } else if (strncmp(line, "sudoCommand:", 12) == 0) {
722 attr = ldif_parse_attribute(line + 12);
724 ldif_store_string(attr, role->cmnds, false);
725 } else if (strncmp(line, "sudoOption:", 11) == 0) {
726 attr = ldif_parse_attribute(line + 11);
728 ldif_store_string(attr, role->options, false);
729 } else if (strncmp(line, "sudoOrder:", 10) == 0) {
731 attr = ldif_parse_attribute(line + 10);
733 role->order = strtod(attr, &ep);
734 if (ep == attr || *ep != '\0')
735 sudo_warnx(U_("invalid sudoOrder attribute: %s"), attr);
737 } else if (strncmp(line, "sudoNotBefore:", 14) == 0) {
738 attr = ldif_parse_attribute(line + 14);
740 free(role->notbefore);
741 role->notbefore = strdup(attr);
742 if (role->notbefore == NULL) {
743 sudo_fatalx(U_("%s: %s"), __func__,
744 U_("unable to allocate memory"));
747 } else if (strncmp(line, "sudoNotAfter:", 13) == 0) {
748 attr = ldif_parse_attribute(line + 13);
750 free(role->notafter);
751 role->notafter = strdup(attr);
752 if (role->notafter == NULL) {
753 sudo_fatalx(U_("%s: %s"), __func__,
754 U_("unable to allocate memory"));
759 sudo_role_free(role);
762 /* Convert from roles to sudoers data structures. */
763 ldif_to_sudoers(parse_tree, &roles, numroles, store_options);
766 rbdestroy(usercache, str_list_free);
767 rbdestroy(groupcache, str_list_free);
768 rbdestroy(hostcache, str_list_free);
773 debug_return_bool(true);