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