1 /*
2   Licensed to the Apache Software Foundation (ASF) under one
3   or more contributor license agreements.  See the NOTICE file
4   distributed with this work for additional information
5   regarding copyright ownership.  The ASF licenses this file
6   to you under the Apache License, Version 2.0 (the
7   "License"); you may not use this file except in compliance
8   with the License.  You may obtain a copy of the License at
9 
10   http://www.apache.org/licenses/LICENSE-2.0
11 
12   Unless required by applicable law or agreed to in writing, software
13   distributed under the License is distributed on an "AS IS" BASIS,
14   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   See the License for the specific language governing permissions and
16   limitations under the License.
17 */
18 
19 /**
20  * @file config.cc
21  * @brief Access Control Plug-in Configuration.
22  * @see config.h
23  */
24 
25 #include <algorithm> /* find_if */
26 #include <fstream>   /* std::ifstream */
27 #include <getopt.h>  /* getopt_long() */
28 
29 #include "common.h"
30 #include "config.h"
31 
32 static bool
isTrue(const char * arg)33 isTrue(const char *arg)
34 {
35   return (nullptr == arg || 0 == strncasecmp("true", arg, 4) || 0 == strncasecmp("1", arg, 1) || 0 == strncasecmp("yes", arg, 3));
36 }
37 
38 /**
39  * trim from start
40  */
41 static inline std::string &
ltrim(std::string & s)42 ltrim(std::string &s)
43 {
44   s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
45   return s;
46 }
47 
48 /**
49  *  trim from end
50  */
51 static inline std::string &
rtrim(std::string & s)52 rtrim(std::string &s)
53 {
54   s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
55   return s;
56 }
57 
58 /**
59  * trim from both ends
60  */
61 static inline std::string &
trim(std::string & s)62 trim(std::string &s)
63 {
64   return ltrim(rtrim(s));
65 }
66 
67 /**
68  * @brief Rebase a relative path onto the configuration directory.
69  */
70 static String
makeConfigPath(const String & path)71 makeConfigPath(const String &path)
72 {
73   if (path.empty() || path[0] == '/') {
74     return path;
75   }
76 
77   return String(TSConfigDirGet()) + "/" + path;
78 }
79 
80 /**
81  * @brief parse and load a single line (base template - does nothing, see specializations for maps and vectors below)
82  */
83 template <class T>
84 void
loadLine(T & container,const String & line)85 loadLine(T &container, const String &line)
86 {
87 }
88 
89 /**
90  * @brief parse and load a single line into a map.
91  */
92 template <>
93 void
loadLine(StringMap & map,const String & line)94 loadLine<StringMap>(StringMap &map, const String &line)
95 {
96   String key;
97   String value;
98   std::istringstream ss(line);
99   std::getline(ss, key, '=');
100   std::getline(ss, value, '=');
101   trim(key);
102   trim(value);
103   map[key] = value;
104 
105 #ifdef ACCESS_CONTROL_LOG_SECRETS
106   AccessControlDebug("Adding secrets[%s]='%s'", key.c_str(), value.c_str());
107 #endif
108 }
109 
110 /**
111  * @brief parse and load a single line into a vector.
112  */
113 template <>
114 void
loadLine(StringVector & vector,const String & line)115 loadLine<StringVector>(StringVector &vector, const String &line)
116 {
117   String trimmedLine(line);
118   trim(trimmedLine);
119   vector.push_back(trimmedLine);
120 
121 #ifdef ACCESS_CONTROL_LOG_SECRETS
122   AccessControlDebug("Adding secrets[%d]='%s'", (int)(vector.size() - 1), trimmedLine.c_str());
123 #endif
124 }
125 
126 /**
127  * @brief parse and load a secrets into a container (i.e. map or vector).
128  */
129 template <typename T>
130 static bool
load(T & container,const String & filename)131 load(T &container, const String &filename)
132 {
133   String line;
134   String::size_type pos;
135 
136   String path(makeConfigPath(filename));
137 
138   AccessControlDebug("reading file %s", path.c_str());
139 
140   std::ifstream infile;
141   infile.open(path.c_str());
142   if (!infile.is_open()) {
143     AccessControlError("failed to load file '%s'", path.c_str());
144     return false;
145   }
146 
147   while (std::getline(infile, line)) {
148     // Allow #-prefixed comments.
149     pos = line.find_first_of('#');
150     if (pos != String::npos) {
151       line.resize(pos);
152     }
153     if (line.empty()) {
154       continue;
155     }
156 
157     loadLine(container, line);
158   }
159   infile.close();
160 
161   return true;
162 }
163 
164 /**
165  * @brief initializes plugin configuration.
166  * @param argc number of plugin parameters
167  * @param argv plugin parameters
168  */
169 bool
init(int argc,char * argv[])170 AccessControlConfig::init(int argc, char *argv[])
171 {
172   static const struct option longopt[] = {{const_cast<char *>("invalid-syntax-status-code"), optional_argument, nullptr, 'a'},
173                                           {const_cast<char *>("invalid-signature-status-code"), optional_argument, nullptr, 'b'},
174                                           {const_cast<char *>("invalid-timing-status-code"), optional_argument, nullptr, 'c'},
175                                           {const_cast<char *>("invalid-scope-status-code"), optional_argument, nullptr, 'd'},
176                                           {const_cast<char *>("invalid-origin-response"), optional_argument, nullptr, 'e'},
177                                           {const_cast<char *>("internal-error-status-code"), optional_argument, nullptr, 'f'},
178                                           {const_cast<char *>("check-cookie"), optional_argument, nullptr, 'g'},
179                                           {const_cast<char *>("symmetric-keys-map"), optional_argument, nullptr, 'h'},
180                                           {const_cast<char *>("reject-invalid-token-requests"), optional_argument, nullptr, 'i'},
181                                           {const_cast<char *>("extract-subject-to-header"), optional_argument, nullptr, 'j'},
182                                           {const_cast<char *>("extract-tokenid-to-header"), optional_argument, nullptr, 'k'},
183                                           {const_cast<char *>("extract-status-to-header"), optional_argument, nullptr, 'l'},
184                                           {const_cast<char *>("token-response-header"), optional_argument, nullptr, 'm'},
185                                           {const_cast<char *>("use-redirects"), optional_argument, nullptr, 'n'},
186                                           {const_cast<char *>("include-uri-paths-file"), optional_argument, nullptr, 'o'},
187                                           {const_cast<char *>("exclude-uri-paths-file"), optional_argument, nullptr, 'p'},
188                                           {nullptr, 0, nullptr, 0}};
189 
190   bool status = true;
191   optind      = 0;
192 
193   /* argv contains the "to" and "from" URLs. Skip the first so that the second one poses as the program name. */
194   argc--;
195   argv++;
196 
197   for (;;) {
198     int opt;
199     opt = getopt_long(argc, (char *const *)argv, "", longopt, nullptr);
200 
201     if (opt == -1) {
202       break;
203     }
204     AccessControlDebug("processing %s", argv[optind - 1]);
205 
206     switch (opt) {
207     case 'a': /* invalid-syntax-status-code */
208     {
209       _invalidSignature = static_cast<TSHttpStatus>(string2int(optarg));
210     } break;
211     case 'b': /* invalid-signature-status-code */
212     {
213       _invalidSignature = static_cast<TSHttpStatus>(string2int(optarg));
214     } break;
215     case 'c': /* invalid-timing-status-code */
216     {
217       _invalidTiming = static_cast<TSHttpStatus>(string2int(optarg));
218     } break;
219     case 'd': /* invalid-scope-status-code */
220     {
221       _invalidScope = static_cast<TSHttpStatus>(string2int(optarg));
222     } break;
223     case 'e': /* invalid-origin-response */
224     {
225       _invalidOriginResponse = static_cast<TSHttpStatus>(string2int(optarg));
226     } break;
227     case 'f': /* internal-error-status-code */
228     {
229       _internalError = static_cast<TSHttpStatus>(string2int(optarg));
230     } break;
231     case 'g': /* check-cookie */
232     {
233       _cookieName.assign(optarg);
234     } break;
235     case 'h': /* symmetric-keys-map */
236     {
237       load(_symmetricKeysMap, optarg);
238     } break;
239     case 'i': /* reject-invalid-token-requests */
240     {
241       _rejectRequestsWithInvalidTokens = ::isTrue(optarg);
242     } break;
243     case 'j': /* extract-subject-to-header */
244     {
245       _extrSubHdrName.assign(optarg);
246     } break;
247     case 'k': /* extract-tokenid-to-header */
248     {
249       _extrTokenIdHdrName.assign(optarg);
250     } break;
251     case 'l': /* extract-status-to-header */
252     {
253       _extrValidationHdrName.assign(optarg);
254     } break;
255     case 'm': /* token-response-header */
256     {
257       _respTokenHeaderName.assign(optarg);
258     } break;
259     case 'n': /* use-redirects */
260     {
261       _useRedirects = ::isTrue(optarg);
262     } break;
263     case 'o': /* include-uri-paths-file */
264       if (!loadMultiPatternsFromFile(optarg, /* denylist = */ false)) {
265         AccessControlError("failed to load uri-path multi-pattern allow-list '%s'", optarg);
266         status = false;
267       }
268       break;
269     case 'p': /* exclude-uri-paths-file */
270       if (!loadMultiPatternsFromFile(optarg, /* denylist = */ true)) {
271         AccessControlError("failed to load uri-path multi-pattern deny-list '%s'", optarg);
272         status = false;
273       }
274       break;
275 
276     default: {
277       status = false;
278     }
279     }
280   }
281 
282   /* Make sure at least 1 secret source is specified */
283   if (_symmetricKeysMap.empty()) {
284     AccessControlDebug("no secrets' source provided");
285     return false;
286   }
287 
288   /* Support only KeyValuePair syntax for now */
289   _tokenFactory = new AccessTokenFactory(_kvpAccessTokenConfig, _symmetricKeysMap, _debugLevel);
290   if (nullptr == _tokenFactory) {
291     AccessControlDebug("failed to initialize the access token factory");
292     return false;
293   }
294   return status;
295 }
296 
297 /**
298  * @brief a helper function which loads the classifier from files.
299  * @param filename file name
300  * @param denylist true - load as a denylist of patterns, false - allow-list of patterns
301  * @return true if successful, false otherwise.
302  */
303 bool
loadMultiPatternsFromFile(const String & filename,bool denylist)304 AccessControlConfig::loadMultiPatternsFromFile(const String &filename, bool denylist)
305 {
306   if (filename.empty()) {
307     AccessControlError("filename cannot be empty");
308     return false;
309   }
310 
311   String path(makeConfigPath(filename));
312 
313   std::ifstream ifstr;
314   String regex;
315   unsigned lineno = 0;
316 
317   ifstr.open(path.c_str());
318   if (!ifstr) {
319     AccessControlError("failed to load uri-path multi-pattern from '%s'", path.c_str());
320     return false;
321   }
322 
323   /* Have the multiplattern be named as same as the filename, would be used only for debugging. */
324   MultiPattern *multiPattern;
325   if (denylist) {
326     multiPattern = new NonMatchingMultiPattern(filename);
327     AccessControlDebug("NonMatchingMultiPattern('%s')", filename.c_str());
328   } else {
329     multiPattern = new MultiPattern(filename);
330     AccessControlDebug("MultiPattern('%s')", filename.c_str());
331   }
332   if (nullptr == multiPattern) {
333     AccessControlError("failed to allocate multi-pattern from '%s'", filename.c_str());
334     return false;
335   }
336 
337   AccessControlDebug("loading multi-pattern '%s' from '%s'", filename.c_str(), path.c_str());
338 
339   while (std::getline(ifstr, regex)) {
340     Pattern *p;
341     String::size_type pos;
342 
343     ++lineno;
344 
345     // Allow #-prefixed comments.
346     pos = regex.find_first_of('#');
347     if (pos != String::npos) {
348       regex.resize(pos);
349     }
350 
351     if (regex.empty()) {
352       continue;
353     }
354 
355     p = new Pattern();
356 
357     if (nullptr != p && p->init(regex)) {
358       if (denylist) {
359         AccessControlDebug("Added pattern '%s' to deny list uri-path multi-pattern '%s'", regex.c_str(), filename.c_str());
360         multiPattern->add(p);
361       } else {
362         AccessControlDebug("Added pattern '%s' to allow list uri-path multi-pattern '%s'", regex.c_str(), filename.c_str());
363         multiPattern->add(p);
364       }
365     } else {
366       AccessControlError("%s:%u: failed to parse regex '%s'", path.c_str(), lineno, regex.c_str());
367       delete p;
368     }
369   }
370 
371   ifstr.close();
372 
373   if (!multiPattern->empty()) {
374     _uriPathScope.add(multiPattern);
375   } else {
376     delete multiPattern;
377   }
378 
379   return true;
380 }
381