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