/src/sudo/plugins/sudoers/parse_ldif.c
Line | Count | Source |
1 | | /* |
2 | | * SPDX-License-Identifier: ISC |
3 | | * |
4 | | * Copyright (c) 2018-2020 Todd C. Miller <Todd.Miller@sudo.ws> |
5 | | * |
6 | | * Permission to use, copy, modify, and distribute this software for any |
7 | | * purpose with or without fee is hereby granted, provided that the above |
8 | | * copyright notice and this permission notice appear in all copies. |
9 | | * |
10 | | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
11 | | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
12 | | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
13 | | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
14 | | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
15 | | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
16 | | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
17 | | */ |
18 | | |
19 | | #include <config.h> |
20 | | |
21 | | #include <stdio.h> |
22 | | #include <stdlib.h> |
23 | | #include <string.h> |
24 | | #ifdef HAVE_STRINGS_H |
25 | | # include <strings.h> |
26 | | #endif /* HAVE_STRINGS_H */ |
27 | | #include <ctype.h> |
28 | | |
29 | | #include <sudoers.h> |
30 | | #include <sudo_ldap.h> |
31 | | #include <redblack.h> |
32 | | #include <strlist.h> |
33 | | #include <gram.h> |
34 | | |
35 | | struct sudo_role { |
36 | | STAILQ_ENTRY(sudo_role) entries; |
37 | | char *cn; |
38 | | char *notbefore; |
39 | | char *notafter; |
40 | | double order; |
41 | | struct sudoers_str_list *cmnds; |
42 | | struct sudoers_str_list *hosts; |
43 | | struct sudoers_str_list *users; |
44 | | struct sudoers_str_list *runasusers; |
45 | | struct sudoers_str_list *runasgroups; |
46 | | struct sudoers_str_list *options; |
47 | | }; |
48 | | STAILQ_HEAD(sudo_role_list, sudo_role); |
49 | | |
50 | | static void |
51 | | sudo_role_free(struct sudo_role *role) |
52 | 72.8k | { |
53 | 72.8k | debug_decl(sudo_role_free, SUDOERS_DEBUG_UTIL); |
54 | | |
55 | 72.8k | if (role != NULL) { |
56 | 68.2k | free(role->cn); |
57 | 68.2k | free(role->notbefore); |
58 | 68.2k | free(role->notafter); |
59 | 68.2k | str_list_free(role->cmnds); |
60 | 68.2k | str_list_free(role->hosts); |
61 | 68.2k | str_list_free(role->users); |
62 | 68.2k | str_list_free(role->runasusers); |
63 | 68.2k | str_list_free(role->runasgroups); |
64 | 68.2k | str_list_free(role->options); |
65 | 68.2k | free(role); |
66 | 68.2k | } |
67 | | |
68 | 72.8k | debug_return; |
69 | 72.8k | } |
70 | | |
71 | | static struct sudo_role * |
72 | | sudo_role_alloc(void) |
73 | 68.2k | { |
74 | 68.2k | struct sudo_role *role; |
75 | 68.2k | debug_decl(sudo_role_alloc, SUDOERS_DEBUG_UTIL); |
76 | | |
77 | 68.2k | role = calloc(1, sizeof(*role)); |
78 | 68.2k | if (role != NULL) { |
79 | 68.2k | role->cmnds = str_list_alloc(); |
80 | 68.2k | role->hosts = str_list_alloc(); |
81 | 68.2k | role->users = str_list_alloc(); |
82 | 68.2k | role->runasusers = str_list_alloc(); |
83 | 68.2k | role->runasgroups = str_list_alloc(); |
84 | 68.2k | role->options = str_list_alloc(); |
85 | 68.2k | if (role->cmnds == NULL || role->hosts == NULL || |
86 | 68.2k | role->users == NULL || role->runasusers == NULL || |
87 | 68.2k | role->runasgroups == NULL || role->options == NULL) { |
88 | 0 | sudo_role_free(role); |
89 | 0 | role = NULL; |
90 | 0 | } |
91 | 68.2k | } |
92 | | |
93 | 68.2k | debug_return_ptr(role); |
94 | 68.2k | } |
95 | | |
96 | | /* |
97 | | * Parse an LDIF line, filling in attribute name and value. |
98 | | * Modifies line, decodes base64 attribute values if present. |
99 | | * See http://www.faqs.org/rfcs/rfc2849.html |
100 | | */ |
101 | | static bool |
102 | | ldif_parse_attribute(char *line, char **name, char **value) |
103 | 817k | { |
104 | 817k | bool encoded = false; |
105 | 817k | char *attr, *cp, *ep, *colon; |
106 | 817k | size_t i, len; |
107 | 817k | debug_decl(ldif_parse_attribute, SUDOERS_DEBUG_UTIL); |
108 | | |
109 | | /* Parse attribute name: [a-zA-Z][a-zA-Z0-9-]*: */ |
110 | 817k | if (!isalpha((unsigned char)*line)) |
111 | 199k | debug_return_bool(false); |
112 | 6.15M | for (cp = line + 1; *cp != ':' && *cp != '\0'; cp++) { |
113 | 5.56M | if (!isalnum((unsigned char)*cp) && *cp != '-') |
114 | 21.6k | debug_return_bool(false); |
115 | 5.56M | } |
116 | 595k | if (*cp != ':') |
117 | 24.5k | debug_return_bool(false); |
118 | 570k | colon = cp++; |
119 | | |
120 | | /* Check for foo:: base64str. */ |
121 | 570k | if (*cp == ':') { |
122 | 2.06k | encoded = true; |
123 | 2.06k | cp++; |
124 | 2.06k | } |
125 | | |
126 | | /* Trim leading and trailing space. */ |
127 | 661k | while (*cp == ' ') |
128 | 90.6k | cp++; |
129 | | |
130 | 570k | ep = cp + strlen(cp); |
131 | 573k | while (ep > cp && ep[-1] == ' ') { |
132 | 3.37k | ep--; |
133 | | /* Don't trim escaped trailing space if not base64. */ |
134 | 3.37k | if (!encoded && ep != cp && ep[-1] == '\\') |
135 | 373 | break; |
136 | 2.99k | *ep = '\0'; |
137 | 2.99k | } |
138 | | |
139 | 570k | attr = cp; |
140 | 570k | if (encoded) { |
141 | | /* |
142 | | * Decode base64 inline and add NUL-terminator. |
143 | | * The copy allows us to provide a useful message on error. |
144 | | */ |
145 | 2.06k | char *copy = strdup(attr); |
146 | 2.06k | if (copy == NULL) { |
147 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
148 | 0 | U_("unable to allocate memory")); |
149 | 0 | } |
150 | 2.06k | len = sudo_base64_decode(attr, (unsigned char *)copy, strlen(copy)); |
151 | 2.06k | if (len == (size_t)-1) { |
152 | 548 | free(copy); |
153 | 548 | debug_return_bool(false); |
154 | 548 | } |
155 | 2.61M | for (i = 0; i < len; i++) { |
156 | 2.61M | if (iscntrl((unsigned char)copy[i]) && copy[i] != '\t') { |
157 | | /* Reject attributes with embedded control characters. */ |
158 | 260 | free(copy); |
159 | 260 | debug_return_bool(false); |
160 | 260 | } |
161 | 2.61M | attr[i] = copy[i]; |
162 | 2.61M | } |
163 | 1.25k | attr[len] = '\0'; |
164 | 1.25k | free(copy); |
165 | 1.25k | } |
166 | | |
167 | 570k | *colon = '\0'; |
168 | 570k | *name = line; |
169 | 570k | *value = attr; |
170 | | |
171 | 570k | debug_return_bool(true); |
172 | 570k | } |
173 | | |
174 | | /* |
175 | | * Allocate a struct sudoers_string, store str in it and |
176 | | * insert into the specified strlist. |
177 | | */ |
178 | | static void |
179 | | ldif_store_string(const char *str, struct sudoers_str_list *strlist, bool sorted) |
180 | 395k | { |
181 | 395k | struct sudoers_string *ls; |
182 | 395k | debug_decl(ldif_store_string, SUDOERS_DEBUG_UTIL); |
183 | | |
184 | 395k | if ((ls = sudoers_string_alloc(str)) == NULL) { |
185 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
186 | 0 | U_("unable to allocate memory")); |
187 | 0 | } |
188 | 395k | if (!sorted) { |
189 | 199k | STAILQ_INSERT_TAIL(strlist, ls, entries); |
190 | 199k | } else { |
191 | 196k | struct sudoers_string *prev, *next; |
192 | | |
193 | | /* Insertion sort, list is small. */ |
194 | 196k | prev = STAILQ_FIRST(strlist); |
195 | 196k | if (prev == NULL || strcasecmp(str, prev->str) <= 0) { |
196 | 185k | STAILQ_INSERT_HEAD(strlist, ls, entries); |
197 | 185k | } else { |
198 | 27.5k | while ((next = STAILQ_NEXT(prev, entries)) != NULL) { |
199 | 20.9k | if (strcasecmp(str, next->str) <= 0) |
200 | 3.80k | break; |
201 | 17.1k | prev = next; |
202 | 17.1k | } |
203 | 10.4k | STAILQ_INSERT_AFTER(strlist, prev, ls, entries); |
204 | 10.4k | } |
205 | 196k | } |
206 | | |
207 | 395k | debug_return; |
208 | 395k | } |
209 | | |
210 | | /* |
211 | | * Iterator for sudo_ldap_role_to_priv(). |
212 | | * Takes a pointer to a struct sudoers_string *. |
213 | | * Returns the string or NULL if we've reached the end. |
214 | | */ |
215 | | static char * |
216 | | sudoers_string_iter(void **vp) |
217 | 466k | { |
218 | 466k | struct sudoers_string *ls = *vp; |
219 | | |
220 | 466k | if (ls == NULL) |
221 | 161k | return NULL; |
222 | | |
223 | 304k | *vp = STAILQ_NEXT(ls, entries); |
224 | | |
225 | 304k | return ls->str; |
226 | 466k | } |
227 | | |
228 | | static int |
229 | | role_order_cmp(const void *va, const void *vb) |
230 | 336k | { |
231 | 336k | const struct sudo_role *a = *(const struct sudo_role **)va; |
232 | 336k | const struct sudo_role *b = *(const struct sudo_role **)vb; |
233 | 336k | debug_decl(role_order_cmp, SUDOERS_DEBUG_LDAP); |
234 | | |
235 | 336k | debug_return_int(a->order < b->order ? -1 : |
236 | 336k | (a->order > b->order ? 1 : 0)); |
237 | 336k | } |
238 | | |
239 | | /* |
240 | | * Parse list of sudoOption and store in the parse tree's defaults list. |
241 | | */ |
242 | | static void |
243 | | ldif_store_options(struct sudoers_parse_tree *parse_tree, |
244 | | struct sudoers_str_list *options) |
245 | 648 | { |
246 | 648 | struct defaults *d; |
247 | 648 | struct sudoers_string *ls; |
248 | 648 | char *var, *val; |
249 | 648 | debug_decl(ldif_store_options, SUDOERS_DEBUG_UTIL); |
250 | | |
251 | 2.08k | STAILQ_FOREACH(ls, options, entries) { |
252 | 2.08k | if ((d = calloc(1, sizeof(*d))) == NULL || |
253 | 2.08k | (d->binding = malloc(sizeof(*d->binding))) == NULL) { |
254 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
255 | 0 | U_("unable to allocate memory")); |
256 | 0 | } |
257 | 2.08k | TAILQ_INIT(&d->binding->members); |
258 | 2.08k | d->binding->refcnt = 1; |
259 | 2.08k | d->type = DEFAULTS; |
260 | 2.08k | d->op = sudo_ldap_parse_option(ls->str, &var, &val); |
261 | 2.08k | if ((d->var = strdup(var)) == NULL) { |
262 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
263 | 0 | U_("unable to allocate memory")); |
264 | 0 | } |
265 | 2.08k | if (val != NULL) { |
266 | 954 | if ((d->val = strdup(val)) == NULL) { |
267 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
268 | 0 | U_("unable to allocate memory")); |
269 | 0 | } |
270 | 954 | } |
271 | 2.08k | TAILQ_INSERT_TAIL(&parse_tree->defaults, d, entries); |
272 | 2.08k | } |
273 | 648 | debug_return; |
274 | 648 | } |
275 | | |
276 | | static int |
277 | | str_list_cmp(const void *aa, const void *bb) |
278 | 606k | { |
279 | 606k | const struct sudoers_str_list *a = aa; |
280 | 606k | const struct sudoers_str_list *b = bb; |
281 | 606k | const struct sudoers_string *lsa = STAILQ_FIRST(a); |
282 | 606k | const struct sudoers_string *lsb = STAILQ_FIRST(b); |
283 | 606k | int ret; |
284 | | |
285 | 846k | while (lsa != NULL && lsb != NULL) { |
286 | 462k | if ((ret = strcasecmp(lsa->str, lsb->str)) != 0) |
287 | 222k | return ret; |
288 | 239k | lsa = STAILQ_NEXT(lsa, entries); |
289 | 239k | lsb = STAILQ_NEXT(lsb, entries); |
290 | 239k | } |
291 | 384k | return lsa == lsb ? 0 : (lsa == NULL ? -1 : 1); |
292 | 606k | } |
293 | | |
294 | | static int |
295 | | str_list_cache(struct rbtree *cache, struct sudoers_str_list **strlistp) |
296 | 241k | { |
297 | 241k | struct sudoers_str_list *strlist = *strlistp; |
298 | 241k | struct rbnode *node; |
299 | 241k | int ret; |
300 | 241k | debug_decl(str_list_cache, SUDOERS_DEBUG_UTIL); |
301 | | |
302 | 241k | ret = rbinsert(cache, strlist, &node); |
303 | 241k | switch (ret) { |
304 | 36.8k | case 0: |
305 | | /* new entry, take a ref for the cache */ |
306 | 36.8k | strlist->refcnt++; |
307 | 36.8k | break; |
308 | 205k | case 1: |
309 | | /* already exists, use existing and take a ref. */ |
310 | 205k | str_list_free(strlist); |
311 | 205k | strlist = node->data; |
312 | 205k | strlist->refcnt++; |
313 | 205k | *strlistp = strlist; |
314 | 205k | break; |
315 | 241k | } |
316 | 241k | debug_return_int(ret); |
317 | 241k | } |
318 | | |
319 | | /* |
320 | | * Convert a sudoRole to sudoers format and store in the parse tree. |
321 | | */ |
322 | | static void |
323 | | role_to_sudoers(struct sudoers_parse_tree *parse_tree, struct sudo_role *role, |
324 | | bool store_options, bool reuse_userspec, bool reuse_privilege, |
325 | | bool reuse_runas) |
326 | 60.4k | { |
327 | 60.4k | struct privilege *priv; |
328 | 60.4k | struct sudoers_string *ls; |
329 | 60.4k | struct userspec *us; |
330 | 60.4k | struct member *m; |
331 | 60.4k | debug_decl(role_to_sudoers, SUDOERS_DEBUG_UTIL); |
332 | | |
333 | | /* |
334 | | * TODO: use cn to create a UserAlias if multiple users in it? |
335 | | */ |
336 | | |
337 | 60.4k | if (reuse_userspec) { |
338 | | /* Reuse the previous userspec */ |
339 | 34.1k | us = TAILQ_LAST(&parse_tree->userspecs, userspec_list); |
340 | 34.1k | } else { |
341 | | /* Allocate a new userspec and fill in the user list. */ |
342 | 26.3k | if ((us = calloc(1, sizeof(*us))) == NULL) { |
343 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
344 | 0 | U_("unable to allocate memory")); |
345 | 0 | } |
346 | 26.3k | TAILQ_INIT(&us->privileges); |
347 | 26.3k | TAILQ_INIT(&us->users); |
348 | 26.3k | STAILQ_INIT(&us->comments); |
349 | | |
350 | 36.9k | STAILQ_FOREACH(ls, role->users, entries) { |
351 | 36.9k | char *user = ls->str; |
352 | | |
353 | 36.9k | if ((m = calloc(1, sizeof(*m))) == NULL) { |
354 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
355 | 0 | U_("unable to allocate memory")); |
356 | 0 | } |
357 | 36.9k | m->negated = sudo_ldap_is_negated(&user); |
358 | 36.9k | switch (*user) { |
359 | 13.6k | case '\0': |
360 | | /* Empty RunAsUser means run as the invoking user. */ |
361 | 13.6k | m->type = MYSELF; |
362 | 13.6k | break; |
363 | 579 | case '+': |
364 | 579 | m->type = NETGROUP; |
365 | 579 | break; |
366 | 2.72k | case '%': |
367 | 2.72k | m->type = USERGROUP; |
368 | 2.72k | break; |
369 | 745 | case 'A': |
370 | 745 | if (strcmp(user, "ALL") == 0) { |
371 | 328 | m->type = ALL; |
372 | 328 | break; |
373 | 328 | } |
374 | 417 | FALLTHROUGH; |
375 | 19.6k | default: |
376 | 19.6k | m->type = WORD; |
377 | 19.6k | break; |
378 | 36.9k | } |
379 | 36.9k | if (m->type != ALL && m->type != MYSELF) { |
380 | 22.9k | if ((m->name = strdup(user)) == NULL) { |
381 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
382 | 0 | U_("unable to allocate memory")); |
383 | 0 | } |
384 | 22.9k | } |
385 | 36.9k | TAILQ_INSERT_TAIL(&us->users, m, entries); |
386 | 36.9k | } |
387 | 26.3k | } |
388 | | |
389 | | /* Add source role as a comment. */ |
390 | 60.4k | if (role->cn != NULL) { |
391 | 11.0k | struct sudoers_comment *comment = NULL; |
392 | 11.0k | if (reuse_userspec) { |
393 | | /* Try to reuse comment too. */ |
394 | 1.70k | STAILQ_FOREACH(comment, &us->comments, entries) { |
395 | 1.05k | if (strncasecmp(comment->str, "sudoRole ", 9) == 0) { |
396 | 1.05k | char *tmpstr; |
397 | 1.05k | if (asprintf(&tmpstr, "%s, %s", comment->str, role->cn) == -1) { |
398 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
399 | 0 | U_("unable to allocate memory")); |
400 | 0 | } |
401 | 1.05k | free(comment->str); |
402 | 1.05k | comment->str = tmpstr; |
403 | 1.05k | break; |
404 | 1.05k | } |
405 | 1.05k | } |
406 | 1.70k | } |
407 | 11.0k | if (comment == NULL) { |
408 | | /* Create a new comment. */ |
409 | 9.95k | if ((comment = malloc(sizeof(*comment))) == NULL) { |
410 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
411 | 0 | U_("unable to allocate memory")); |
412 | 0 | } |
413 | 9.95k | if (asprintf(&comment->str, "sudoRole %s", role->cn) == -1) { |
414 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
415 | 0 | U_("unable to allocate memory")); |
416 | 0 | } |
417 | 9.95k | STAILQ_INSERT_TAIL(&us->comments, comment, entries); |
418 | 9.95k | } |
419 | 11.0k | } |
420 | | |
421 | | /* Convert role to sudoers privilege. */ |
422 | 60.4k | priv = sudo_ldap_role_to_priv(role->cn, STAILQ_FIRST(role->hosts), |
423 | 60.4k | STAILQ_FIRST(role->runasusers), STAILQ_FIRST(role->runasgroups), |
424 | 60.4k | STAILQ_FIRST(role->cmnds), STAILQ_FIRST(role->options), |
425 | 60.4k | role->notbefore, role->notafter, true, store_options, |
426 | 60.4k | sudoers_string_iter); |
427 | 60.4k | if (priv == NULL) { |
428 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
429 | 0 | U_("unable to allocate memory")); |
430 | 0 | } |
431 | | |
432 | 60.4k | if (reuse_privilege && !TAILQ_EMPTY(&us->privileges)) { |
433 | | /* Hostspec unchanged, append cmndlist to previous privilege. */ |
434 | 0 | struct privilege *prev_priv = TAILQ_LAST(&us->privileges, privilege_list); |
435 | 0 | if (reuse_runas) { |
436 | | /* Runas users and groups same if as in previous privilege. */ |
437 | 0 | struct cmndspec *cmndspec = TAILQ_FIRST(&priv->cmndlist); |
438 | 0 | const struct cmndspec *prev_cmndspec = |
439 | 0 | TAILQ_LAST(&prev_priv->cmndlist, cmndspec_list); |
440 | 0 | struct member_list *runasuserlist = prev_cmndspec->runasuserlist; |
441 | 0 | struct member_list *runasgrouplist = prev_cmndspec->runasgrouplist; |
442 | | |
443 | | /* Free duplicate runas lists. */ |
444 | 0 | if (cmndspec->runasuserlist != NULL) { |
445 | 0 | free_members(cmndspec->runasuserlist); |
446 | 0 | free(cmndspec->runasuserlist); |
447 | 0 | } |
448 | 0 | if (cmndspec->runasgrouplist != NULL) { |
449 | 0 | free_members(cmndspec->runasgrouplist); |
450 | 0 | free(cmndspec->runasgrouplist); |
451 | 0 | } |
452 | | |
453 | | /* Update cmndspec with previous runas lists. */ |
454 | 0 | TAILQ_FOREACH(cmndspec, &priv->cmndlist, entries) { |
455 | 0 | cmndspec->runasuserlist = runasuserlist; |
456 | 0 | cmndspec->runasgrouplist = runasgrouplist; |
457 | 0 | } |
458 | 0 | } |
459 | 0 | TAILQ_CONCAT(&prev_priv->cmndlist, &priv->cmndlist, entries); |
460 | 0 | free_privilege(priv); |
461 | 60.4k | } else { |
462 | 60.4k | TAILQ_INSERT_TAIL(&us->privileges, priv, entries); |
463 | 60.4k | } |
464 | | |
465 | | /* Add finished userspec to the list if new. */ |
466 | 60.4k | if (!reuse_userspec) |
467 | 26.3k | TAILQ_INSERT_TAIL(&parse_tree->userspecs, us, entries); |
468 | | |
469 | 60.4k | debug_return; |
470 | 60.4k | } |
471 | | |
472 | | /* |
473 | | * Convert the list of sudoRoles to sudoers format and store in the parse tree. |
474 | | */ |
475 | | static void |
476 | | ldif_to_sudoers(struct sudoers_parse_tree *parse_tree, |
477 | | struct sudo_role_list *roles, unsigned int numroles, bool store_options) |
478 | 2.82k | { |
479 | 2.82k | struct sudo_role **role_array, *role = NULL; |
480 | 2.82k | unsigned int n; |
481 | 2.82k | debug_decl(ldif_to_sudoers, SUDOERS_DEBUG_UTIL); |
482 | | |
483 | | /* Convert from list of roles to array and sort by order. */ |
484 | 2.82k | role_array = reallocarray(NULL, numroles + 1, sizeof(*role_array)); |
485 | 2.82k | if (role_array == NULL) |
486 | 0 | sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); |
487 | 63.3k | for (n = 0; n < numroles; n++) { |
488 | 60.4k | if ((role = STAILQ_FIRST(roles)) == NULL) |
489 | 0 | break; /* cannot happen */ |
490 | 60.4k | STAILQ_REMOVE_HEAD(roles, entries); |
491 | 60.4k | role_array[n] = role; |
492 | 60.4k | } |
493 | 2.82k | role_array[n] = NULL; |
494 | 2.82k | qsort(role_array, numroles, sizeof(*role_array), role_order_cmp); |
495 | | |
496 | | /* |
497 | | * Iterate over roles in sorted order, converting to sudoers. |
498 | | */ |
499 | 63.3k | for (n = 0, role = NULL; n < numroles; n++) { |
500 | 60.4k | bool reuse_userspec = false; |
501 | 60.4k | bool reuse_privilege = false; |
502 | 60.4k | bool reuse_runas = false; |
503 | 60.4k | struct sudo_role *prev_role = role; |
504 | | |
505 | 60.4k | role = role_array[n]; |
506 | | |
507 | | /* Check whether we can reuse the previous user and host specs */ |
508 | 60.4k | if (prev_role != NULL && role->users == prev_role->users) { |
509 | 34.1k | reuse_userspec = true; |
510 | | |
511 | | /* |
512 | | * Since options are stored per-privilege we can't |
513 | | * append to the previous privilege's cmndlist if |
514 | | * we are storing options. |
515 | | */ |
516 | 34.1k | if (!store_options) { |
517 | 0 | if (role->hosts == prev_role->hosts) { |
518 | 0 | reuse_privilege = true; |
519 | | |
520 | | /* Reuse runasusers and runasgroups if possible. */ |
521 | 0 | if (role->runasusers == prev_role->runasusers && |
522 | 0 | role->runasgroups == prev_role->runasgroups) |
523 | 0 | reuse_runas = true; |
524 | 0 | } |
525 | 0 | } |
526 | 34.1k | } |
527 | | |
528 | 60.4k | role_to_sudoers(parse_tree, role, store_options, reuse_userspec, |
529 | 60.4k | reuse_privilege, reuse_runas); |
530 | 60.4k | } |
531 | | |
532 | | /* Clean up. */ |
533 | 63.3k | for (n = 0; n < numroles; n++) |
534 | 60.4k | sudo_role_free(role_array[n]); |
535 | 2.82k | free(role_array); |
536 | | |
537 | 2.82k | debug_return; |
538 | 2.82k | } |
539 | | |
540 | | /* |
541 | | * Given a cn with possible quoted characters, return a copy of |
542 | | * the cn with quote characters ('\\') removed. |
543 | | * The caller is responsible for freeing the returned string. |
544 | | */ |
545 | | static |
546 | | char *unquote_cn(const char *src) |
547 | 16.0k | { |
548 | 16.0k | char *dst, *new_cn; |
549 | 16.0k | size_t len; |
550 | 16.0k | debug_decl(unquote_cn, SUDOERS_DEBUG_UTIL); |
551 | | |
552 | 16.0k | len = strlen(src); |
553 | 16.0k | if ((new_cn = malloc(len + 1)) == NULL) |
554 | 0 | debug_return_str(NULL); |
555 | | |
556 | 5.79M | for (dst = new_cn; *src != '\0';) { |
557 | 5.77M | if (src[0] == '\\' && src[1] != '\0') |
558 | 1.29k | src++; |
559 | 5.77M | *dst++ = *src++; |
560 | 5.77M | } |
561 | 16.0k | *dst = '\0'; |
562 | | |
563 | 16.0k | debug_return_str(new_cn); |
564 | 16.0k | } |
565 | | |
566 | | /* |
567 | | * Parse a sudoers file in LDIF format, https://tools.ietf.org/html/rfc2849 |
568 | | * Parsed sudoRole objects are stored in the specified parse_tree which |
569 | | * must already be initialized. |
570 | | */ |
571 | | bool |
572 | | sudoers_parse_ldif(struct sudoers_parse_tree *parse_tree, |
573 | | FILE *fp, const char *sudoers_base, bool store_options) |
574 | 4.60k | { |
575 | 4.60k | struct sudo_role_list roles = STAILQ_HEAD_INITIALIZER(roles); |
576 | 4.60k | struct sudo_role *role = NULL; |
577 | 4.60k | struct rbtree *usercache, *groupcache, *hostcache; |
578 | 4.60k | unsigned numroles = 0; |
579 | 4.60k | bool in_role = false; |
580 | 4.60k | size_t linesize = 0; |
581 | 4.60k | char *attr, *name, *line = NULL, *savedline = NULL; |
582 | 4.60k | size_t savedlen = 0; |
583 | 4.60k | bool mismatch = false; |
584 | 4.60k | int errors = 0; |
585 | 4.60k | debug_decl(sudoers_parse_ldif, SUDOERS_DEBUG_UTIL); |
586 | | |
587 | | /* |
588 | | * We cache user, group and host lists to make it easy to detect when there |
589 | | * are identical lists (simple pointer compare). This makes it possible |
590 | | * to merge multiple sudoRole objects into a single UserSpec and/or |
591 | | * Privilege. The lists are sorted since LDAP order is arbitrary. |
592 | | */ |
593 | 4.60k | usercache = rbcreate(str_list_cmp); |
594 | 4.60k | groupcache = rbcreate(str_list_cmp); |
595 | 4.60k | hostcache = rbcreate(str_list_cmp); |
596 | 4.60k | if (usercache == NULL || groupcache == NULL || hostcache == NULL) |
597 | 0 | sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); |
598 | | |
599 | | /* Read through input, parsing into sudo_roles and global defaults. */ |
600 | 916k | for (;;) { |
601 | 916k | int ch; |
602 | 916k | ssize_t len = getdelim(&line, &linesize, '\n', fp); |
603 | | |
604 | | /* Trim trailing return or newline. */ |
605 | 1.96M | while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n')) |
606 | 1.04M | line[--len] = '\0'; |
607 | | |
608 | | /* Blank line or EOF terminates an entry. */ |
609 | 916k | if (len <= 0) { |
610 | 83.5k | if (in_role) { |
611 | 68.2k | if (role->cn != NULL && strcasecmp(role->cn, "defaults") == 0) { |
612 | 648 | ldif_store_options(parse_tree, role->options); |
613 | 648 | sudo_role_free(role); |
614 | 67.6k | } else if (STAILQ_EMPTY(role->users) || |
615 | 64.1k | STAILQ_EMPTY(role->hosts) || STAILQ_EMPTY(role->cmnds)) { |
616 | | /* Incomplete role. */ |
617 | 7.14k | sudo_warnx(U_("ignoring incomplete sudoRole: cn: %s"), |
618 | 7.14k | role->cn ? role->cn : "UNKNOWN"); |
619 | 7.14k | sudo_role_free(role); |
620 | 60.4k | } else { |
621 | | /* Cache users, hosts, runasusers and runasgroups. */ |
622 | 60.4k | if (str_list_cache(usercache, &role->users) == -1 || |
623 | 60.4k | str_list_cache(hostcache, &role->hosts) == -1 || |
624 | 60.4k | str_list_cache(usercache, &role->runasusers) == -1 || |
625 | 60.4k | str_list_cache(groupcache, &role->runasgroups) == -1) { |
626 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
627 | 0 | U_("unable to allocate memory")); |
628 | 0 | } |
629 | | |
630 | | /* Store finished role. */ |
631 | 60.4k | STAILQ_INSERT_TAIL(&roles, role, entries); |
632 | 60.4k | numroles++; |
633 | 60.4k | } |
634 | 68.2k | role = NULL; |
635 | 68.2k | in_role = false; |
636 | 68.2k | } |
637 | 83.5k | if (len == -1) { |
638 | | /* EOF */ |
639 | 4.60k | break; |
640 | 4.60k | } |
641 | 78.9k | mismatch = false; |
642 | 78.9k | continue; |
643 | 83.5k | } |
644 | | |
645 | 833k | if (savedline != NULL) { |
646 | 787 | char *tmp; |
647 | | |
648 | | /* Append to saved line. */ |
649 | 787 | linesize = savedlen + (size_t)len + 1; |
650 | 787 | if ((tmp = realloc(savedline, linesize)) == NULL) { |
651 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
652 | 0 | U_("unable to allocate memory")); |
653 | 0 | } |
654 | 787 | memcpy(tmp + savedlen, line, (size_t)len + 1); |
655 | 787 | free(line); |
656 | 787 | line = tmp; |
657 | 787 | savedline = NULL; |
658 | 787 | } |
659 | | |
660 | | /* Check for folded line */ |
661 | 833k | if ((ch = getc(fp)) == ' ') { |
662 | | /* folded line, append to the saved portion. */ |
663 | 788 | savedlen = (size_t)len; |
664 | 788 | savedline = line; |
665 | 788 | line = NULL; |
666 | 788 | linesize = 0; |
667 | 788 | continue; |
668 | 788 | } |
669 | 832k | ungetc(ch, fp); /* not folded, push back ch */ |
670 | | |
671 | | /* Skip comment lines or records that don't match the base. */ |
672 | 832k | if (*line == '#' || mismatch) |
673 | 15.2k | continue; |
674 | | |
675 | | /* Reject invalid LDIF. */ |
676 | 817k | if (!ldif_parse_attribute(line, &name, &attr)) { |
677 | 246k | sudo_warnx(U_("invalid LDIF attribute: %s"), line); |
678 | 246k | errors++; |
679 | 246k | continue; |
680 | 246k | } |
681 | | |
682 | | /* Parse dn and objectClass. */ |
683 | 570k | if (strcasecmp(name, "dn") == 0) { |
684 | | /* Compare dn to base, if specified. */ |
685 | 5.95k | if (sudoers_base != NULL) { |
686 | | /* Skip over cn if present. */ |
687 | 5.95k | if (strncasecmp(attr, "cn=", 3) == 0) { |
688 | 36.8k | for (attr += 3; *attr != '\0'; attr++) { |
689 | | /* Handle escaped ',' chars. */ |
690 | 36.0k | if (*attr == '\\' && attr[1] != '\0') |
691 | 450 | attr++; |
692 | 36.0k | if (*attr == ',') { |
693 | 4.34k | attr++; |
694 | 4.34k | break; |
695 | 4.34k | } |
696 | 36.0k | } |
697 | 5.08k | } |
698 | 5.95k | if (strcasecmp(attr, sudoers_base) != 0) { |
699 | | /* Doesn't match base, skip the rest of it. */ |
700 | 2.57k | mismatch = true; |
701 | 2.57k | continue; |
702 | 2.57k | } |
703 | 5.95k | } |
704 | 564k | } else if (strcasecmp(name, "objectClass") == 0) { |
705 | 78.4k | if (strcasecmp(attr, "sudoRole") == 0) { |
706 | | /* Allocate new role as needed. */ |
707 | 70.6k | if (role == NULL) { |
708 | 68.2k | if ((role = sudo_role_alloc()) == NULL) { |
709 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
710 | 0 | U_("unable to allocate memory")); |
711 | 0 | } |
712 | 68.2k | } |
713 | 70.6k | in_role = true; |
714 | 70.6k | } |
715 | 78.4k | } |
716 | | |
717 | | /* Not in a sudoRole, keep reading. */ |
718 | 567k | if (!in_role) |
719 | 48.7k | continue; |
720 | | |
721 | | /* Part of a sudoRole, parse it. */ |
722 | 518k | if (strcasecmp(name, "cn") == 0) { |
723 | 16.0k | free(role->cn); |
724 | 16.0k | role->cn = unquote_cn(attr); |
725 | 16.0k | if (role->cn == NULL) { |
726 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
727 | 0 | U_("unable to allocate memory")); |
728 | 0 | } |
729 | 502k | } else if (strcasecmp(name, "sudoUser") == 0) { |
730 | 76.9k | ldif_store_string(attr, role->users, true); |
731 | 425k | } else if (strcasecmp(name, "sudoHost") == 0) { |
732 | 77.5k | ldif_store_string(attr, role->hosts, true); |
733 | 348k | } else if (strcasecmp(name, "sudoRunAs") == 0) { |
734 | 33.6k | ldif_store_string(attr, role->runasusers, true); |
735 | 314k | } else if (strcasecmp(name, "sudoRunAsUser") == 0) { |
736 | 5.09k | ldif_store_string(attr, role->runasusers, true); |
737 | 309k | } else if (strcasecmp(name, "sudoRunAsGroup") == 0) { |
738 | 2.98k | ldif_store_string(attr, role->runasgroups, true); |
739 | 306k | } else if (strcasecmp(name, "sudoCommand") == 0) { |
740 | 75.3k | ldif_store_string(attr, role->cmnds, false); |
741 | 231k | } else if (strcasecmp(name, "sudoOption") == 0) { |
742 | 124k | ldif_store_string(attr, role->options, false); |
743 | 124k | } else if (strcasecmp(name, "sudoOrder") == 0) { |
744 | 3.20k | char *ep; |
745 | 3.20k | role->order = strtod(attr, &ep); |
746 | 3.20k | if (ep == attr || *ep != '\0') { |
747 | 859 | sudo_warnx(U_("invalid sudoOrder attribute: %s"), attr); |
748 | 859 | errors++; |
749 | 859 | } |
750 | 103k | } else if (strcasecmp(name, "sudoNotBefore") == 0) { |
751 | 3.64k | free(role->notbefore); |
752 | 3.64k | role->notbefore = strdup(attr); |
753 | 3.64k | if (role->notbefore == NULL) { |
754 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
755 | 0 | U_("unable to allocate memory")); |
756 | 0 | } |
757 | 100k | } else if (strcasecmp(name, "sudoNotAfter") == 0) { |
758 | 3.16k | free(role->notafter); |
759 | 3.16k | role->notafter = strdup(attr); |
760 | 3.16k | if (role->notafter == NULL) { |
761 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
762 | 0 | U_("unable to allocate memory")); |
763 | 0 | } |
764 | 3.16k | } |
765 | 518k | } |
766 | 4.60k | sudo_role_free(role); |
767 | 4.60k | free(line); |
768 | 4.60k | free(savedline); |
769 | | |
770 | | /* Convert from roles to sudoers data structures. */ |
771 | 4.60k | if (numroles > 0) |
772 | 2.82k | ldif_to_sudoers(parse_tree, &roles, numroles, store_options); |
773 | | |
774 | | /* Clean up. */ |
775 | 4.60k | rbdestroy(usercache, str_list_free); |
776 | 4.60k | rbdestroy(groupcache, str_list_free); |
777 | 4.60k | rbdestroy(hostcache, str_list_free); |
778 | | |
779 | | debug_return_bool(errors == 0); |
780 | 4.60k | } |