/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 | 99.7k | { |
53 | 99.7k | debug_decl(sudo_role_free, SUDOERS_DEBUG_UTIL); |
54 | | |
55 | 99.7k | if (role != NULL) { |
56 | 95.1k | free(role->cn); |
57 | 95.1k | free(role->notbefore); |
58 | 95.1k | free(role->notafter); |
59 | 95.1k | str_list_free(role->cmnds); |
60 | 95.1k | str_list_free(role->hosts); |
61 | 95.1k | str_list_free(role->users); |
62 | 95.1k | str_list_free(role->runasusers); |
63 | 95.1k | str_list_free(role->runasgroups); |
64 | 95.1k | str_list_free(role->options); |
65 | 95.1k | free(role); |
66 | 95.1k | } |
67 | | |
68 | 99.7k | debug_return; |
69 | 99.7k | } |
70 | | |
71 | | static struct sudo_role * |
72 | | sudo_role_alloc(void) |
73 | 95.1k | { |
74 | 95.1k | struct sudo_role *role; |
75 | 95.1k | debug_decl(sudo_role_alloc, SUDOERS_DEBUG_UTIL); |
76 | | |
77 | 95.1k | role = calloc(1, sizeof(*role)); |
78 | 95.1k | if (role != NULL) { |
79 | 95.1k | role->cmnds = str_list_alloc(); |
80 | 95.1k | role->hosts = str_list_alloc(); |
81 | 95.1k | role->users = str_list_alloc(); |
82 | 95.1k | role->runasusers = str_list_alloc(); |
83 | 95.1k | role->runasgroups = str_list_alloc(); |
84 | 95.1k | role->options = str_list_alloc(); |
85 | 95.1k | if (role->cmnds == NULL || role->hosts == NULL || |
86 | 95.1k | role->users == NULL || role->runasusers == NULL || |
87 | 95.1k | role->runasgroups == NULL || role->options == NULL) { |
88 | 0 | sudo_role_free(role); |
89 | 0 | role = NULL; |
90 | 0 | } |
91 | 95.1k | } |
92 | | |
93 | 95.1k | debug_return_ptr(role); |
94 | 95.1k | } |
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 | 1.49M | { |
104 | 1.49M | bool encoded = false; |
105 | 1.49M | char *attr, *cp, *ep, *colon; |
106 | 1.49M | size_t len; |
107 | 1.49M | debug_decl(ldif_parse_attribute, SUDOERS_DEBUG_UTIL); |
108 | | |
109 | | /* Parse attribute name: [a-zA-Z][a-zA-Z0-9-]*: */ |
110 | 1.49M | if (!isalpha((unsigned char)*line)) |
111 | 701k | debug_return_bool(false); |
112 | 8.10M | for (cp = line + 1; *cp != ':' && *cp != '\0'; cp++) { |
113 | 7.34M | if (!isalnum((unsigned char)*cp) && *cp != '-') |
114 | 32.2k | debug_return_bool(false); |
115 | 7.34M | } |
116 | 760k | if (*cp != ':') |
117 | 34.5k | debug_return_bool(false); |
118 | 726k | colon = cp++; |
119 | | |
120 | | /* Check for foo:: base64str. */ |
121 | 726k | if (*cp == ':') { |
122 | 1.37k | encoded = true; |
123 | 1.37k | cp++; |
124 | 1.37k | } |
125 | | |
126 | | /* Trim leading and trailing space. */ |
127 | 847k | while (*cp == ' ') |
128 | 121k | cp++; |
129 | | |
130 | 726k | ep = cp + strlen(cp); |
131 | 727k | while (ep > cp && ep[-1] == ' ') { |
132 | 1.71k | ep--; |
133 | | /* Don't trim escaped trailing space if not base64. */ |
134 | 1.71k | if (!encoded && ep != cp && ep[-1] == '\\') |
135 | 217 | break; |
136 | 1.49k | *ep = '\0'; |
137 | 1.49k | } |
138 | | |
139 | 726k | attr = cp; |
140 | 726k | 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 | 1.37k | char *copy = strdup(attr); |
146 | 1.37k | if (copy == NULL) { |
147 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
148 | 0 | U_("unable to allocate memory")); |
149 | 0 | } |
150 | 1.37k | len = sudo_base64_decode(attr, (unsigned char *)copy, strlen(copy)); |
151 | 1.37k | if (len == (size_t)-1) { |
152 | 332 | free(copy); |
153 | 332 | debug_return_bool(false); |
154 | 332 | } |
155 | 1.04k | memcpy(attr, copy, len); |
156 | 1.04k | attr[len] = '\0'; |
157 | 1.04k | free(copy); |
158 | 1.04k | } |
159 | | |
160 | 726k | *colon = '\0'; |
161 | 726k | *name = line; |
162 | 726k | *value = attr; |
163 | | |
164 | 726k | debug_return_bool(true); |
165 | 726k | } |
166 | | |
167 | | /* |
168 | | * Allocate a struct sudoers_string, store str in it and |
169 | | * insert into the specified strlist. |
170 | | */ |
171 | | static void |
172 | | ldif_store_string(const char *str, struct sudoers_str_list *strlist, bool sorted) |
173 | 509k | { |
174 | 509k | struct sudoers_string *ls; |
175 | 509k | debug_decl(ldif_store_string, SUDOERS_DEBUG_UTIL); |
176 | | |
177 | 509k | if ((ls = sudoers_string_alloc(str)) == NULL) { |
178 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
179 | 0 | U_("unable to allocate memory")); |
180 | 0 | } |
181 | 509k | if (!sorted) { |
182 | 258k | STAILQ_INSERT_TAIL(strlist, ls, entries); |
183 | 258k | } else { |
184 | 250k | struct sudoers_string *prev, *next; |
185 | | |
186 | | /* Insertion sort, list is small. */ |
187 | 250k | prev = STAILQ_FIRST(strlist); |
188 | 250k | if (prev == NULL || strcasecmp(str, prev->str) <= 0) { |
189 | 236k | STAILQ_INSERT_HEAD(strlist, ls, entries); |
190 | 236k | } else { |
191 | 34.1k | while ((next = STAILQ_NEXT(prev, entries)) != NULL) { |
192 | 25.3k | if (strcasecmp(str, next->str) <= 0) |
193 | 5.55k | break; |
194 | 19.7k | prev = next; |
195 | 19.7k | } |
196 | 14.3k | STAILQ_INSERT_AFTER(strlist, prev, ls, entries); |
197 | 14.3k | } |
198 | 250k | } |
199 | | |
200 | 509k | debug_return; |
201 | 509k | } |
202 | | |
203 | | /* |
204 | | * Iterator for sudo_ldap_role_to_priv(). |
205 | | * Takes a pointer to a struct sudoers_string *. |
206 | | * Returns the string or NULL if we've reached the end. |
207 | | */ |
208 | | static char * |
209 | | sudoers_string_iter(void **vp) |
210 | 590k | { |
211 | 590k | struct sudoers_string *ls = *vp; |
212 | | |
213 | 590k | if (ls == NULL) |
214 | 205k | return NULL; |
215 | | |
216 | 384k | *vp = STAILQ_NEXT(ls, entries); |
217 | | |
218 | 384k | return ls->str; |
219 | 590k | } |
220 | | |
221 | | static int |
222 | | role_order_cmp(const void *va, const void *vb) |
223 | 481k | { |
224 | 481k | const struct sudo_role *a = *(const struct sudo_role **)va; |
225 | 481k | const struct sudo_role *b = *(const struct sudo_role **)vb; |
226 | 481k | debug_decl(role_order_cmp, SUDOERS_DEBUG_LDAP); |
227 | | |
228 | 481k | debug_return_int(a->order < b->order ? -1 : |
229 | 481k | (a->order > b->order ? 1 : 0)); |
230 | 481k | } |
231 | | |
232 | | /* |
233 | | * Parse list of sudoOption and store in the parse tree's defaults list. |
234 | | */ |
235 | | static void |
236 | | ldif_store_options(struct sudoers_parse_tree *parse_tree, |
237 | | struct sudoers_str_list *options) |
238 | 936 | { |
239 | 936 | struct defaults *d; |
240 | 936 | struct sudoers_string *ls; |
241 | 936 | char *var, *val; |
242 | 936 | debug_decl(ldif_store_options, SUDOERS_DEBUG_UTIL); |
243 | | |
244 | 2.13k | STAILQ_FOREACH(ls, options, entries) { |
245 | 2.13k | if ((d = calloc(1, sizeof(*d))) == NULL || |
246 | 2.13k | (d->binding = malloc(sizeof(*d->binding))) == NULL) { |
247 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
248 | 0 | U_("unable to allocate memory")); |
249 | 0 | } |
250 | 2.13k | TAILQ_INIT(&d->binding->members); |
251 | 2.13k | d->binding->refcnt = 1; |
252 | 2.13k | d->type = DEFAULTS; |
253 | 2.13k | d->op = sudo_ldap_parse_option(ls->str, &var, &val); |
254 | 2.13k | if ((d->var = strdup(var)) == NULL) { |
255 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
256 | 0 | U_("unable to allocate memory")); |
257 | 0 | } |
258 | 2.13k | if (val != NULL) { |
259 | 1.07k | if ((d->val = strdup(val)) == NULL) { |
260 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
261 | 0 | U_("unable to allocate memory")); |
262 | 0 | } |
263 | 1.07k | } |
264 | 2.13k | TAILQ_INSERT_TAIL(&parse_tree->defaults, d, entries); |
265 | 2.13k | } |
266 | 936 | debug_return; |
267 | 936 | } |
268 | | |
269 | | static int |
270 | | str_list_cmp(const void *aa, const void *bb) |
271 | 843k | { |
272 | 843k | const struct sudoers_str_list *a = aa; |
273 | 843k | const struct sudoers_str_list *b = bb; |
274 | 843k | const struct sudoers_string *lsa = STAILQ_FIRST(a); |
275 | 843k | const struct sudoers_string *lsb = STAILQ_FIRST(b); |
276 | 843k | int ret; |
277 | | |
278 | 1.16M | while (lsa != NULL && lsb != NULL) { |
279 | 606k | if ((ret = strcasecmp(lsa->str, lsb->str)) != 0) |
280 | 287k | return ret; |
281 | 319k | lsa = STAILQ_NEXT(lsa, entries); |
282 | 319k | lsb = STAILQ_NEXT(lsb, entries); |
283 | 319k | } |
284 | 556k | return lsa == lsb ? 0 : (lsa == NULL ? -1 : 1); |
285 | 843k | } |
286 | | |
287 | | static int |
288 | | str_list_cache(struct rbtree *cache, struct sudoers_str_list **strlistp) |
289 | 337k | { |
290 | 337k | struct sudoers_str_list *strlist = *strlistp; |
291 | 337k | struct rbnode *node; |
292 | 337k | int ret; |
293 | 337k | debug_decl(str_list_cache, SUDOERS_DEBUG_UTIL); |
294 | | |
295 | 337k | ret = rbinsert(cache, strlist, &node); |
296 | 337k | switch (ret) { |
297 | 33.5k | case 0: |
298 | | /* new entry, take a ref for the cache */ |
299 | 33.5k | strlist->refcnt++; |
300 | 33.5k | break; |
301 | 303k | case 1: |
302 | | /* already exists, use existing and take a ref. */ |
303 | 303k | str_list_free(strlist); |
304 | 303k | strlist = node->data; |
305 | 303k | strlist->refcnt++; |
306 | 303k | *strlistp = strlist; |
307 | 303k | break; |
308 | 337k | } |
309 | 337k | debug_return_int(ret); |
310 | 337k | } |
311 | | |
312 | | /* |
313 | | * Convert a sudoRole to sudoers format and store in the parse tree. |
314 | | */ |
315 | | static void |
316 | | role_to_sudoers(struct sudoers_parse_tree *parse_tree, struct sudo_role *role, |
317 | | bool store_options, bool reuse_userspec, bool reuse_privilege, |
318 | | bool reuse_runas) |
319 | 84.3k | { |
320 | 84.3k | struct privilege *priv; |
321 | 84.3k | struct sudoers_string *ls; |
322 | 84.3k | struct userspec *us; |
323 | 84.3k | struct member *m; |
324 | 84.3k | debug_decl(role_to_sudoers, SUDOERS_DEBUG_UTIL); |
325 | | |
326 | | /* |
327 | | * TODO: use cn to create a UserAlias if multiple users in it? |
328 | | */ |
329 | | |
330 | 84.3k | if (reuse_userspec) { |
331 | | /* Reuse the previous userspec */ |
332 | 59.6k | us = TAILQ_LAST(&parse_tree->userspecs, userspec_list); |
333 | 59.6k | } else { |
334 | | /* Allocate a new userspec and fill in the user list. */ |
335 | 24.6k | if ((us = calloc(1, sizeof(*us))) == NULL) { |
336 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
337 | 0 | U_("unable to allocate memory")); |
338 | 0 | } |
339 | 24.6k | TAILQ_INIT(&us->privileges); |
340 | 24.6k | TAILQ_INIT(&us->users); |
341 | 24.6k | STAILQ_INIT(&us->comments); |
342 | | |
343 | 37.2k | STAILQ_FOREACH(ls, role->users, entries) { |
344 | 37.2k | char *user = ls->str; |
345 | | |
346 | 37.2k | if ((m = calloc(1, sizeof(*m))) == NULL) { |
347 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
348 | 0 | U_("unable to allocate memory")); |
349 | 0 | } |
350 | 37.2k | m->negated = sudo_ldap_is_negated(&user); |
351 | 37.2k | switch (*user) { |
352 | 11.3k | case '\0': |
353 | | /* Empty RunAsUser means run as the invoking user. */ |
354 | 11.3k | m->type = MYSELF; |
355 | 11.3k | break; |
356 | 513 | case '+': |
357 | 513 | m->type = NETGROUP; |
358 | 513 | break; |
359 | 3.95k | case '%': |
360 | 3.95k | m->type = USERGROUP; |
361 | 3.95k | break; |
362 | 657 | case 'A': |
363 | 657 | if (strcmp(user, "ALL") == 0) { |
364 | 270 | m->type = ALL; |
365 | 270 | break; |
366 | 270 | } |
367 | 387 | FALLTHROUGH; |
368 | 21.0k | default: |
369 | 21.0k | m->type = WORD; |
370 | 21.0k | break; |
371 | 37.2k | } |
372 | 37.2k | if (m->type != ALL && m->type != MYSELF) { |
373 | 25.5k | if ((m->name = strdup(user)) == NULL) { |
374 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
375 | 0 | U_("unable to allocate memory")); |
376 | 0 | } |
377 | 25.5k | } |
378 | 37.2k | TAILQ_INSERT_TAIL(&us->users, m, entries); |
379 | 37.2k | } |
380 | 24.6k | } |
381 | | |
382 | | /* Add source role as a comment. */ |
383 | 84.3k | if (role->cn != NULL) { |
384 | 7.33k | struct sudoers_comment *comment = NULL; |
385 | 7.33k | if (reuse_userspec) { |
386 | | /* Try to reuse comment too. */ |
387 | 2.01k | STAILQ_FOREACH(comment, &us->comments, entries) { |
388 | 1.20k | if (strncasecmp(comment->str, "sudoRole ", 9) == 0) { |
389 | 1.20k | char *tmpstr; |
390 | 1.20k | if (asprintf(&tmpstr, "%s, %s", comment->str, role->cn) == -1) { |
391 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
392 | 0 | U_("unable to allocate memory")); |
393 | 0 | } |
394 | 1.20k | free(comment->str); |
395 | 1.20k | comment->str = tmpstr; |
396 | 1.20k | break; |
397 | 1.20k | } |
398 | 1.20k | } |
399 | 2.01k | } |
400 | 7.33k | if (comment == NULL) { |
401 | | /* Create a new comment. */ |
402 | 6.13k | if ((comment = malloc(sizeof(*comment))) == NULL) { |
403 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
404 | 0 | U_("unable to allocate memory")); |
405 | 0 | } |
406 | 6.13k | if (asprintf(&comment->str, "sudoRole %s", role->cn) == -1) { |
407 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
408 | 0 | U_("unable to allocate memory")); |
409 | 0 | } |
410 | 6.13k | STAILQ_INSERT_TAIL(&us->comments, comment, entries); |
411 | 6.13k | } |
412 | 7.33k | } |
413 | | |
414 | | /* Convert role to sudoers privilege. */ |
415 | 84.3k | priv = sudo_ldap_role_to_priv(role->cn, STAILQ_FIRST(role->hosts), |
416 | 84.3k | STAILQ_FIRST(role->runasusers), STAILQ_FIRST(role->runasgroups), |
417 | 84.3k | STAILQ_FIRST(role->cmnds), STAILQ_FIRST(role->options), |
418 | 84.3k | role->notbefore, role->notafter, true, store_options, |
419 | 84.3k | sudoers_string_iter); |
420 | 84.3k | if (priv == NULL) { |
421 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
422 | 0 | U_("unable to allocate memory")); |
423 | 0 | } |
424 | | |
425 | 84.3k | if (reuse_privilege && !TAILQ_EMPTY(&us->privileges)) { |
426 | | /* Hostspec unchanged, append cmndlist to previous privilege. */ |
427 | 0 | struct privilege *prev_priv = TAILQ_LAST(&us->privileges, privilege_list); |
428 | 0 | if (reuse_runas) { |
429 | | /* Runas users and groups same if as in previous privilege. */ |
430 | 0 | struct cmndspec *cmndspec = TAILQ_FIRST(&priv->cmndlist); |
431 | 0 | const struct cmndspec *prev_cmndspec = |
432 | 0 | TAILQ_LAST(&prev_priv->cmndlist, cmndspec_list); |
433 | 0 | struct member_list *runasuserlist = prev_cmndspec->runasuserlist; |
434 | 0 | struct member_list *runasgrouplist = prev_cmndspec->runasgrouplist; |
435 | | |
436 | | /* Free duplicate runas lists. */ |
437 | 0 | if (cmndspec->runasuserlist != NULL) { |
438 | 0 | free_members(cmndspec->runasuserlist); |
439 | 0 | free(cmndspec->runasuserlist); |
440 | 0 | } |
441 | 0 | if (cmndspec->runasgrouplist != NULL) { |
442 | 0 | free_members(cmndspec->runasgrouplist); |
443 | 0 | free(cmndspec->runasgrouplist); |
444 | 0 | } |
445 | | |
446 | | /* Update cmndspec with previous runas lists. */ |
447 | 0 | TAILQ_FOREACH(cmndspec, &priv->cmndlist, entries) { |
448 | 0 | cmndspec->runasuserlist = runasuserlist; |
449 | 0 | cmndspec->runasgrouplist = runasgrouplist; |
450 | 0 | } |
451 | 0 | } |
452 | 0 | TAILQ_CONCAT(&prev_priv->cmndlist, &priv->cmndlist, entries); |
453 | 0 | free_privilege(priv); |
454 | 84.3k | } else { |
455 | 84.3k | TAILQ_INSERT_TAIL(&us->privileges, priv, entries); |
456 | 84.3k | } |
457 | | |
458 | | /* Add finished userspec to the list if new. */ |
459 | 84.3k | if (!reuse_userspec) |
460 | 24.6k | TAILQ_INSERT_TAIL(&parse_tree->userspecs, us, entries); |
461 | | |
462 | 84.3k | debug_return; |
463 | 84.3k | } |
464 | | |
465 | | /* |
466 | | * Convert the list of sudoRoles to sudoers format and store in the parse tree. |
467 | | */ |
468 | | static void |
469 | | ldif_to_sudoers(struct sudoers_parse_tree *parse_tree, |
470 | | struct sudo_role_list *roles, unsigned int numroles, bool store_options) |
471 | 2.80k | { |
472 | 2.80k | struct sudo_role **role_array, *role = NULL; |
473 | 2.80k | unsigned int n; |
474 | 2.80k | debug_decl(ldif_to_sudoers, SUDOERS_DEBUG_UTIL); |
475 | | |
476 | | /* Convert from list of roles to array and sort by order. */ |
477 | 2.80k | role_array = reallocarray(NULL, numroles + 1, sizeof(*role_array)); |
478 | 2.80k | if (role_array == NULL) |
479 | 0 | sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); |
480 | 87.1k | for (n = 0; n < numroles; n++) { |
481 | 84.3k | if ((role = STAILQ_FIRST(roles)) == NULL) |
482 | 0 | break; /* cannot happen */ |
483 | 84.3k | STAILQ_REMOVE_HEAD(roles, entries); |
484 | 84.3k | role_array[n] = role; |
485 | 84.3k | } |
486 | 2.80k | role_array[n] = NULL; |
487 | 2.80k | qsort(role_array, numroles, sizeof(*role_array), role_order_cmp); |
488 | | |
489 | | /* |
490 | | * Iterate over roles in sorted order, converting to sudoers. |
491 | | */ |
492 | 87.1k | for (n = 0, role = NULL; n < numroles; n++) { |
493 | 84.3k | bool reuse_userspec = false; |
494 | 84.3k | bool reuse_privilege = false; |
495 | 84.3k | bool reuse_runas = false; |
496 | 84.3k | struct sudo_role *prev_role = role; |
497 | | |
498 | 84.3k | role = role_array[n]; |
499 | | |
500 | | /* Check whether we can reuse the previous user and host specs */ |
501 | 84.3k | if (prev_role != NULL && role->users == prev_role->users) { |
502 | 59.6k | reuse_userspec = true; |
503 | | |
504 | | /* |
505 | | * Since options are stored per-privilege we can't |
506 | | * append to the previous privilege's cmndlist if |
507 | | * we are storing options. |
508 | | */ |
509 | 59.6k | if (!store_options) { |
510 | 0 | if (role->hosts == prev_role->hosts) { |
511 | 0 | reuse_privilege = true; |
512 | | |
513 | | /* Reuse runasusers and runasgroups if possible. */ |
514 | 0 | if (role->runasusers == prev_role->runasusers && |
515 | 0 | role->runasgroups == prev_role->runasgroups) |
516 | 0 | reuse_runas = true; |
517 | 0 | } |
518 | 0 | } |
519 | 59.6k | } |
520 | | |
521 | 84.3k | role_to_sudoers(parse_tree, role, store_options, reuse_userspec, |
522 | 84.3k | reuse_privilege, reuse_runas); |
523 | 84.3k | } |
524 | | |
525 | | /* Clean up. */ |
526 | 87.1k | for (n = 0; n < numroles; n++) |
527 | 84.3k | sudo_role_free(role_array[n]); |
528 | 2.80k | free(role_array); |
529 | | |
530 | 2.80k | debug_return; |
531 | 2.80k | } |
532 | | |
533 | | /* |
534 | | * Given a cn with possible quoted characters, return a copy of |
535 | | * the cn with quote characters ('\\') removed. |
536 | | * The caller is responsible for freeing the returned string. |
537 | | */ |
538 | | static |
539 | | char *unquote_cn(const char *src) |
540 | 14.9k | { |
541 | 14.9k | char *dst, *new_cn; |
542 | 14.9k | size_t len; |
543 | 14.9k | debug_decl(unquote_cn, SUDOERS_DEBUG_UTIL); |
544 | | |
545 | 14.9k | len = strlen(src); |
546 | 14.9k | if ((new_cn = malloc(len + 1)) == NULL) |
547 | 0 | debug_return_str(NULL); |
548 | | |
549 | 1.44M | for (dst = new_cn; *src != '\0';) { |
550 | 1.42M | if (src[0] == '\\' && src[1] != '\0') |
551 | 1.80k | src++; |
552 | 1.42M | *dst++ = *src++; |
553 | 1.42M | } |
554 | 14.9k | *dst = '\0'; |
555 | | |
556 | 14.9k | debug_return_str(new_cn); |
557 | 14.9k | } |
558 | | |
559 | | /* |
560 | | * Parse a sudoers file in LDIF format, https://tools.ietf.org/html/rfc2849 |
561 | | * Parsed sudoRole objects are stored in the specified parse_tree which |
562 | | * must already be initialized. |
563 | | */ |
564 | | bool |
565 | | sudoers_parse_ldif(struct sudoers_parse_tree *parse_tree, |
566 | | FILE *fp, const char *sudoers_base, bool store_options) |
567 | 4.58k | { |
568 | 4.58k | struct sudo_role_list roles = STAILQ_HEAD_INITIALIZER(roles); |
569 | 4.58k | struct sudo_role *role = NULL; |
570 | 4.58k | struct rbtree *usercache, *groupcache, *hostcache; |
571 | 4.58k | unsigned numroles = 0; |
572 | 4.58k | bool in_role = false; |
573 | 4.58k | size_t linesize = 0; |
574 | 4.58k | char *attr, *name, *line = NULL, *savedline = NULL; |
575 | 4.58k | size_t savedlen = 0; |
576 | 4.58k | bool mismatch = false; |
577 | 4.58k | int errors = 0; |
578 | 4.58k | debug_decl(sudoers_parse_ldif, SUDOERS_DEBUG_UTIL); |
579 | | |
580 | | /* |
581 | | * We cache user, group and host lists to make it easy to detect when there |
582 | | * are identical lists (simple pointer compare). This makes it possible |
583 | | * to merge multiple sudoRole objects into a single UserSpec and/or |
584 | | * Privilege. The lists are sorted since LDAP order is arbitrary. |
585 | | */ |
586 | 4.58k | usercache = rbcreate(str_list_cmp); |
587 | 4.58k | groupcache = rbcreate(str_list_cmp); |
588 | 4.58k | hostcache = rbcreate(str_list_cmp); |
589 | 4.58k | if (usercache == NULL || groupcache == NULL || hostcache == NULL) |
590 | 0 | sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); |
591 | | |
592 | | /* Read through input, parsing into sudo_roles and global defaults. */ |
593 | 1.61M | for (;;) { |
594 | 1.61M | int ch; |
595 | 1.61M | ssize_t len = getdelim(&line, &linesize, '\n', fp); |
596 | | |
597 | | /* Trim trailing return or newline. */ |
598 | 3.36M | while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n')) |
599 | 1.74M | line[--len] = '\0'; |
600 | | |
601 | | /* Blank line or EOF terminates an entry. */ |
602 | 1.61M | if (len <= 0) { |
603 | 111k | if (in_role) { |
604 | 95.1k | if (role->cn != NULL && strcasecmp(role->cn, "defaults") == 0) { |
605 | 936 | ldif_store_options(parse_tree, role->options); |
606 | 936 | sudo_role_free(role); |
607 | 94.2k | } else if (STAILQ_EMPTY(role->users) || |
608 | 89.9k | STAILQ_EMPTY(role->hosts) || STAILQ_EMPTY(role->cmnds)) { |
609 | | /* Incomplete role. */ |
610 | 9.87k | sudo_warnx(U_("ignoring incomplete sudoRole: cn: %s"), |
611 | 9.87k | role->cn ? role->cn : "UNKNOWN"); |
612 | 9.87k | sudo_role_free(role); |
613 | 84.3k | } else { |
614 | | /* Cache users, hosts, runasusers and runasgroups. */ |
615 | 84.3k | if (str_list_cache(usercache, &role->users) == -1 || |
616 | 84.3k | str_list_cache(hostcache, &role->hosts) == -1 || |
617 | 84.3k | str_list_cache(usercache, &role->runasusers) == -1 || |
618 | 84.3k | str_list_cache(groupcache, &role->runasgroups) == -1) { |
619 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
620 | 0 | U_("unable to allocate memory")); |
621 | 0 | } |
622 | | |
623 | | /* Store finished role. */ |
624 | 84.3k | STAILQ_INSERT_TAIL(&roles, role, entries); |
625 | 84.3k | numroles++; |
626 | 84.3k | } |
627 | 95.1k | role = NULL; |
628 | 95.1k | in_role = false; |
629 | 95.1k | } |
630 | 111k | if (len == -1) { |
631 | | /* EOF */ |
632 | 4.58k | break; |
633 | 4.58k | } |
634 | 106k | mismatch = false; |
635 | 106k | continue; |
636 | 111k | } |
637 | | |
638 | 1.50M | if (savedline != NULL) { |
639 | 879 | char *tmp; |
640 | | |
641 | | /* Append to saved line. */ |
642 | 879 | linesize = savedlen + (size_t)len + 1; |
643 | 879 | if ((tmp = realloc(savedline, linesize)) == NULL) { |
644 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
645 | 0 | U_("unable to allocate memory")); |
646 | 0 | } |
647 | 879 | memcpy(tmp + savedlen, line, (size_t)len + 1); |
648 | 879 | free(line); |
649 | 879 | line = tmp; |
650 | 879 | savedline = NULL; |
651 | 879 | } |
652 | | |
653 | | /* Check for folded line */ |
654 | 1.50M | if ((ch = getc(fp)) == ' ') { |
655 | | /* folded line, append to the saved portion. */ |
656 | 883 | savedlen = (size_t)len; |
657 | 883 | savedline = line; |
658 | 883 | line = NULL; |
659 | 883 | linesize = 0; |
660 | 883 | continue; |
661 | 883 | } |
662 | 1.50M | ungetc(ch, fp); /* not folded, push back ch */ |
663 | | |
664 | | /* Skip comment lines or records that don't match the base. */ |
665 | 1.50M | if (*line == '#' || mismatch) |
666 | 9.38k | continue; |
667 | | |
668 | | /* Reject invalid LDIF. */ |
669 | 1.49M | if (!ldif_parse_attribute(line, &name, &attr)) { |
670 | 768k | sudo_warnx(U_("invalid LDIF attribute: %s"), line); |
671 | 768k | errors++; |
672 | 768k | continue; |
673 | 768k | } |
674 | | |
675 | | /* Parse dn and objectClass. */ |
676 | 726k | if (strcasecmp(name, "dn") == 0) { |
677 | | /* Compare dn to base, if specified. */ |
678 | 6.56k | if (sudoers_base != NULL) { |
679 | | /* Skip over cn if present. */ |
680 | 6.56k | if (strncasecmp(attr, "cn=", 3) == 0) { |
681 | 40.1k | for (attr += 3; *attr != '\0'; attr++) { |
682 | | /* Handle escaped ',' chars. */ |
683 | 39.3k | if (*attr == '\\' && attr[1] != '\0') |
684 | 759 | attr++; |
685 | 39.3k | if (*attr == ',') { |
686 | 4.97k | attr++; |
687 | 4.97k | break; |
688 | 4.97k | } |
689 | 39.3k | } |
690 | 5.78k | } |
691 | 6.56k | if (strcasecmp(attr, sudoers_base) != 0) { |
692 | | /* Doesn't match base, skip the rest of it. */ |
693 | 2.21k | mismatch = true; |
694 | 2.21k | continue; |
695 | 2.21k | } |
696 | 6.56k | } |
697 | 719k | } else if (strcasecmp(name, "objectClass") == 0) { |
698 | 107k | if (strcasecmp(attr, "sudoRole") == 0) { |
699 | | /* Allocate new role as needed. */ |
700 | 98.0k | if (role == NULL) { |
701 | 95.1k | if ((role = sudo_role_alloc()) == NULL) { |
702 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
703 | 0 | U_("unable to allocate memory")); |
704 | 0 | } |
705 | 95.1k | } |
706 | 98.0k | in_role = true; |
707 | 98.0k | } |
708 | 107k | } |
709 | | |
710 | | /* Not in a sudoRole, keep reading. */ |
711 | 723k | if (!in_role) |
712 | 56.1k | continue; |
713 | | |
714 | | /* Part of a sudoRole, parse it. */ |
715 | 667k | if (strcasecmp(name, "cn") == 0) { |
716 | 14.9k | free(role->cn); |
717 | 14.9k | role->cn = unquote_cn(attr); |
718 | 14.9k | if (role->cn == NULL) { |
719 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
720 | 0 | U_("unable to allocate memory")); |
721 | 0 | } |
722 | 652k | } else if (strcasecmp(name, "sudoUser") == 0) { |
723 | 105k | ldif_store_string(attr, role->users, true); |
724 | 547k | } else if (strcasecmp(name, "sudoHost") == 0) { |
725 | 105k | ldif_store_string(attr, role->hosts, true); |
726 | 442k | } else if (strcasecmp(name, "sudoRunAs") == 0) { |
727 | 28.0k | ldif_store_string(attr, role->runasusers, true); |
728 | 414k | } else if (strcasecmp(name, "sudoRunAsUser") == 0) { |
729 | 8.11k | ldif_store_string(attr, role->runasusers, true); |
730 | 406k | } else if (strcasecmp(name, "sudoRunAsGroup") == 0) { |
731 | 4.24k | ldif_store_string(attr, role->runasgroups, true); |
732 | 402k | } else if (strcasecmp(name, "sudoCommand") == 0) { |
733 | 104k | ldif_store_string(attr, role->cmnds, false); |
734 | 297k | } else if (strcasecmp(name, "sudoOption") == 0) { |
735 | 154k | ldif_store_string(attr, role->options, false); |
736 | 154k | } else if (strcasecmp(name, "sudoOrder") == 0) { |
737 | 3.98k | char *ep; |
738 | 3.98k | role->order = strtod(attr, &ep); |
739 | 3.98k | if (ep == attr || *ep != '\0') { |
740 | 828 | sudo_warnx(U_("invalid sudoOrder attribute: %s"), attr); |
741 | 828 | errors++; |
742 | 828 | } |
743 | 139k | } else if (strcasecmp(name, "sudoNotBefore") == 0) { |
744 | 4.04k | free(role->notbefore); |
745 | 4.04k | role->notbefore = strdup(attr); |
746 | 4.04k | if (role->notbefore == NULL) { |
747 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
748 | 0 | U_("unable to allocate memory")); |
749 | 0 | } |
750 | 135k | } else if (strcasecmp(name, "sudoNotAfter") == 0) { |
751 | 3.44k | free(role->notafter); |
752 | 3.44k | role->notafter = strdup(attr); |
753 | 3.44k | if (role->notafter == NULL) { |
754 | 0 | sudo_fatalx(U_("%s: %s"), __func__, |
755 | 0 | U_("unable to allocate memory")); |
756 | 0 | } |
757 | 3.44k | } |
758 | 667k | } |
759 | 4.58k | sudo_role_free(role); |
760 | 4.58k | free(line); |
761 | 4.58k | free(savedline); |
762 | | |
763 | | /* Convert from roles to sudoers data structures. */ |
764 | 4.58k | if (numroles > 0) |
765 | 2.80k | ldif_to_sudoers(parse_tree, &roles, numroles, store_options); |
766 | | |
767 | | /* Clean up. */ |
768 | 4.58k | rbdestroy(usercache, str_list_free); |
769 | 4.58k | rbdestroy(groupcache, str_list_free); |
770 | 4.58k | rbdestroy(hostcache, str_list_free); |
771 | | |
772 | | debug_return_bool(errors == 0); |
773 | 4.58k | } |