Code Coverage

Cobertura Coverage Report > osm_nbi >

auth.py

Trend

Classes0%
 
Lines0%
 
Conditionals100%
 

File Coverage summary

NameClassesLinesConditionals
auth.py
0%
0/1
0%
0/379
100%
0/0

Coverage Breakdown by Class

NameLinesConditionals
auth.py
0%
0/379
N/A

Source

osm_nbi/auth.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright 2018 Whitestack, LLC
4 # Copyright 2018 Telefonica S.A.
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License"); you may
7 # not use this file except in compliance with the License. You may obtain
8 # 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, WITHOUT
14 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 # License for the specific language governing permissions and limitations
16 # under the License.
17 #
18 # For those usages not covered by the Apache License, Version 2.0 please
19 # contact: esousa@whitestack.com or alfonso.tiernosepulveda@telefonica.com
20 ##
21
22
23 0 """
24 Authenticator is responsible for authenticating the users,
25 create the tokens unscoped and scoped, retrieve the role
26 list inside the projects that they are inserted
27 """
28
29 0 __author__ = "Eduardo Sousa <esousa@whitestack.com>; Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
30 0 __date__ = "$27-jul-2018 23:59:59$"
31
32 0 import cherrypy
33 0 import logging
34 0 import yaml
35 0 from base64 import standard_b64decode
36 0 from copy import deepcopy
37 # from functools import reduce
38 0 from http import HTTPStatus
39 0 from time import time
40 0 from os import path
41
42 0 from osm_nbi.authconn import AuthException, AuthconnException, AuthExceptionUnauthorized
43 0 from osm_nbi.authconn_keystone import AuthconnKeystone
44 0 from osm_nbi.authconn_internal import AuthconnInternal
45 0 from osm_nbi.authconn_tacacs import AuthconnTacacs
46 0 from osm_common import dbmemory, dbmongo, msglocal, msgkafka
47 0 from osm_common.dbbase import DbException
48 0 from osm_nbi.validation import is_valid_uuid
49 0 from itertools import chain
50 0 from uuid import uuid4
51
52
53 0 class Authenticator:
54     """
55     This class should hold all the mechanisms for User Authentication and
56     Authorization. Initially it should support Openstack Keystone as a
57     backend through a plugin model where more backends can be added and a
58     RBAC model to manage permissions on operations.
59     This class must be threading safe
60     """
61
62 0     periodin_db_pruning = 60 * 30  # for the internal backend only. every 30 minutes expired tokens will be pruned
63 0     token_limit = 500   # when reached, the token cache will be cleared
64
65 0     def __init__(self, valid_methods, valid_query_string):
66         """
67         Authenticator initializer. Setup the initial state of the object,
68         while it waits for the config dictionary and database initialization.
69         """
70 0         self.backend = None
71 0         self.config = None
72 0         self.db = None
73 0         self.msg = None
74 0         self.tokens_cache = dict()
75 0         self.next_db_prune_time = 0  # time when next cleaning of expired tokens must be done
76 0         self.roles_to_operations_file = None
77         # self.roles_to_operations_table = None
78 0         self.resources_to_operations_mapping = {}
79 0         self.operation_to_allowed_roles = {}
80 0         self.logger = logging.getLogger("nbi.authenticator")
81 0         self.role_permissions = []
82 0         self.valid_methods = valid_methods
83 0         self.valid_query_string = valid_query_string
84 0         self.system_admin_role_id = None   # system_role id
85 0         self.test_project_id = None  # test_project_id
86
87 0     def start(self, config):
88         """
89         Method to configure the Authenticator object. This method should be called
90         after object creation. It is responsible by initializing the selected backend,
91         as well as the initialization of the database connection.
92
93         :param config: dictionary containing the relevant parameters for this object.
94         """
95 0         self.config = config
96
97 0         try:
98 0             if not self.db:
99 0                 if config["database"]["driver"] == "mongo":
100 0                     self.db = dbmongo.DbMongo()
101 0                     self.db.db_connect(config["database"])
102 0                 elif config["database"]["driver"] == "memory":
103 0                     self.db = dbmemory.DbMemory()
104 0                     self.db.db_connect(config["database"])
105                 else:
106 0                     raise AuthException("Invalid configuration param '{}' at '[database]':'driver'"
107                                         .format(config["database"]["driver"]))
108 0             if not self.msg:
109 0                 if config["message"]["driver"] == "local":
110 0                     self.msg = msglocal.MsgLocal()
111 0                     self.msg.connect(config["message"])
112 0                 elif config["message"]["driver"] == "kafka":
113 0                     self.msg = msgkafka.MsgKafka()
114 0                     self.msg.connect(config["message"])
115                 else:
116 0                     raise AuthException("Invalid configuration param '{}' at '[message]':'driver'"
117                                         .format(config["message"]["driver"]))
118 0             if not self.backend:
119 0                 if config["authentication"]["backend"] == "keystone":
120 0                     self.backend = AuthconnKeystone(self.config["authentication"], self.db, self.role_permissions)
121 0                 elif config["authentication"]["backend"] == "internal":
122 0                     self.backend = AuthconnInternal(self.config["authentication"], self.db, self.role_permissions)
123 0                     self._internal_tokens_prune("tokens")
124 0                 elif config["authentication"]["backend"] == "tacacs":
125 0                     self.backend = AuthconnTacacs(self.config["authentication"], self.db, self.role_permissions)
126 0                     self._internal_tokens_prune("tokens_tacacs")
127                 else:
128 0                     raise AuthException("Unknown authentication backend: {}"
129                                         .format(config["authentication"]["backend"]))
130
131 0             if not self.roles_to_operations_file:
132 0                 if "roles_to_operations" in config["rbac"]:
133 0                     self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
134                 else:
135 0                     possible_paths = (
136                         __file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml",
137                         "./roles_to_operations.yml"
138                     )
139 0                     for config_file in possible_paths:
140 0                         if path.isfile(config_file):
141 0                             self.roles_to_operations_file = config_file
142 0                             break
143 0                 if not self.roles_to_operations_file:
144 0                     raise AuthException("Invalid permission configuration: roles_to_operations file missing")
145
146             # load role_permissions
147 0             def load_role_permissions(method_dict):
148 0                 for k in method_dict:
149 0                     if k == "ROLE_PERMISSION":
150 0                         for method in chain(method_dict.get("METHODS", ()), method_dict.get("TODO", ())):
151 0                             permission = method_dict["ROLE_PERMISSION"] + method.lower()
152 0                             if permission not in self.role_permissions:
153 0                                 self.role_permissions.append(permission)
154 0                     elif k in ("TODO", "METHODS"):
155 0                         continue
156 0                     elif method_dict[k]:
157 0                         load_role_permissions(method_dict[k])
158
159 0             load_role_permissions(self.valid_methods)
160 0             for query_string in self.valid_query_string:
161 0                 for method in ("get", "put", "patch", "post", "delete"):
162 0                     permission = query_string.lower() + ":" + method
163 0                     if permission not in self.role_permissions:
164 0                         self.role_permissions.append(permission)
165
166             # get ids of role system_admin and test project
167 0             role_system_admin = self.db.get_one("roles", {"name": "system_admin"}, fail_on_empty=False)
168 0             if role_system_admin:
169 0                 self.system_admin_role_id = role_system_admin["_id"]
170 0             test_project_name = self.config["authentication"].get("project_not_authorized", "admin")
171 0             test_project = self.db.get_one("projects", {"name": test_project_name}, fail_on_empty=False)
172 0             if test_project:
173 0                 self.test_project_id = test_project["_id"]
174
175 0         except Exception as e:
176 0             raise AuthException(str(e))
177
178 0     def stop(self):
179 0         try:
180 0             if self.db:
181 0                 self.db.db_disconnect()
182 0         except DbException as e:
183 0             raise AuthException(str(e), http_code=e.http_code)
184
185 0     def create_admin_project(self):
186         """
187         Creates a new project 'admin' into database if it doesn't exist. Useful for initialization.
188         :return: _id identity of the 'admin' project
189         """
190
191         # projects = self.db.get_one("projects", fail_on_empty=False, fail_on_more=False)
192 0         project_desc = {"name": "admin"}
193 0         projects = self.backend.get_project_list(project_desc)
194 0         if projects:
195 0             return projects[0]["_id"]
196 0         now = time()
197 0         project_desc["_id"] = str(uuid4())
198 0         project_desc["_admin"] = {"created": now, "modified": now}
199 0         pid = self.backend.create_project(project_desc)
200 0         self.logger.info("Project '{}' created at database".format(project_desc["name"]))
201 0         return pid
202
203 0     def create_admin_user(self, project_id):
204         """
205         Creates a new user admin/admin into database if database is empty. Useful for initialization
206         :return: _id identity of the inserted data, or None
207         """
208         # users = self.db.get_one("users", fail_on_empty=False, fail_on_more=False)
209 0         users = self.backend.get_user_list()
210 0         if users:
211 0             return None
212         # user_desc = {"username": "admin", "password": "admin", "projects": [project_id]}
213 0         now = time()
214 0         user_desc = {"username": "admin", "password": "admin", "_admin": {"created": now, "modified": now}}
215 0         if project_id:
216 0             pid = project_id
217         else:
218             # proj = self.db.get_one("projects", {"name": "admin"}, fail_on_empty=False, fail_on_more=False)
219 0             proj = self.backend.get_project_list({"name": "admin"})
220 0             pid = proj[0]["_id"] if proj else None
221         # role = self.db.get_one("roles", {"name": "system_admin"}, fail_on_empty=False, fail_on_more=False)
222 0         roles = self.backend.get_role_list({"name": "system_admin"})
223 0         if pid and roles:
224 0             user_desc["project_role_mappings"] = [{"project": pid, "role": roles[0]["_id"]}]
225 0         uid = self.backend.create_user(user_desc)
226 0         self.logger.info("User '{}' created at database".format(user_desc["username"]))
227 0         return uid
228
229 0     def init_db(self, target_version='1.0'):
230         """
231         Check if the database has been initialized, with at least one user. If not, create the required tables
232         and insert the predefined mappings between roles and permissions.
233
234         :param target_version: schema version that should be present in the database.
235         :return: None if OK, exception if error or version is different.
236         """
237
238 0         records = self.backend.get_role_list()
239
240         # Loading permissions to AUTH. At lease system_admin must be present.
241 0         if not records or not next((r for r in records if r["name"] == "system_admin"), None):
242 0             with open(self.roles_to_operations_file, "r") as stream:
243 0                 roles_to_operations_yaml = yaml.load(stream, Loader=yaml.Loader)
244
245 0             role_names = []
246 0             for role_with_operations in roles_to_operations_yaml["roles"]:
247                 # Verifying if role already exists. If it does, raise exception
248 0                 if role_with_operations["name"] not in role_names:
249 0                     role_names.append(role_with_operations["name"])
250                 else:
251 0                     raise AuthException("Duplicated role name '{}' at file '{}''"
252                                         .format(role_with_operations["name"], self.roles_to_operations_file))
253
254 0                 if not role_with_operations["permissions"]:
255 0                     continue
256
257 0                 for permission, is_allowed in role_with_operations["permissions"].items():
258 0                     if not isinstance(is_allowed, bool):
259 0                         raise AuthException("Invalid value for permission '{}' at role '{}'; at file '{}'"
260                                             .format(permission, role_with_operations["name"],
261                                                     self.roles_to_operations_file))
262
263                     # TODO check permission is ok
264 0                     if permission[-1] == ":":
265 0                         raise AuthException("Invalid permission '{}' terminated in ':' for role '{}'; at file {}"
266                                             .format(permission, role_with_operations["name"],
267                                                     self.roles_to_operations_file))
268
269 0                 if "default" not in role_with_operations["permissions"]:
270 0                     role_with_operations["permissions"]["default"] = False
271 0                 if "admin" not in role_with_operations["permissions"]:
272 0                     role_with_operations["permissions"]["admin"] = False
273
274 0                 now = time()
275 0                 role_with_operations["_admin"] = {
276                     "created": now,
277                     "modified": now,
278                 }
279
280                 # self.db.create(self.roles_to_operations_table, role_with_operations)
281 0                 try:
282 0                     self.backend.create_role(role_with_operations)
283 0                     self.logger.info("Role '{}' created".format(role_with_operations["name"]))
284 0                 except (AuthException, AuthconnException) as e:
285 0                     if role_with_operations["name"] == "system_admin":
286 0                         raise
287 0                     self.logger.error("Role '{}' cannot be created: {}".format(role_with_operations["name"], e))
288
289         # Create admin project&user if required
290 0         pid = self.create_admin_project()
291 0         user_id = self.create_admin_user(pid)
292
293         # try to assign system_admin role to user admin if not any user has this role
294 0         if not user_id:
295 0             try:
296 0                 users = self.backend.get_user_list()
297 0                 roles = self.backend.get_role_list({"name": "system_admin"})
298 0                 role_id = roles[0]["_id"]
299 0                 user_with_system_admin = False
300 0                 user_admin_id = None
301 0                 for user in users:
302 0                     if not user_admin_id:
303 0                         user_admin_id = user["_id"]
304 0                     if user["username"] == "admin":
305 0                         user_admin_id = user["_id"]
306 0                     for prm in user.get("project_role_mappings", ()):
307 0                         if prm["role"] == role_id:
308 0                             user_with_system_admin = True
309 0                             break
310 0                     if user_with_system_admin:
311 0                         break
312 0                 if not user_with_system_admin:
313 0                     self.backend.update_user({"_id": user_admin_id,
314                                               "add_project_role_mappings": [{"project": pid, "role": role_id}]})
315 0                     self.logger.info("Added role system admin to user='{}' project=admin".format(user_admin_id))
316 0             except Exception as e:
317 0                 self.logger.error("Error in Authorization DataBase initialization: {}: {}".format(type(e).__name__, e))
318
319 0         self.load_operation_to_allowed_roles()
320
321 0     def load_operation_to_allowed_roles(self):
322         """
323         Fills the internal self.operation_to_allowed_roles based on database role content and self.role_permissions
324         It works in a shadow copy and replace at the end to allow other threads working with the old copy
325         :return: None
326         """
327
328 0         permissions = {oper: [] for oper in self.role_permissions}
329         # records = self.db.get_list(self.roles_to_operations_table)
330 0         records = self.backend.get_role_list()
331
332 0         ignore_fields = ["_id", "_admin", "name", "default"]
333 0         for record in records:
334 0             if not record.get("permissions"):
335 0                 continue
336 0             record_permissions = {oper: record["permissions"].get("default", False) for oper in self.role_permissions}
337 0             operations_joined = [(oper, value) for oper, value in record["permissions"].items()
338                                  if oper not in ignore_fields]
339 0             operations_joined.sort(key=lambda x: x[0].count(":"))
340
341 0             for oper in operations_joined:
342 0                 match = list(filter(lambda x: x.find(oper[0]) == 0, record_permissions.keys()))
343
344 0                 for m in match:
345 0                     record_permissions[m] = oper[1]
346
347 0             allowed_operations = [k for k, v in record_permissions.items() if v is True]
348
349 0             for allowed_op in allowed_operations:
350 0                 permissions[allowed_op].append(record["name"])
351
352 0         self.operation_to_allowed_roles = permissions
353
354 0     def authorize(self, role_permission=None, query_string_operations=None, item_id=None):
355 0         token = None
356 0         user_passwd64 = None
357 0         try:
358             # 1. Get token Authorization bearer
359 0             auth = cherrypy.request.headers.get("Authorization")
360 0             if auth:
361 0                 auth_list = auth.split(" ")
362 0                 if auth_list[0].lower() == "bearer":
363 0                     token = auth_list[-1]
364 0                 elif auth_list[0].lower() == "basic":
365 0                     user_passwd64 = auth_list[-1]
366 0             if not token:
367 0                 if cherrypy.session.get("Authorization"):
368                     # 2. Try using session before request a new token. If not, basic authentication will generate
369 0                     token = cherrypy.session.get("Authorization")
370 0                     if token == "logout":
371 0                         token = None  # force Unauthorized response to insert user password again
372 0                 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
373                     # 3. Get new token from user password
374 0                     user = None
375 0                     passwd = None
376 0                     try:
377 0                         user_passwd = standard_b64decode(user_passwd64).decode()
378 0                         user, _, passwd = user_passwd.partition(":")
379 0                     except Exception:
380 0                         pass
381 0                     outdata = self.new_token(None, {"username": user, "password": passwd})
382 0                     token = outdata["_id"]
383 0                     cherrypy.session['Authorization'] = token
384
385 0             if not token:
386 0                 raise AuthException("Needed a token or Authorization http header",
387                                     http_code=HTTPStatus.UNAUTHORIZED)
388
389             # try to get from cache first
390 0             now = time()
391 0             token_info = self.tokens_cache.get(token)
392 0             if token_info and token_info["expires"] < now:
393                 # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del
394 0                 self.tokens_cache.pop(token, None)
395 0                 token_info = None
396
397             # get from database if not in cache
398 0             if not token_info:
399 0                 token_info = self.backend.validate_token(token)
400                 # Clear cache if token limit reached
401 0                 if len(self.tokens_cache) > self.token_limit:
402 0                     self.tokens_cache.clear()
403 0                 self.tokens_cache[token] = token_info
404             # TODO add to token info remote host, port
405
406 0             if role_permission:
407 0                 RBAC_auth = self.check_permissions(token_info, cherrypy.request.method, role_permission,
408                                                    query_string_operations, item_id)
409 0                 token_info["allow_show_user_project_role"] = RBAC_auth
410
411 0             return token_info
412 0         except AuthException as e:
413 0             if not isinstance(e, AuthExceptionUnauthorized):
414 0                 if cherrypy.session.get('Authorization'):
415 0                     del cherrypy.session['Authorization']
416 0                 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
417 0             if self.config["authentication"].get("user_not_authorized"):
418 0                 return {"id": "testing-token", "_id": "testing-token",
419                         "project_id": self.test_project_id,
420                         "username": self.config["authentication"]["user_not_authorized"],
421                         "roles": [self.system_admin_role_id],
422                         "admin": True, "allow_show_user_project_role": True}
423 0             raise
424
425 0     def new_token(self, token_info, indata, remote):
426 0         new_token_info = self.backend.authenticate(
427             credentials=indata,
428             token_info=token_info,
429         )
430
431 0         new_token_info["remote_port"] = remote.port
432 0         if not new_token_info.get("expires"):
433 0             new_token_info["expires"] = time() + 3600
434 0         if not new_token_info.get("admin"):
435 0             new_token_info["admin"] = True if new_token_info.get("project_name") == "admin" else False
436             # TODO put admin in RBAC
437
438 0         if remote.name:
439 0             new_token_info["remote_host"] = remote.name
440 0         elif remote.ip:
441 0             new_token_info["remote_host"] = remote.ip
442
443         # TODO call self._internal_tokens_prune(now) ?
444 0         return deepcopy(new_token_info)
445
446 0     def get_token_list(self, token_info):
447 0         if self.config["authentication"]["backend"] == "internal":
448 0             return self._internal_get_token_list(token_info)
449         else:
450             # TODO: check if this can be avoided. Backend may provide enough information
451 0             return [deepcopy(token) for token in self.tokens_cache.values()
452                     if token["username"] == token_info["username"]]
453
454 0     def get_token(self, token_info, token):
455 0         if self.config["authentication"]["backend"] == "internal":
456 0             return self._internal_get_token(token_info, token)
457         else:
458             # TODO: check if this can be avoided. Backend may provide enough information
459 0             token_value = self.tokens_cache.get(token)
460 0             if not token_value:
461 0                 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
462 0             if token_value["username"] != token_info["username"] and not token_info["admin"]:
463 0                 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
464 0             return token_value
465
466 0     def del_token(self, token):
467 0         try:
468 0             self.backend.revoke_token(token)
469             # self.tokens_cache.pop(token, None)
470 0             self.remove_token_from_cache(token)
471 0             return "token '{}' deleted".format(token)
472 0         except KeyError:
473 0             raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
474
475 0     def check_permissions(self, token_info, method, role_permission=None, query_string_operations=None, item_id=None):
476         """
477         Checks that operation has permissions to be done, base on the assigned roles to this user project
478         :param token_info: Dictionary that contains "roles" with a list of assigned roles.
479             This method fills the token_info["admin"] with True or False based on assigned tokens, if any allows admin
480             This will be used among others to hide or not the _admin content of topics
481         :param method: GET,PUT, POST, ...
482         :param role_permission: role permission name of the operation required
483         :param query_string_operations: list of possible admin query strings provided by user. It is checked that the
484             assigned role allows this query string for this method
485         :param item_id: item identifier if included in the URL, None otherwise
486         :return: True if access granted by permission rules, False if access granted by default rules (Bug 853)
487         :raises: AuthExceptionUnauthorized if access denied
488         """
489
490 0         roles_required = self.operation_to_allowed_roles[role_permission]
491 0         roles_allowed = [role["name"] for role in token_info["roles"]]
492
493         # fills token_info["admin"] if some roles allows it
494 0         token_info["admin"] = False
495 0         for role in roles_allowed:
496 0             if role in self.operation_to_allowed_roles["admin:" + method.lower()]:
497 0                 token_info["admin"] = True
498 0                 break
499
500 0         if "anonymous" in roles_required:
501 0             return True
502 0         operation_allowed = False
503 0         for role in roles_allowed:
504 0             if role in roles_required:
505 0                 operation_allowed = True
506                 # if query_string operations, check if this role allows it
507 0                 if not query_string_operations:
508 0                     return True
509 0                 for query_string_operation in query_string_operations:
510 0                     if role not in self.operation_to_allowed_roles[query_string_operation]:
511 0                         break
512                 else:
513 0                     return True
514
515         # Bug 853 - Final Solution
516         # User/Project/Role whole listings are filtered elsewhere
517         # uid, pid, rid = ("user_id", "project_id", "id") if is_valid_uuid(id) else ("username", "project_name", "name")
518 0         uid = "user_id" if is_valid_uuid(item_id) else "username"
519 0         if (role_permission in ["projects:get", "projects:id:get", "roles:get", "roles:id:get", "users:get"]) \
520                 or (role_permission == "users:id:get" and item_id == token_info[uid]):
521             # or (role_permission == "projects:id:get" and item_id == token_info[pid]) \
522             # or (role_permission == "roles:id:get" and item_id in [role[rid] for role in token_info["roles"]]):
523 0             return False
524
525 0         if not operation_allowed:
526 0             raise AuthExceptionUnauthorized("Access denied: lack of permissions.")
527         else:
528 0             raise AuthExceptionUnauthorized("Access denied: You have not permissions to use these admin query string")
529
530 0     def get_user_list(self):
531 0         return self.backend.get_user_list()
532
533 0     def _normalize_url(self, url, method):
534         # DEPRECATED !!!
535         # Removing query strings
536 0         normalized_url = url if '?' not in url else url[:url.find("?")]
537 0         normalized_url_splitted = normalized_url.split("/")
538 0         parameters = {}
539
540 0         filtered_keys = [key for key in self.resources_to_operations_mapping.keys()
541                          if method in key.split()[0]]
542
543 0         for idx, path_part in enumerate(normalized_url_splitted):
544 0             tmp_keys = []
545 0             for tmp_key in filtered_keys:
546 0                 splitted = tmp_key.split()[1].split("/")
547 0                 if idx >= len(splitted):
548 0                     continue
549 0                 elif "<" in splitted[idx] and ">" in splitted[idx]:
550 0                     if splitted[idx] == "<artifactPath>":
551 0                         tmp_keys.append(tmp_key)
552 0                         continue
553 0                     elif idx == len(normalized_url_splitted) - 1 and \
554                             len(normalized_url_splitted) != len(splitted):
555 0                         continue
556                     else:
557 0                         tmp_keys.append(tmp_key)
558 0                 elif splitted[idx] == path_part:
559 0                     if idx == len(normalized_url_splitted) - 1 and \
560                             len(normalized_url_splitted) != len(splitted):
561 0                         continue
562                     else:
563 0                         tmp_keys.append(tmp_key)
564 0             filtered_keys = tmp_keys
565 0             if len(filtered_keys) == 1 and \
566                     filtered_keys[0].split("/")[-1] == "<artifactPath>":
567 0                 break
568
569 0         if len(filtered_keys) == 0:
570 0             raise AuthException("Cannot make an authorization decision. URL not found. URL: {0}".format(url))
571 0         elif len(filtered_keys) > 1:
572 0             raise AuthException("Cannot make an authorization decision. Multiple URLs found. URL: {0}".format(url))
573
574 0         filtered_key = filtered_keys[0]
575
576 0         for idx, path_part in enumerate(filtered_key.split()[1].split("/")):
577 0             if "<" in path_part and ">" in path_part:
578 0                 if path_part == "<artifactPath>":
579 0                     parameters[path_part[1:-1]] = "/".join(normalized_url_splitted[idx:])
580                 else:
581 0                     parameters[path_part[1:-1]] = normalized_url_splitted[idx]
582
583 0         return filtered_key, parameters
584
585 0     def _internal_get_token_list(self, token_info):
586 0         now = time()
587 0         token_list = self.db.get_list("tokens", {"username": token_info["username"], "expires.gt": now})
588 0         return token_list
589
590 0     def _internal_get_token(self, token_info, token_id):
591 0         token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False)
592 0         if not token_value:
593 0             raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
594 0         if token_value["username"] != token_info["username"] and not token_info["admin"]:
595 0             raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
596 0         return token_value
597
598 0     def _internal_tokens_prune(self, token_collection, now=None):
599 0         now = now or time()
600 0         if not self.next_db_prune_time or self.next_db_prune_time >= now:
601 0             self.db.del_list(token_collection, {"expires.lt": now})
602 0             self.next_db_prune_time = self.periodin_db_pruning + now
603             # self.tokens_cache.clear()  # not required any more
604
605 0     def remove_token_from_cache(self, token=None):
606 0         if token:
607 0             self.tokens_cache.pop(token, None)
608         else:
609 0             self.tokens_cache.clear()
610 0         self.msg.write("admin", "revoke_token", {"_id": token} if token else None)