1 /*
2 Copyright 2021 Northern.tech AS
3
4 This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5
6 This program is free software; you can redistribute it and/or modify it
7 under the terms of the GNU General Public License as published by the
8 Free Software Foundation; version 3.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
18
19 To the extent this program is licensed as part of the Enterprise
20 versions of CFEngine, the applicable Commercial Open Source License
21 (COSL) may apply to this file if you as a licensee so wish it. See
22 included file COSL.txt.
23 */
24
25
26 #include <platform.h>
27
28 #include "server_access.h"
29 #include "strlist.h"
30 #include "server.h"
31
32 #include <addr_lib.h> /* FuzzySetMatch */
33 #include <string_lib.h> /* StringMatchFull TODO REMOVE */
34 #include <misc_lib.h>
35 #include <file_lib.h>
36 #include <regex.h>
37
38
39 struct acl *paths_acl;
40 struct acl *classes_acl;
41 struct acl *vars_acl;
42 struct acl *literals_acl;
43 struct acl *query_acl;
44 struct acl *bundles_acl;
45 struct acl *roles_acl;
46
47
48 /**
49 * Run this function on every resource (file, class, var etc) access to
50 * grant/deny rights. Currently it checks if:
51 * 1. #ipaddr matches the subnet expression in {admit,deny}_ips
52 * 2. #hostname matches the subdomain expression in {admit,deny}_hostnames
53 * 3. #key is searched as-is in {admit,deny}_keys
54 * 4. #username is searched as-is in {admit,deny}_usernames
55 *
56 * @param #found If not NULL then it returns whether the denial was implicit
57 * (found=false) or explicit (found=true). If not NULL it also
58 * changes the way the ACLs are traversed to a SLOWER mode,
59 * since every entry has to be checked for explicit denial.
60 *
61 * @return Default is false, i.e. deny. If a match is found in #acl->admit.*
62 * then return true, unless a match is also found in #acl->deny.* in
63 * which case return false.
64 *
65 * @TODO preprocess our global ACL the moment a client connects, and store in
66 * ServerConnectionState a list of objects that he can access. That way
67 * only his relevant resources will be stored in e.g. {admit,deny}_paths
68 * lists, and running through these two lists on every file request will
69 * be much faster.
70 */
access_CheckResource(const struct resource_acl * acl,bool * found,const char * ipaddr,const char * hostname,const char * key,const char * username)71 static bool access_CheckResource(const struct resource_acl *acl,
72 bool *found,
73 const char *ipaddr, const char *hostname,
74 const char *key, const char *username)
75 {
76 bool access = false; /* DENY by default */
77 bool have_match = false; /* No matching rule found yet */
78
79 /* First we check for admission, secondly for denial, so that denial takes
80 * precedence. */
81
82 if (!NULL_OR_EMPTY(ipaddr) && acl->admit.ips != NULL)
83 {
84 /* Still using legacy code here, doing linear search over all IPs in
85 * textual representation... too CPU intensive! TODO store the ACL as
86 * one list of struct sockaddr_storage, together with CIDR notation
87 * subnet length.
88 */
89
90 const char *rule = NULL;
91 for (size_t i = 0; i < StrList_Len(acl->admit.ips); i++)
92 {
93 if (FuzzySetMatch(StrList_At(acl->admit.ips, i), ipaddr) == 0 ||
94 /* Legacy regex matching, TODO DEPRECATE */
95 StringMatchFull(StrList_At(acl->admit.ips, i), ipaddr))
96 {
97 rule = StrList_At(acl->admit.ips, i);
98 break;
99 }
100 }
101
102 if (rule != NULL)
103 {
104 Log(LOG_LEVEL_DEBUG,
105 "Admit IP due to rule: %s",
106 rule);
107 access = true;
108 have_match = true;
109 }
110 }
111 if (!access && !NULL_OR_EMPTY(hostname) &&
112 acl->admit.hostnames != NULL)
113 {
114 size_t pos = StrList_SearchLongestPrefix(acl->admit.hostnames,
115 hostname, 0,
116 '.', false);
117
118 /* === Legacy regex matching, slow, TODO DEPRECATE === */
119 if (pos == (size_t) -1)
120 {
121 for (size_t i = 0; i < StrList_Len(acl->admit.hostnames); i++)
122 {
123 if (StringMatchFull(StrList_At(acl->admit.hostnames, i),
124 hostname))
125 {
126 pos = i;
127 break;
128 }
129 }
130 }
131 /* =================================================== */
132
133 if (pos != (size_t) -1)
134 {
135 Log(LOG_LEVEL_DEBUG,
136 "Admit hostname due to rule: %s",
137 StrList_At(acl->admit.hostnames, pos));
138 access = true;
139 have_match = true;
140 }
141 else
142 {
143 Log(LOG_LEVEL_VERBOSE, "Hostname '%s' not admitted", hostname);
144 }
145 }
146 if (!access && !NULL_OR_EMPTY(key) &&
147 acl->admit.keys != NULL)
148 {
149 size_t pos;
150 bool ret = StrList_BinarySearch(acl->admit.keys, key, &pos);
151 if (ret)
152 {
153 Log(LOG_LEVEL_DEBUG,
154 "Admit key due to rule: %s",
155 StrList_At(acl->admit.keys, pos));
156 access = true;
157 have_match = true;
158 }
159 }
160 if (!access && !NULL_OR_EMPTY(username) &&
161 acl->admit.usernames != NULL)
162 {
163 size_t pos;
164 bool ret = StrList_BinarySearch(acl->admit.usernames, username, &pos);
165 if (ret)
166 {
167 Log(LOG_LEVEL_DEBUG,
168 "Admit username due to rule: %s",
169 StrList_At(acl->admit.usernames, pos));
170 access = true;
171 have_match = true;
172 }
173 }
174
175
176 /* An admit rule was not found, and we don't care whether the denial is
177 * explicit or implicit: we can finish now. */
178 if (!access && found == NULL)
179 {
180 assert(!have_match);
181 return false; /* EARLY RETURN! */
182 }
183
184 /* If access has been granted, we might need to deny it based on ACL. */
185 /* Same goes if access has not been granted and "found" is not NULL, in
186 * which case we have to return in "found", whether an explicit denial
187 * rule matched or not. */
188
189 assert((access && have_match) ||
190 (!access && !have_match && found != NULL));
191
192 if ((access || !have_match) &&
193 !NULL_OR_EMPTY(ipaddr) &&
194 acl->deny.ips != NULL)
195 {
196 const char *rule = NULL;
197 for (size_t i = 0; i < StrList_Len(acl->deny.ips); i++)
198 {
199 if (FuzzySetMatch(StrList_At(acl->deny.ips, i), ipaddr) == 0 ||
200 /* Legacy regex matching, TODO DEPRECATE */
201 StringMatchFull(StrList_At(acl->deny.ips, i), ipaddr))
202 {
203 rule = StrList_At(acl->deny.ips, i);
204 break;
205 }
206 }
207
208 if (rule != NULL)
209 {
210 Log(LOG_LEVEL_DEBUG,
211 "Deny IP due to rule: %s",
212 rule);
213 access = false;
214 have_match = true;
215 }
216 }
217 if ((access || !have_match) &&
218 !NULL_OR_EMPTY(hostname) &&
219 acl->deny.hostnames != NULL)
220 {
221 size_t pos = StrList_SearchLongestPrefix(acl->deny.hostnames,
222 hostname, 0,
223 '.', false);
224
225 /* === Legacy regex matching, slow, TODO DEPRECATE === */
226 if (pos == (size_t) -1)
227 {
228 for (size_t i = 0; i < StrList_Len(acl->deny.hostnames); i++)
229 {
230 if (StringMatchFull(StrList_At(acl->deny.hostnames, i),
231 hostname))
232 {
233 pos = i;
234 break;
235 }
236 }
237 }
238 /* =================================================== */
239
240 if (pos != (size_t) -1)
241 {
242 Log(LOG_LEVEL_DEBUG,
243 "Deny hostname due to rule: %s",
244 StrList_At(acl->deny.hostnames, pos));
245 access = false;
246 have_match = true;
247 }
248 }
249 if ((access || !have_match) &&
250 !NULL_OR_EMPTY(key) &&
251 acl->deny.keys != NULL)
252 {
253 size_t pos;
254 bool ret = StrList_BinarySearch(acl->deny.keys, key, &pos);
255 if (ret)
256 {
257 Log(LOG_LEVEL_DEBUG,
258 "Deny key due to rule: %s",
259 StrList_At(acl->deny.keys, pos));
260 access = false;
261 have_match = true;
262 }
263 }
264 if ((access || !have_match) &&
265 !NULL_OR_EMPTY(username) &&
266 acl->deny.usernames != NULL)
267 {
268 size_t pos;
269 bool ret = StrList_BinarySearch(acl->deny.usernames, username, &pos);
270 if (ret)
271 {
272 Log(LOG_LEVEL_DEBUG,
273 "Deny username due to rule: %s",
274 StrList_At(acl->deny.usernames, pos));
275 access = false;
276 have_match = true;
277 }
278 }
279
280 /* We can't have implicit admittance,
281 admittance must always be explicit. */
282 assert(! (access && !have_match));
283
284 if (found != NULL)
285 {
286 *found = have_match;
287 }
288 return access;
289 }
290
291
292 /**
293 * Search #req_path in #acl, if found check its rules. The longest parent
294 * directory of #req_path is searched, or an exact match. Directories *must*
295 * end with FILE_SEPARATOR in the ACL list.
296 *
297 * @return If ACL entry is found, and host is listed in there return
298 * true. Else return false.
299 */
acl_CheckPath(const struct acl * acl,const char * reqpath,const char * ipaddr,const char * hostname,const char * key)300 bool acl_CheckPath(const struct acl *acl, const char *reqpath,
301 const char *ipaddr, const char *hostname,
302 const char *key)
303 {
304 bool access = false; /* Deny access by default */
305 size_t reqpath_len = strlen(reqpath);
306
307 /* CHECK 1: Search for parent directory or exact entry in ACL. */
308 size_t pos = StrList_SearchLongestPrefix(acl->resource_names,
309 reqpath, reqpath_len,
310 FILE_SEPARATOR, true);
311
312 if (pos != (size_t) -1) /* acl entry was found */
313 {
314 const struct resource_acl *racl = &acl->acls[pos];
315 bool ret = access_CheckResource(racl, NULL, ipaddr, hostname, key, NULL);
316 if (ret == true) /* entry found that grants access */
317 {
318 access = true;
319 }
320 Log(LOG_LEVEL_DEBUG,
321 "acl_CheckPath: '%s' found in ACL entry '%s', admit=%s",
322 reqpath, acl->resource_names->list[pos]->str,
323 ret == true ? "true" : "false");
324 }
325
326 /* CHECK 2: replace ACL entry parts with special variables (if applicable),
327 * e.g. turn "/path/to/192.168.1.1.json"
328 * to "/path/to/$(connection.ip).json" */
329 char mangled_path[PATH_MAX];
330 memcpy(mangled_path, reqpath, reqpath_len + 1);
331 size_t mangled_path_len =
332 ReplaceSpecialVariables(mangled_path, sizeof(mangled_path),
333 ipaddr, "$(connection.ip)",
334 hostname, "$(connection.hostname)",
335 key, "$(connection.key)");
336
337 /* If there were special variables replaced */
338 if (mangled_path_len != 0 &&
339 mangled_path_len != (size_t) -1) /* Overflow, TODO handle separately. */
340 {
341 size_t pos2 = StrList_SearchLongestPrefix(acl->resource_names,
342 mangled_path, mangled_path_len,
343 FILE_SEPARATOR, true);
344
345 if (pos2 != (size_t) -1) /* acl entry was found */
346 {
347 /* TODO make sure this match is more specific than the other one. */
348 const struct resource_acl *racl = &acl->acls[pos2];
349 /* Check if the magic strings are allowed or denied. */
350 bool ret =
351 access_CheckResource(racl, NULL,
352 "$(connection.ip)",
353 "$(connection.hostname)",
354 "$(connection.key)", NULL);
355 if (ret == true) /* entry found that grants access */
356 {
357 access = true;
358 }
359 Log(LOG_LEVEL_DEBUG,
360 "acl_CheckPath: '%s' found in ACL entry '%s', admit=%s",
361 mangled_path, acl->resource_names->list[pos2]->str,
362 ret == true ? "true" : "false");
363 }
364 }
365
366 return access;
367 }
368
acl_CheckExact(const struct acl * acl,const char * req_string,const char * ipaddr,const char * hostname,const char * key)369 bool acl_CheckExact(const struct acl *acl, const char *req_string,
370 const char *ipaddr, const char *hostname,
371 const char *key)
372 {
373 bool access = false;
374
375 size_t pos = -1;
376 bool found = StrList_BinarySearch(acl->resource_names, req_string, &pos);
377 if (found)
378 {
379 const struct resource_acl *racl = &acl->acls[pos];
380 bool ret = access_CheckResource(racl, NULL,
381 ipaddr, hostname, key, NULL);
382 if (ret == true) /* entry found that grants access */
383 {
384 access = true;
385 }
386 }
387
388 return access;
389 }
390
391 /**
392 * Go linearly over all the #acl and check every rule if it matches.
393 * ADMIT only if at least one rule matches admit and none matches deny.
394 * DENY if no rule matches OR if at least one matches deny.
395 */
acl_CheckRegex(const struct acl * acl,const char * req_string,const char * ipaddr,const char * hostname,const char * key,const char * username)396 bool acl_CheckRegex(const struct acl *acl, const char *req_string,
397 const char *ipaddr, const char *hostname,
398 const char *key, const char *username)
399 {
400 bool retval = false;
401
402 /* For all ACLs */
403 for (size_t i = 0; i < acl->len; i++)
404 {
405 const char *regex = acl->resource_names->list[i]->str;
406
407 /* Does this ACL matches the req_string? */
408 if (StringMatchFull(regex, req_string))
409 {
410 const struct resource_acl *racl = &acl->acls[i];
411
412 /* Does this ACL apply to this host? */
413 bool found;
414 bool admit = access_CheckResource(racl, &found,
415 ipaddr, hostname, key, username);
416 if (found && !admit)
417 {
418 return false;
419 }
420 else if (found && admit)
421 {
422 retval = true;
423 }
424 else
425 {
426 /* If it's not found, there should be no admittance. */
427 assert(!found);
428 assert(!admit);
429 /* We are not touching retval, because it was possibly found
430 * before and retval has been set to "true". */
431 }
432 }
433 }
434
435 return retval;
436 }
437
438
439 /**
440 * Search the list of resources for the handle. If found return the index of
441 * the resource ACL that corresponds to that handle, else add the handle with
442 * empty ACL, reallocating if necessary. The new handle is inserted in the
443 * proper position to keep the acl->resource_names list sorted.
444 *
445 * @note acl->resource_names list should already be sorted, no problem if all
446 * inserts are done with this function.
447 *
448 * @return the index of the resource_acl corresponding to handle. -1 means
449 * reallocation failed, but existing values are still valid.
450 */
acl_SortedInsert(struct acl ** a,const char * handle)451 size_t acl_SortedInsert(struct acl **a, const char *handle)
452 {
453 assert(handle != NULL);
454
455 struct acl *acl = *a; /* for clarity */
456
457 size_t position = (size_t) -1;
458 bool found = StrList_BinarySearch(acl->resource_names,
459 handle, &position);
460 if (found)
461 {
462 /* Found it, return existing entry. */
463 assert(position < acl->len);
464 return position;
465 }
466
467 /* handle is not in acl, we must insert at the position returned. */
468 assert(position <= acl->len);
469
470 /* 1. Check if reallocation is needed. */
471 if (acl->len == acl->alloc_len)
472 {
473 size_t new_alloc_len = acl->alloc_len * 2;
474 if (new_alloc_len == 0)
475 {
476 new_alloc_len = 1;
477 }
478
479 struct acl *p =
480 realloc(acl, sizeof(*p) + sizeof(*p->acls) * new_alloc_len);
481 if (p == NULL)
482 {
483 return (size_t) -1;
484 }
485
486 acl = p;
487 acl->alloc_len = new_alloc_len;
488 *a = acl; /* Change the caller's variable */
489 }
490
491 /* 2. We now have enough space, so insert the resource at the proper
492 index. */
493 size_t ret = StrList_Insert(&acl->resource_names,
494 handle, position);
495 if (ret == (size_t) -1)
496 {
497 /* realloc() failed but the data structure is still valid. */
498 return (size_t) -1;
499 }
500
501 /* 3. Make room. */
502 memmove(&acl->acls[position + 1], &acl->acls[position],
503 (acl->len - position) * sizeof(acl->acls[position]));
504 acl->len++;
505
506 /* 4. Initialise all ACLs for the resource as empty. */
507 acl->acls[position] = (struct resource_acl) { {0}, {0} }; /* NULL acls <=> empty */
508
509 Log(LOG_LEVEL_DEBUG, "Inserted in ACL position %zu: %s",
510 position, handle);
511
512 assert(acl->len == StrList_Len(acl->resource_names));
513
514 return position;
515 }
516
acl_Free(struct acl * a)517 void acl_Free(struct acl *a)
518 {
519 StrList_Free(&a->resource_names);
520
521 size_t i;
522 for (i = 0; i < a->len; i++)
523 {
524 StrList_Free(&a->acls[i].admit.ips);
525 StrList_Free(&a->acls[i].admit.hostnames);
526 StrList_Free(&a->acls[i].admit.usernames);
527 StrList_Free(&a->acls[i].admit.keys);
528 StrList_Free(&a->acls[i].deny.ips);
529 StrList_Free(&a->acls[i].deny.hostnames);
530 StrList_Free(&a->acls[i].deny.keys);
531 }
532
533 free(a);
534 }
535
acl_Summarise(const struct acl * acl,const char * title)536 void acl_Summarise(const struct acl *acl, const char *title)
537 {
538 assert(acl->len == StrList_Len(acl->resource_names));
539
540 size_t i, j;
541 for (i = 0; i < acl->len; i++)
542 {
543 Log(LOG_LEVEL_VERBOSE, "\t%s: %s",
544 title, StrList_At(acl->resource_names, i));
545
546 const struct resource_acl *racl = &acl->acls[i];
547
548 for (j = 0; j < StrList_Len(racl->admit.ips); j++)
549 {
550 Log(LOG_LEVEL_VERBOSE, "\t\tadmit_ips: %s",
551 StrList_At(racl->admit.ips, j));
552 }
553 for (j = 0; j < StrList_Len(racl->admit.hostnames); j++)
554 {
555 Log(LOG_LEVEL_VERBOSE, "\t\tadmit_hostnames: %s",
556 StrList_At(racl->admit.hostnames, j));
557 }
558 for (j = 0; j < StrList_Len(racl->admit.keys); j++)
559 {
560 Log(LOG_LEVEL_VERBOSE, "\t\tadmit_keys: %s",
561 StrList_At(racl->admit.keys, j));
562 }
563 for (j = 0; j < StrList_Len(racl->deny.ips); j++)
564 {
565 Log(LOG_LEVEL_VERBOSE, "\t\tdeny_ips: %s",
566 StrList_At(racl->deny.ips, j));
567 }
568 for (j = 0; j < StrList_Len(racl->deny.hostnames); j++)
569 {
570 Log(LOG_LEVEL_VERBOSE, "\t\tdeny_hostnames: %s",
571 StrList_At(racl->deny.hostnames, j));
572 }
573 for (j = 0; j < StrList_Len(racl->deny.keys); j++)
574 {
575 Log(LOG_LEVEL_VERBOSE, "\t\tdeny_keys: %s",
576 StrList_At(racl->deny.keys, j));
577 }
578 }
579 }
580