blob: b1f73fee0a7fb28cab8c317fedb9eb68a24a98bc [file] [log] [blame]
Eduardo Sousa819d34c2018-07-31 01:20:02 +01001# -*- coding: utf-8 -*-
2
Eduardo Sousad795f872019-02-05 16:05:53 +00003# Copyright 2018 Whitestack, LLC
4# Copyright 2018 Telefonica S.A.
tierno0ea204e2019-01-25 14:16:24 +00005#
Eduardo Sousad795f872019-02-05 16:05:53 +00006# 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
tierno0ea204e2019-01-25 14:16:24 +000011#
12# Unless required by applicable law or agreed to in writing, software
Eduardo Sousad795f872019-02-05 16:05:53 +000013# 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##
tierno0ea204e2019-01-25 14:16:24 +000021
22
Eduardo Sousa819d34c2018-07-31 01:20:02 +010023"""
24Authenticator is responsible for authenticating the users,
25create the tokens unscoped and scoped, retrieve the role
26list inside the projects that they are inserted
27"""
28
tierno0ea204e2019-01-25 14:16:24 +000029__author__ = "Eduardo Sousa <esousa@whitestack.com>; Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
Eduardo Sousa819d34c2018-07-31 01:20:02 +010030__date__ = "$27-jul-2018 23:59:59$"
31
Eduardo Sousad1b525d2018-10-04 04:24:18 +010032import cherrypy
Eduardo Sousa819d34c2018-07-31 01:20:02 +010033import logging
Eduardo Sousa29933fc2018-11-14 06:36:35 +000034import yaml
Eduardo Sousa2f988212018-07-26 01:04:11 +010035from base64 import standard_b64decode
Eduardo Sousa819d34c2018-07-31 01:20:02 +010036from copy import deepcopy
tierno38dcfeb2019-06-10 16:44:00 +000037# from functools import reduce
Eduardo Sousa2f988212018-07-26 01:04:11 +010038from http import HTTPStatus
Eduardo Sousa819d34c2018-07-31 01:20:02 +010039from time import time
Eduardo Sousa5c01e192019-05-08 02:35:47 +010040from os import path
Eduardo Sousa2f988212018-07-26 01:04:11 +010041
tiernoc8445362019-06-14 12:07:15 +000042from authconn import AuthException, AuthExceptionUnauthorized
Eduardo Sousa819d34c2018-07-31 01:20:02 +010043from authconn_keystone import AuthconnKeystone
delacruzramoceb8baf2019-06-21 14:25:38 +020044from authconn_internal import AuthconnInternal # Comment out for testing&debugging, uncomment when ready
Eduardo Sousad1b525d2018-10-04 04:24:18 +010045from osm_common import dbmongo
46from osm_common import dbmemory
47from osm_common.dbbase import DbException
Eduardo Sousa2f988212018-07-26 01:04:11 +010048
delacruzramoceb8baf2019-06-21 14:25:38 +020049from uuid import uuid4 # For Role _id with internal authentication backend
50
Eduardo Sousa2f988212018-07-26 01:04:11 +010051
Eduardo Sousa819d34c2018-07-31 01:20:02 +010052class Authenticator:
53 """
54 This class should hold all the mechanisms for User Authentication and
55 Authorization. Initially it should support Openstack Keystone as a
56 backend through a plugin model where more backends can be added and a
57 RBAC model to manage permissions on operations.
tierno65ca36d2019-02-12 19:27:52 +010058 This class must be threading safe
Eduardo Sousa819d34c2018-07-31 01:20:02 +010059 """
Eduardo Sousa2f988212018-07-26 01:04:11 +010060
Eduardo Sousa29933fc2018-11-14 06:36:35 +000061 periodin_db_pruning = 60 * 30 # for the internal backend only. every 30 minutes expired tokens will be pruned
tierno0ea204e2019-01-25 14:16:24 +000062
Eduardo Sousad1b525d2018-10-04 04:24:18 +010063 def __init__(self):
Eduardo Sousa819d34c2018-07-31 01:20:02 +010064 """
65 Authenticator initializer. Setup the initial state of the object,
66 while it waits for the config dictionary and database initialization.
Eduardo Sousa819d34c2018-07-31 01:20:02 +010067 """
Eduardo Sousa819d34c2018-07-31 01:20:02 +010068 self.backend = None
69 self.config = None
70 self.db = None
tierno0ea204e2019-01-25 14:16:24 +000071 self.tokens_cache = dict()
72 self.next_db_prune_time = 0 # time when next cleaning of expired tokens must be done
Eduardo Sousa29933fc2018-11-14 06:36:35 +000073 self.resources_to_operations_file = None
74 self.roles_to_operations_file = None
delacruzramoceb8baf2019-06-21 14:25:38 +020075 self.roles_to_operations_table = None
Eduardo Sousa29933fc2018-11-14 06:36:35 +000076 self.resources_to_operations_mapping = {}
77 self.operation_to_allowed_roles = {}
Eduardo Sousa819d34c2018-07-31 01:20:02 +010078 self.logger = logging.getLogger("nbi.authenticator")
tierno1f029d82019-06-13 22:37:04 +000079 self.operations = []
Eduardo Sousa819d34c2018-07-31 01:20:02 +010080
81 def start(self, config):
82 """
83 Method to configure the Authenticator object. This method should be called
84 after object creation. It is responsible by initializing the selected backend,
85 as well as the initialization of the database connection.
86
87 :param config: dictionary containing the relevant parameters for this object.
88 """
89 self.config = config
90
91 try:
Eduardo Sousa819d34c2018-07-31 01:20:02 +010092 if not self.db:
Eduardo Sousad1b525d2018-10-04 04:24:18 +010093 if config["database"]["driver"] == "mongo":
94 self.db = dbmongo.DbMongo()
95 self.db.db_connect(config["database"])
96 elif config["database"]["driver"] == "memory":
97 self.db = dbmemory.DbMemory()
98 self.db.db_connect(config["database"])
99 else:
100 raise AuthException("Invalid configuration param '{}' at '[database]':'driver'"
101 .format(config["database"]["driver"]))
tierno0ea204e2019-01-25 14:16:24 +0000102 if not self.backend:
103 if config["authentication"]["backend"] == "keystone":
104 self.backend = AuthconnKeystone(self.config["authentication"])
105 elif config["authentication"]["backend"] == "internal":
delacruzramoceb8baf2019-06-21 14:25:38 +0200106 self.backend = AuthconnInternal(self.config["authentication"], self.db, self.tokens_cache)
tierno0ea204e2019-01-25 14:16:24 +0000107 self._internal_tokens_prune()
108 else:
109 raise AuthException("Unknown authentication backend: {}"
110 .format(config["authentication"]["backend"]))
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000111 if not self.resources_to_operations_file:
112 if "resources_to_operations" in config["rbac"]:
113 self.resources_to_operations_file = config["rbac"]["resources_to_operations"]
114 else:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100115 possible_paths = (
116 __file__[:__file__.rfind("auth.py")] + "resources_to_operations.yml",
117 "./resources_to_operations.yml"
118 )
119 for config_file in possible_paths:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000120 if path.isfile(config_file):
121 self.resources_to_operations_file = config_file
122 break
123 if not self.resources_to_operations_file:
124 raise AuthException("Invalid permission configuration: resources_to_operations file missing")
125 if not self.roles_to_operations_file:
126 if "roles_to_operations" in config["rbac"]:
127 self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
128 else:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100129 possible_paths = (
130 __file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml",
131 "./roles_to_operations.yml"
132 )
133 for config_file in possible_paths:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000134 if path.isfile(config_file):
135 self.roles_to_operations_file = config_file
136 break
137 if not self.roles_to_operations_file:
138 raise AuthException("Invalid permission configuration: roles_to_operations file missing")
delacruzramoceb8baf2019-06-21 14:25:38 +0200139 if not self.roles_to_operations_table: # PROVISIONAL ?
140 self.roles_to_operations_table = "roles_operations" \
141 if config["authentication"]["backend"] == "keystone" \
142 else "roles"
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100143 except Exception as e:
144 raise AuthException(str(e))
145
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100146 def stop(self):
147 try:
148 if self.db:
149 self.db.db_disconnect()
150 except DbException as e:
151 raise AuthException(str(e), http_code=e.http_code)
152
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000153 def init_db(self, target_version='1.0'):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100154 """
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000155 Check if the database has been initialized, with at least one user. If not, create the required tables
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100156 and insert the predefined mappings between roles and permissions.
157
158 :param target_version: schema version that should be present in the database.
159 :return: None if OK, exception if error or version is different.
160 """
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000161 # Always reads operation to resource mapping from file (this is static, no need to store it in MongoDB)
162 # Operations encoding: "<METHOD> <URL>"
163 # Note: it is faster to rewrite the value than to check if it is already there or not
delacruzramoceb8baf2019-06-21 14:25:38 +0200164
165 # PCR 28/05/2019 Commented out to allow initialization for internal backend
166 # if self.config["authentication"]["backend"] == "internal":
167 # return
Eduardo Sousa044f4312019-05-20 15:17:35 +0100168
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000169 with open(self.resources_to_operations_file, "r") as stream:
170 resources_to_operations_yaml = yaml.load(stream)
171
172 for resource, operation in resources_to_operations_yaml["resources_to_operations"].items():
tierno1f029d82019-06-13 22:37:04 +0000173 if operation not in self.operations:
174 self.operations.append(operation)
Eduardo Sousac4650362019-06-04 13:24:22 +0100175 self.resources_to_operations_mapping[resource] = operation
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000176
delacruzramoceb8baf2019-06-21 14:25:38 +0200177 records = self.db.get_list(self.roles_to_operations_table)
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000178
tierno1f029d82019-06-13 22:37:04 +0000179 # Loading permissions to MongoDB if there is not any permission.
180 if not records:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000181 with open(self.roles_to_operations_file, "r") as stream:
182 roles_to_operations_yaml = yaml.load(stream)
183
tierno1f029d82019-06-13 22:37:04 +0000184 role_names = []
185 for role_with_operations in roles_to_operations_yaml["roles"]:
186 # Verifying if role already exists. If it does, raise exception
187 if role_with_operations["name"] not in role_names:
188 role_names.append(role_with_operations["name"])
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000189 else:
tierno1f029d82019-06-13 22:37:04 +0000190 raise AuthException("Duplicated role name '{}' at file '{}''"
191 .format(role_with_operations["name"], self.roles_to_operations_file))
192
193 if not role_with_operations["permissions"]:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000194 continue
195
tierno1f029d82019-06-13 22:37:04 +0000196 for permission, is_allowed in role_with_operations["permissions"].items():
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000197 if not isinstance(is_allowed, bool):
tierno1f029d82019-06-13 22:37:04 +0000198 raise AuthException("Invalid value for permission '{}' at role '{}'; at file '{}'"
199 .format(permission, role_with_operations["name"],
200 self.roles_to_operations_file))
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000201
tierno1f029d82019-06-13 22:37:04 +0000202 # TODO chek permission is ok
203 if permission[-1] == ":":
204 raise AuthException("Invalid permission '{}' terminated in ':' for role '{}'; at file {}"
205 .format(permission, role_with_operations["name"],
206 self.roles_to_operations_file))
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000207
tierno1f029d82019-06-13 22:37:04 +0000208 if "default" not in role_with_operations["permissions"]:
209 role_with_operations["permissions"]["default"] = False
210 if "admin" not in role_with_operations["permissions"]:
211 role_with_operations["permissions"]["admin"] = False
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000212
213 now = time()
tierno1f029d82019-06-13 22:37:04 +0000214 role_with_operations["_admin"] = {
215 "created": now,
216 "modified": now,
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000217 }
218
delacruzramoceb8baf2019-06-21 14:25:38 +0200219 if self.config["authentication"]["backend"] == "keystone":
220 if role_with_operations["name"] != "anonymous":
221 backend_roles = self.backend.get_role_list(filter_q={"name": role_with_operations["name"]})
222 if backend_roles:
223 backend_id = backend_roles[0]["_id"]
224 else:
225 backend_id = self.backend.create_role(role_with_operations["name"])
226 role_with_operations["_id"] = backend_id
227 else:
228 role_with_operations["_id"] = str(uuid4())
tierno1f029d82019-06-13 22:37:04 +0000229
delacruzramoceb8baf2019-06-21 14:25:38 +0200230 self.db.create(self.roles_to_operations_table, role_with_operations)
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000231
tierno1f029d82019-06-13 22:37:04 +0000232 if self.config["authentication"]["backend"] != "internal":
233 self.backend.assign_role_to_user("admin", "admin", "system_admin")
234
235 self.load_operation_to_allowed_roles()
236
237 def load_operation_to_allowed_roles(self):
238 """
239 Fills the internal self.operation_to_allowed_roles based on database role content and self.operations
tiernoa6bb45d2019-06-14 09:45:39 +0000240 It works in a shadow copy and replace at the end to allow other threads working with the old copy
tierno1f029d82019-06-13 22:37:04 +0000241 :return: None
242 """
243
244 permissions = {oper: [] for oper in self.operations}
delacruzramoceb8baf2019-06-21 14:25:38 +0200245 records = self.db.get_list(self.roles_to_operations_table)
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000246
tiernoa6bb45d2019-06-14 09:45:39 +0000247 ignore_fields = ["_id", "_admin", "name", "default"]
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000248 for record in records:
tierno1f029d82019-06-13 22:37:04 +0000249 record_permissions = {oper: record["permissions"].get("default", False) for oper in self.operations}
250 operations_joined = [(oper, value) for oper, value in record["permissions"].items()
251 if oper not in ignore_fields]
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000252 operations_joined.sort(key=lambda x: x[0].count(":"))
253
254 for oper in operations_joined:
255 match = list(filter(lambda x: x.find(oper[0]) == 0, record_permissions.keys()))
256
257 for m in match:
258 record_permissions[m] = oper[1]
259
260 allowed_operations = [k for k, v in record_permissions.items() if v is True]
261
262 for allowed_op in allowed_operations:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100263 permissions[allowed_op].append(record["name"])
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000264
tiernoa6bb45d2019-06-14 09:45:39 +0000265 self.operation_to_allowed_roles = permissions
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000266
Eduardo Sousa2f988212018-07-26 01:04:11 +0100267 def authorize(self):
268 token = None
269 user_passwd64 = None
270 try:
271 # 1. Get token Authorization bearer
272 auth = cherrypy.request.headers.get("Authorization")
273 if auth:
274 auth_list = auth.split(" ")
275 if auth_list[0].lower() == "bearer":
276 token = auth_list[-1]
277 elif auth_list[0].lower() == "basic":
278 user_passwd64 = auth_list[-1]
279 if not token:
280 if cherrypy.session.get("Authorization"):
281 # 2. Try using session before request a new token. If not, basic authentication will generate
282 token = cherrypy.session.get("Authorization")
283 if token == "logout":
tierno0ea204e2019-01-25 14:16:24 +0000284 token = None # force Unauthorized response to insert user password again
Eduardo Sousa2f988212018-07-26 01:04:11 +0100285 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
286 # 3. Get new token from user password
287 user = None
288 passwd = None
289 try:
290 user_passwd = standard_b64decode(user_passwd64).decode()
291 user, _, passwd = user_passwd.partition(":")
292 except Exception:
293 pass
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100294 outdata = self.new_token(None, {"username": user, "password": passwd})
Eduardo Sousa2f988212018-07-26 01:04:11 +0100295 token = outdata["id"]
296 cherrypy.session['Authorization'] = token
tiernoa6bb45d2019-06-14 09:45:39 +0000297
delacruzramoceb8baf2019-06-21 14:25:38 +0200298 if not token:
299 raise AuthException("Needed a token or Authorization http header",
300 http_code=HTTPStatus.UNAUTHORIZED)
301 token_info = self.backend.validate_token(token)
302 # TODO add to token info remote host, port
303
304 self.check_permissions(token_info, cherrypy.request.path_info,
305 cherrypy.request.method)
306 return token_info
307
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100308 except AuthException as e:
tiernoc8445362019-06-14 12:07:15 +0000309 if not isinstance(e, AuthExceptionUnauthorized):
310 if cherrypy.session.get('Authorization'):
311 del cherrypy.session['Authorization']
312 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
313 raise
Eduardo Sousa2f988212018-07-26 01:04:11 +0100314
315 def new_token(self, session, indata, remote):
delacruzramoceb8baf2019-06-21 14:25:38 +0200316 current_token = None
317 if session:
318 # current_token = session.get("token")
319 current_token = session.get("_id") if self.config["authentication"]["backend"] == "keystone" \
320 else session
321 token_info = self.backend.authenticate(
322 user=indata.get("username"),
323 password=indata.get("password"),
324 token=current_token,
325 project=indata.get("project_id")
326 )
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100327
delacruzramoceb8baf2019-06-21 14:25:38 +0200328 now = time()
329 new_session = {
330 "_id": token_info["_id"],
331 "id": token_info["_id"],
332 "issued_at": now,
333 "expires": token_info.get("expires", now + 3600),
334 "project_id": token_info["project_id"],
335 "username": token_info.get("username") or session.get("username"),
336 "remote_port": remote.port,
337 "admin": True if token_info.get("project_name") == "admin" else False # TODO put admin in RBAC
338 }
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100339
delacruzramoceb8baf2019-06-21 14:25:38 +0200340 if remote.name:
341 new_session["remote_host"] = remote.name
342 elif remote.ip:
343 new_session["remote_host"] = remote.ip
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100344
delacruzramoceb8baf2019-06-21 14:25:38 +0200345 # TODO: check if this can be avoided. Backend may provide enough information
346 self.tokens_cache[token_info["_id"]] = new_session
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100347
delacruzramoceb8baf2019-06-21 14:25:38 +0200348 return deepcopy(new_session)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100349
350 def get_token_list(self, session):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100351 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100352 return self._internal_get_token_list(session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100353 else:
tierno0ea204e2019-01-25 14:16:24 +0000354 # TODO: check if this can be avoided. Backend may provide enough information
355 return [deepcopy(token) for token in self.tokens_cache.values()
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100356 if token["username"] == session["username"]]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100357
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100358 def get_token(self, session, token):
359 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100360 return self._internal_get_token(session, token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100361 else:
tierno0ea204e2019-01-25 14:16:24 +0000362 # TODO: check if this can be avoided. Backend may provide enough information
363 token_value = self.tokens_cache.get(token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100364 if not token_value:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100365 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100366 if token_value["username"] != session["username"] and not session["admin"]:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100367 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100368 return token_value
Eduardo Sousa2f988212018-07-26 01:04:11 +0100369
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100370 def del_token(self, token):
delacruzramoceb8baf2019-06-21 14:25:38 +0200371 try:
372 self.backend.revoke_token(token)
373 self.tokens_cache.pop(token, None)
374 return "token '{}' deleted".format(token)
375 except KeyError:
376 raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100377
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000378 def check_permissions(self, session, url, method):
379 self.logger.info("Session: {}".format(session))
380 self.logger.info("URL: {}".format(url))
381 self.logger.info("Method: {}".format(method))
382
383 key, parameters = self._normalize_url(url, method)
384
385 # TODO: Check if parameters might be useful for the decision
386
387 operation = self.resources_to_operations_mapping[key]
388 roles_required = self.operation_to_allowed_roles[operation]
tiernoa6bb45d2019-06-14 09:45:39 +0000389 roles_allowed = [role["name"] for role in session["roles"]]
390
391 # fills session["admin"] if some roles allows it
392 session["admin"] = False
393 for role in roles_allowed:
394 if role in self.operation_to_allowed_roles["admin"]:
395 session["admin"] = True
396 break
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000397
398 if "anonymous" in roles_required:
399 return
400
401 for role in roles_allowed:
402 if role in roles_required:
403 return
404
tiernoc8445362019-06-14 12:07:15 +0000405 raise AuthExceptionUnauthorized("Access denied: lack of permissions.")
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000406
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100407 def get_user_list(self):
408 return self.backend.get_user_list()
409
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000410 def _normalize_url(self, url, method):
411 # Removing query strings
412 normalized_url = url if '?' not in url else url[:url.find("?")]
413 normalized_url_splitted = normalized_url.split("/")
414 parameters = {}
415
416 filtered_keys = [key for key in self.resources_to_operations_mapping.keys()
417 if method in key.split()[0]]
418
419 for idx, path_part in enumerate(normalized_url_splitted):
420 tmp_keys = []
421 for tmp_key in filtered_keys:
422 splitted = tmp_key.split()[1].split("/")
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000423 if idx >= len(splitted):
424 continue
425 elif "<" in splitted[idx] and ">" in splitted[idx]:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000426 if splitted[idx] == "<artifactPath>":
427 tmp_keys.append(tmp_key)
428 continue
429 elif idx == len(normalized_url_splitted) - 1 and \
430 len(normalized_url_splitted) != len(splitted):
431 continue
432 else:
433 tmp_keys.append(tmp_key)
434 elif splitted[idx] == path_part:
435 if idx == len(normalized_url_splitted) - 1 and \
436 len(normalized_url_splitted) != len(splitted):
437 continue
438 else:
439 tmp_keys.append(tmp_key)
440 filtered_keys = tmp_keys
441 if len(filtered_keys) == 1 and \
442 filtered_keys[0].split("/")[-1] == "<artifactPath>":
443 break
444
445 if len(filtered_keys) == 0:
446 raise AuthException("Cannot make an authorization decision. URL not found. URL: {0}".format(url))
447 elif len(filtered_keys) > 1:
448 raise AuthException("Cannot make an authorization decision. Multiple URLs found. URL: {0}".format(url))
449
450 filtered_key = filtered_keys[0]
451
452 for idx, path_part in enumerate(filtered_key.split()[1].split("/")):
453 if "<" in path_part and ">" in path_part:
454 if path_part == "<artifactPath>":
455 parameters[path_part[1:-1]] = "/".join(normalized_url_splitted[idx:])
456 else:
457 parameters[path_part[1:-1]] = normalized_url_splitted[idx]
458
459 return filtered_key, parameters
460
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100461 def _internal_get_token_list(self, session):
tierno0ea204e2019-01-25 14:16:24 +0000462 now = time()
463 token_list = self.db.get_list("tokens", {"username": session["username"], "expires.gt": now})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100464 return token_list
465
466 def _internal_get_token(self, session, token_id):
tierno0ea204e2019-01-25 14:16:24 +0000467 token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False)
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100468 if not token_value:
469 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
470 if token_value["username"] != session["username"] and not session["admin"]:
471 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
472 return token_value
473
tierno0ea204e2019-01-25 14:16:24 +0000474 def _internal_tokens_prune(self, now=None):
475 now = now or time()
476 if not self.next_db_prune_time or self.next_db_prune_time >= now:
477 self.db.del_list("tokens", {"expires.lt": now})
478 self.next_db_prune_time = self.periodin_db_pruning + now
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000479 self.tokens_cache.clear() # force to reload tokens from database