Coverage Report

Created: 2026-10-10 06:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
74.8k
{
53
74.8k
    debug_decl(sudo_role_free, SUDOERS_DEBUG_UTIL);
54
55
74.8k
    if (role != NULL) {
56
70.2k
  free(role->cn);
57
70.2k
  free(role->notbefore);
58
70.2k
  free(role->notafter);
59
70.2k
  str_list_free(role->cmnds);
60
70.2k
  str_list_free(role->hosts);
61
70.2k
  str_list_free(role->users);
62
70.2k
  str_list_free(role->runasusers);
63
70.2k
  str_list_free(role->runasgroups);
64
70.2k
  str_list_free(role->options);
65
70.2k
  free(role);
66
70.2k
    }
67
68
74.8k
    debug_return;
69
74.8k
}
70
71
static struct sudo_role *
72
sudo_role_alloc(void)
73
70.2k
{
74
70.2k
    struct sudo_role *role;
75
70.2k
    debug_decl(sudo_role_alloc, SUDOERS_DEBUG_UTIL);
76
77
70.2k
    role = calloc(1, sizeof(*role));
78
70.2k
    if (role != NULL) {
79
70.2k
  role->cmnds = str_list_alloc();
80
70.2k
  role->hosts = str_list_alloc();
81
70.2k
  role->users = str_list_alloc();
82
70.2k
  role->runasusers = str_list_alloc();
83
70.2k
  role->runasgroups = str_list_alloc();
84
70.2k
  role->options = str_list_alloc();
85
70.2k
  if (role->cmnds == NULL || role->hosts == NULL ||
86
70.2k
      role->users == NULL || role->runasusers == NULL ||
87
70.2k
      role->runasgroups == NULL || role->options == NULL) {
88
0
      sudo_role_free(role);
89
0
      role = NULL;
90
0
  }
91
70.2k
    }
92
93
70.2k
    debug_return_ptr(role);
94
70.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
1.02M
{
104
1.02M
    bool encoded = false;
105
1.02M
    char *attr, *cp, *ep, *colon;
106
1.02M
    size_t i, len;
107
1.02M
    debug_decl(ldif_parse_attribute, SUDOERS_DEBUG_UTIL);
108
109
    /* Parse attribute name: [a-zA-Z][a-zA-Z0-9-]*: */
110
1.02M
    if (!isalpha((unsigned char)*line))
111
313k
  debug_return_bool(false);
112
6.96M
    for (cp = line + 1; *cp != ':' && *cp != '\0'; cp++) {
113
6.27M
  if (!isalnum((unsigned char)*cp) && *cp != '-')
114
28.0k
      debug_return_bool(false);
115
6.27M
    }
116
686k
    if (*cp != ':')
117
29.1k
  debug_return_bool(false);
118
657k
    colon = cp++;
119
120
    /* Check for foo:: base64str. */
121
657k
    if (*cp == ':') {
122
3.16k
  encoded = true;
123
3.16k
  cp++;
124
3.16k
    }
125
126
    /* Trim leading and trailing space. */
127
768k
    while (*cp == ' ')
128
110k
  cp++;
129
130
657k
    ep = cp + strlen(cp);
131
660k
    while (ep > cp && ep[-1] == ' ') {
132
3.38k
  ep--;
133
  /* Don't trim escaped trailing space if not base64. */
134
3.38k
  if (!encoded && ep != cp && ep[-1] == '\\')
135
203
      break;
136
3.18k
  *ep = '\0';
137
3.18k
    }
138
139
657k
    attr = cp;
140
657k
    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
3.16k
  char *copy = strdup(attr);
146
3.16k
  if (copy == NULL) {
147
0
      sudo_fatalx(U_("%s: %s"), __func__,
148
0
    U_("unable to allocate memory"));
149
0
  }
150
3.16k
  len = sudo_base64_decode(attr, (unsigned char *)copy, strlen(copy));
151
3.16k
  if (len == (size_t)-1) {
152
528
      free(copy);
153
528
      debug_return_bool(false);
154
528
  }
155
2.74M
  for (i = 0; i < len; i++) {
156
2.74M
      if (iscntrl((unsigned char)copy[i]) && copy[i] != '\t') {
157
    /* Reject attributes with embedded control characters. */
158
1.27k
    free(copy);
159
1.27k
    debug_return_bool(false);
160
1.27k
      }
161
2.74M
      attr[i] = copy[i];
162
2.74M
  }
163
1.35k
  attr[len] = '\0';
164
1.35k
  free(copy);
165
1.35k
    }
166
167
655k
    *colon = '\0';
168
655k
    *name = line;
169
655k
    *value = attr;
170
171
655k
    debug_return_bool(true);
172
655k
}
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
457k
{
181
457k
    struct sudoers_string *ls;
182
457k
    debug_decl(ldif_store_string, SUDOERS_DEBUG_UTIL);
183
184
457k
    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
457k
    if (!sorted) {
189
250k
  STAILQ_INSERT_TAIL(strlist, ls, entries);
190
250k
    } else {
191
207k
  struct sudoers_string *prev, *next;
192
193
  /* Insertion sort, list is small. */
194
207k
  prev = STAILQ_FIRST(strlist);
195
207k
  if (prev == NULL || strcasecmp(str, prev->str) <= 0) {
196
192k
      STAILQ_INSERT_HEAD(strlist, ls, entries);
197
192k
  } else {
198
33.1k
      while ((next = STAILQ_NEXT(prev, entries)) != NULL) {
199
24.8k
    if (strcasecmp(str, next->str) <= 0)
200
6.39k
        break;
201
18.4k
    prev = next;
202
18.4k
      }
203
14.6k
      STAILQ_INSERT_AFTER(strlist, prev, ls, entries);
204
14.6k
  }
205
207k
    }
206
207
457k
    debug_return;
208
457k
}
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
520k
{
218
520k
    struct sudoers_string *ls = *vp;
219
220
520k
    if (ls == NULL)
221
163k
  return NULL;
222
223
356k
    *vp = STAILQ_NEXT(ls, entries);
224
225
356k
    return ls->str;
226
520k
}
227
228
static int
229
role_order_cmp(const void *va, const void *vb)
230
334k
{
231
334k
    const struct sudo_role *a = *(const struct sudo_role **)va;
232
334k
    const struct sudo_role *b = *(const struct sudo_role **)vb;
233
334k
    debug_decl(role_order_cmp, SUDOERS_DEBUG_LDAP);
234
235
334k
    debug_return_int(a->order < b->order ? -1 :
236
334k
        (a->order > b->order ? 1 : 0));
237
334k
}
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
683
{
246
683
    struct defaults *d;
247
683
    struct sudoers_string *ls;
248
683
    char *var, *val;
249
683
    debug_decl(ldif_store_options, SUDOERS_DEBUG_UTIL);
250
251
2.21k
    STAILQ_FOREACH(ls, options, entries) {
252
2.21k
  if ((d = calloc(1, sizeof(*d))) == NULL ||
253
2.21k
      (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.21k
  TAILQ_INIT(&d->binding->members);
258
2.21k
  d->binding->refcnt = 1;
259
2.21k
  d->type = DEFAULTS;
260
2.21k
  d->op = sudo_ldap_parse_option(ls->str, &var, &val);
261
2.21k
  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.21k
  if (val != NULL) {
266
1.14k
      if ((d->val = strdup(val)) == NULL) {
267
0
    sudo_fatalx(U_("%s: %s"), __func__,
268
0
        U_("unable to allocate memory"));
269
0
      }
270
1.14k
  }
271
2.21k
  TAILQ_INSERT_TAIL(&parse_tree->defaults, d, entries);
272
2.21k
    }
273
683
    debug_return;
274
683
}
275
276
static int
277
str_list_cmp(const void *aa, const void *bb)
278
662k
{
279
662k
    const struct sudoers_str_list *a = aa;
280
662k
    const struct sudoers_str_list *b = bb;
281
662k
    const struct sudoers_string *lsa = STAILQ_FIRST(a);
282
662k
    const struct sudoers_string *lsb = STAILQ_FIRST(b);
283
662k
    int ret;
284
285
910k
    while (lsa != NULL && lsb != NULL) {
286
525k
  if ((ret = strcasecmp(lsa->str, lsb->str)) != 0)
287
278k
      return ret;
288
247k
  lsa = STAILQ_NEXT(lsa, entries);
289
247k
  lsb = STAILQ_NEXT(lsb, entries);
290
247k
    }
291
384k
    return lsa == lsb ? 0 : (lsa == NULL ? -1 : 1);
292
662k
}
293
294
static int
295
str_list_cache(struct rbtree *cache, struct sudoers_str_list **strlistp)
296
245k
{
297
245k
    struct sudoers_str_list *strlist = *strlistp;
298
245k
    struct rbnode *node;
299
245k
    int ret;
300
245k
    debug_decl(str_list_cache, SUDOERS_DEBUG_UTIL);
301
302
245k
    ret = rbinsert(cache, strlist, &node);
303
245k
    switch (ret) {
304
34.5k
    case 0:
305
  /* new entry, take a ref for the cache */
306
34.5k
  strlist->refcnt++;
307
34.5k
  break;
308
210k
    case 1:
309
  /* already exists, use existing and take a ref. */
310
210k
  str_list_free(strlist);
311
210k
  strlist = node->data;
312
210k
  strlist->refcnt++;
313
210k
  *strlistp = strlist;
314
210k
  break;
315
245k
    }
316
245k
    debug_return_int(ret);
317
245k
}
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
61.3k
{
327
61.3k
    struct privilege *priv;
328
61.3k
    struct sudoers_string *ls;
329
61.3k
    struct userspec *us;
330
61.3k
    struct member *m;
331
61.3k
    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
61.3k
    if (reuse_userspec) {
338
  /* Reuse the previous userspec */
339
31.9k
  us = TAILQ_LAST(&parse_tree->userspecs, userspec_list);
340
31.9k
    } else {
341
  /* Allocate a new userspec and fill in the user list. */
342
29.4k
  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
29.4k
  TAILQ_INIT(&us->privileges);
347
29.4k
  TAILQ_INIT(&us->users);
348
29.4k
  STAILQ_INIT(&us->comments);
349
350
42.3k
  STAILQ_FOREACH(ls, role->users, entries) {
351
42.3k
      char *user = ls->str;
352
353
42.3k
      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
42.3k
      m->negated = sudo_ldap_is_negated(&user);
358
42.3k
      switch (*user) {
359
14.9k
      case '\0':
360
    /* Empty RunAsUser means run as the invoking user. */
361
14.9k
    m->type = MYSELF;
362
14.9k
    break;
363
1.53k
      case '+':
364
1.53k
    m->type = NETGROUP;
365
1.53k
    break;
366
3.19k
      case '%':
367
3.19k
    m->type = USERGROUP;
368
3.19k
    break;
369
693
      case 'A':
370
693
    if (strcmp(user, "ALL") == 0) {
371
254
        m->type = ALL;
372
254
        break;
373
254
    }
374
439
    FALLTHROUGH;
375
22.4k
      default:
376
22.4k
    m->type = WORD;
377
22.4k
    break;
378
42.3k
      }
379
42.3k
      if (m->type != ALL && m->type != MYSELF) {
380
27.1k
    if ((m->name = strdup(user)) == NULL) {
381
0
        sudo_fatalx(U_("%s: %s"), __func__,
382
0
      U_("unable to allocate memory"));
383
0
    }
384
27.1k
      }
385
42.3k
      TAILQ_INSERT_TAIL(&us->users, m, entries);
386
42.3k
  }
387
29.4k
    }
388
389
    /* Add source role as a comment. */
390
61.3k
    if (role->cn != NULL) {
391
12.5k
  struct sudoers_comment *comment = NULL;
392
12.5k
  if (reuse_userspec) {
393
      /* Try to reuse comment too. */
394
2.12k
      STAILQ_FOREACH(comment, &us->comments, entries) {
395
1.39k
    if (strncasecmp(comment->str, "sudoRole ", 9) == 0) {
396
1.39k
        char *tmpstr;
397
1.39k
        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.39k
        free(comment->str);
402
1.39k
        comment->str = tmpstr;
403
1.39k
        break;
404
1.39k
    }
405
1.39k
      }
406
2.12k
  }
407
12.5k
  if (comment == NULL) {
408
      /* Create a new comment. */
409
11.2k
      if ((comment = malloc(sizeof(*comment))) == NULL) {
410
0
    sudo_fatalx(U_("%s: %s"), __func__,
411
0
        U_("unable to allocate memory"));
412
0
      }
413
11.2k
      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
11.2k
      STAILQ_INSERT_TAIL(&us->comments, comment, entries);
418
11.2k
  }
419
12.5k
    }
420
421
    /* Convert role to sudoers privilege. */
422
61.3k
    priv = sudo_ldap_role_to_priv(role->cn, STAILQ_FIRST(role->hosts),
423
61.3k
  STAILQ_FIRST(role->runasusers), STAILQ_FIRST(role->runasgroups),
424
61.3k
  STAILQ_FIRST(role->cmnds), STAILQ_FIRST(role->options),
425
61.3k
  role->notbefore, role->notafter, true, store_options,
426
61.3k
  sudoers_string_iter);
427
61.3k
    if (priv == NULL) {
428
0
  sudo_fatalx(U_("%s: %s"), __func__,
429
0
      U_("unable to allocate memory"));
430
0
    }
431
432
61.3k
    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
61.3k
    } else {
462
61.3k
  TAILQ_INSERT_TAIL(&us->privileges, priv, entries);
463
61.3k
    }
464
465
    /* Add finished userspec to the list if new. */
466
61.3k
    if (!reuse_userspec)
467
29.4k
  TAILQ_INSERT_TAIL(&parse_tree->userspecs, us, entries);
468
469
61.3k
    debug_return;
470
61.3k
}
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.81k
{
479
2.81k
    struct sudo_role **role_array, *role = NULL;
480
2.81k
    unsigned int n;
481
2.81k
    debug_decl(ldif_to_sudoers, SUDOERS_DEBUG_UTIL);
482
483
    /* Convert from list of roles to array and sort by order. */
484
2.81k
    role_array = reallocarray(NULL, numroles + 1, sizeof(*role_array));
485
2.81k
    if (role_array == NULL)
486
0
  sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
487
64.1k
    for (n = 0; n < numroles; n++) {
488
61.3k
  if ((role = STAILQ_FIRST(roles)) == NULL)
489
0
      break; /* cannot happen */
490
61.3k
  STAILQ_REMOVE_HEAD(roles, entries);
491
61.3k
  role_array[n] = role;
492
61.3k
    }
493
2.81k
    role_array[n] = NULL;
494
2.81k
    qsort(role_array, numroles, sizeof(*role_array), role_order_cmp);
495
496
    /*
497
     * Iterate over roles in sorted order, converting to sudoers.
498
     */
499
64.1k
    for (n = 0, role = NULL; n < numroles; n++) {
500
61.3k
  bool reuse_userspec = false;
501
61.3k
  bool reuse_privilege = false;
502
61.3k
  bool reuse_runas = false;
503
61.3k
  struct sudo_role *prev_role = role;
504
505
61.3k
  role = role_array[n];
506
507
  /* Check whether we can reuse the previous user and host specs */
508
61.3k
  if (prev_role != NULL && role->users == prev_role->users) {
509
31.9k
      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
31.9k
      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
31.9k
  }
527
528
61.3k
  role_to_sudoers(parse_tree, role, store_options, reuse_userspec,
529
61.3k
      reuse_privilege, reuse_runas);
530
61.3k
    }
531
532
    /* Clean up. */
533
64.1k
    for (n = 0; n < numroles; n++)
534
61.3k
  sudo_role_free(role_array[n]);
535
2.81k
    free(role_array);
536
537
2.81k
    debug_return;
538
2.81k
}
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
19.7k
{
548
19.7k
    char *dst, *new_cn;
549
19.7k
    size_t len;
550
19.7k
    debug_decl(unquote_cn, SUDOERS_DEBUG_UTIL);
551
552
19.7k
    len = strlen(src);
553
19.7k
    if ((new_cn = malloc(len + 1)) == NULL)
554
0
  debug_return_str(NULL);
555
556
5.49M
    for (dst = new_cn; *src != '\0';) {
557
5.47M
  if (src[0] == '\\' && src[1] != '\0')
558
1.71k
      src++;
559
5.47M
  *dst++ = *src++;
560
5.47M
    }
561
19.7k
    *dst = '\0';
562
563
19.7k
    debug_return_str(new_cn);
564
19.7k
}
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.65k
{
575
4.65k
    struct sudo_role_list roles = STAILQ_HEAD_INITIALIZER(roles);
576
4.65k
    struct sudo_role *role = NULL;
577
4.65k
    struct rbtree *usercache, *groupcache, *hostcache;
578
4.65k
    unsigned numroles = 0;
579
4.65k
    bool in_role = false;
580
4.65k
    size_t linesize = 0;
581
4.65k
    char *attr, *name, *line = NULL, *savedline = NULL;
582
4.65k
    size_t savedlen = 0;
583
4.65k
    bool mismatch = false;
584
4.65k
    int errors = 0;
585
4.65k
    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.65k
    usercache = rbcreate(str_list_cmp);
594
4.65k
    groupcache = rbcreate(str_list_cmp);
595
4.65k
    hostcache = rbcreate(str_list_cmp);
596
4.65k
    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
1.12M
    for (;;) {
601
1.12M
  int ch;
602
1.12M
  ssize_t len = getdelim(&line, &linesize, '\n', fp);
603
604
  /* Trim trailing return or newline. */
605
2.38M
  while (len > 0 && (line[len - 1] == '\r' || line[len - 1] == '\n'))
606
1.25M
      line[--len] = '\0';
607
608
  /* Blank line or EOF terminates an entry. */
609
1.12M
  if (len <= 0) {
610
86.2k
      if (in_role) {
611
70.2k
    if (role->cn != NULL && strcasecmp(role->cn, "defaults") == 0) {
612
683
        ldif_store_options(parse_tree, role->options);
613
683
        sudo_role_free(role);
614
69.5k
    } else if (STAILQ_EMPTY(role->users) ||
615
65.8k
        STAILQ_EMPTY(role->hosts) || STAILQ_EMPTY(role->cmnds)) {
616
        /* Incomplete role. */
617
8.20k
        sudo_warnx(U_("ignoring incomplete sudoRole: cn: %s"),
618
8.20k
      role->cn ? role->cn : "UNKNOWN");
619
8.20k
        sudo_role_free(role);
620
61.3k
    } else {
621
        /* Cache users, hosts, runasusers and runasgroups. */
622
61.3k
        if (str_list_cache(usercache, &role->users) == -1 ||
623
61.3k
      str_list_cache(hostcache, &role->hosts) == -1 ||
624
61.3k
      str_list_cache(usercache, &role->runasusers) == -1 ||
625
61.3k
      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
61.3k
        STAILQ_INSERT_TAIL(&roles, role, entries);
632
61.3k
        numroles++;
633
61.3k
    }
634
70.2k
    role = NULL;
635
70.2k
    in_role = false;
636
70.2k
      }
637
86.2k
      if (len == -1) {
638
    /* EOF */
639
4.65k
    break;
640
4.65k
      }
641
81.6k
      mismatch = false;
642
81.6k
      continue;
643
86.2k
  }
644
645
1.04M
  if (savedline != NULL) {
646
993
      char *tmp;
647
648
      /* Append to saved line. */
649
993
      linesize = savedlen + (size_t)len + 1;
650
993
      if ((tmp = realloc(savedline, linesize)) == NULL) {
651
0
    sudo_fatalx(U_("%s: %s"), __func__,
652
0
        U_("unable to allocate memory"));
653
0
      }
654
993
      memcpy(tmp + savedlen, line, (size_t)len + 1);
655
993
      free(line);
656
993
      line = tmp;
657
993
      savedline = NULL;
658
993
  }
659
660
  /* Check for folded line */
661
1.04M
  if ((ch = getc(fp)) == ' ') {
662
      /* folded line, append to the saved portion. */
663
999
      savedlen = (size_t)len;
664
999
      savedline = line;
665
999
      line = NULL;
666
999
      linesize = 0;
667
999
      continue;
668
999
  }
669
1.03M
  ungetc(ch, fp);   /* not folded, push back ch */
670
671
  /* Skip comment lines or records that don't match the base. */
672
1.03M
  if (*line == '#' || mismatch)
673
11.8k
      continue;
674
675
  /* Reject invalid LDIF. */
676
1.02M
  if (!ldif_parse_attribute(line, &name, &attr)) {
677
372k
      sudo_warnx(U_("invalid LDIF attribute: %s"), line);
678
372k
      errors++;
679
372k
      continue;
680
372k
  }
681
682
  /* Parse dn and objectClass. */
683
655k
  if (strcasecmp(name, "dn") == 0) {
684
      /* Compare dn to base, if specified. */
685
6.34k
      if (sudoers_base != NULL) {
686
    /* Skip over cn if present. */
687
6.34k
    if (strncasecmp(attr, "cn=", 3) == 0) {
688
47.2k
        for (attr += 3; *attr != '\0'; attr++) {
689
      /* Handle escaped ',' chars. */
690
46.4k
      if (*attr == '\\' && attr[1] != '\0')
691
650
          attr++;
692
46.4k
      if (*attr == ',') {
693
4.71k
          attr++;
694
4.71k
          break;
695
4.71k
      }
696
46.4k
        }
697
5.53k
    }
698
6.34k
    if (strcasecmp(attr, sudoers_base) != 0) {
699
        /* Doesn't match base, skip the rest of it. */
700
2.34k
        mismatch = true;
701
2.34k
        continue;
702
2.34k
    }
703
6.34k
      }
704
649k
  } else if (strcasecmp(name, "objectClass") == 0) {
705
82.1k
      if (strcasecmp(attr, "sudoRole") == 0) {
706
    /* Allocate new role as needed. */
707
73.1k
    if (role == NULL) {
708
70.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
70.2k
    }
713
73.1k
    in_role = true;
714
73.1k
      }
715
82.1k
  }
716
717
  /* Not in a sudoRole, keep reading. */
718
653k
  if (!in_role)
719
58.4k
      continue;
720
721
  /* Part of a sudoRole, parse it. */
722
594k
  if (strcasecmp(name, "cn") == 0) {
723
19.7k
      free(role->cn);
724
19.7k
      role->cn = unquote_cn(attr);
725
19.7k
      if (role->cn == NULL) {
726
0
    sudo_fatalx(U_("%s: %s"), __func__,
727
0
        U_("unable to allocate memory"));
728
0
      }
729
575k
  } else if (strcasecmp(name, "sudoUser") == 0) {
730
81.7k
      ldif_store_string(attr, role->users, true);
731
493k
  } else if (strcasecmp(name, "sudoHost") == 0) {
732
80.9k
      ldif_store_string(attr, role->hosts, true);
733
412k
  } else if (strcasecmp(name, "sudoRunAs") == 0) {
734
33.9k
      ldif_store_string(attr, role->runasusers, true);
735
378k
  } else if (strcasecmp(name, "sudoRunAsUser") == 0) {
736
6.89k
      ldif_store_string(attr, role->runasusers, true);
737
371k
  } else if (strcasecmp(name, "sudoRunAsGroup") == 0) {
738
3.99k
      ldif_store_string(attr, role->runasgroups, true);
739
367k
  } else if (strcasecmp(name, "sudoCommand") == 0) {
740
78.5k
      ldif_store_string(attr, role->cmnds, false);
741
289k
  } else if (strcasecmp(name, "sudoOption") == 0) {
742
171k
      ldif_store_string(attr, role->options, false);
743
171k
  } else if (strcasecmp(name, "sudoOrder") == 0) {
744
3.62k
      char *ep;
745
3.62k
      role->order = strtod(attr, &ep);
746
3.62k
      if (ep == attr || *ep != '\0') {
747
841
    sudo_warnx(U_("invalid sudoOrder attribute: %s"), attr);
748
841
    errors++;
749
841
      }
750
114k
  } else if (strcasecmp(name, "sudoNotBefore") == 0) {
751
3.84k
      free(role->notbefore);
752
3.84k
      role->notbefore = strdup(attr);
753
3.84k
      if (role->notbefore == NULL) {
754
0
    sudo_fatalx(U_("%s: %s"), __func__,
755
0
        U_("unable to allocate memory"));
756
0
      }
757
110k
  } else if (strcasecmp(name, "sudoNotAfter") == 0) {
758
3.65k
      free(role->notafter);
759
3.65k
      role->notafter = strdup(attr);
760
3.65k
      if (role->notafter == NULL) {
761
0
    sudo_fatalx(U_("%s: %s"), __func__,
762
0
        U_("unable to allocate memory"));
763
0
      }
764
3.65k
  }
765
594k
    }
766
4.65k
    sudo_role_free(role);
767
4.65k
    free(line);
768
4.65k
    free(savedline);
769
770
    /* Convert from roles to sudoers data structures. */
771
4.65k
    if (numroles > 0)
772
2.81k
  ldif_to_sudoers(parse_tree, &roles, numroles, store_options);
773
774
    /* Clean up. */
775
4.65k
    rbdestroy(usercache, str_list_free);
776
4.65k
    rbdestroy(groupcache, str_list_free);
777
4.65k
    rbdestroy(hostcache, str_list_free);
778
779
    debug_return_bool(errors == 0);
780
4.65k
}