blob: 751dd90e5cca6a2aace81e66373470f28a061c91 [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
37from functools import reduce
Eduardo Sousad1b525d2018-10-04 04:24:18 +010038from hashlib import sha256
Eduardo Sousa2f988212018-07-26 01:04:11 +010039from http import HTTPStatus
Eduardo Sousad1b525d2018-10-04 04:24:18 +010040from random import choice as random_choice
Eduardo Sousa819d34c2018-07-31 01:20:02 +010041from time import time
Eduardo Sousa5c01e192019-05-08 02:35:47 +010042from os import path
Eduardo Sousa2f988212018-07-26 01:04:11 +010043
Eduardo Sousa819d34c2018-07-31 01:20:02 +010044from authconn import AuthException
45from authconn_keystone import AuthconnKeystone
Eduardo Sousad1b525d2018-10-04 04:24:18 +010046from osm_common import dbmongo
47from osm_common import dbmemory
48from osm_common.dbbase import DbException
Eduardo Sousa2f988212018-07-26 01:04:11 +010049
Eduardo Sousa2f988212018-07-26 01:04:11 +010050
Eduardo Sousa819d34c2018-07-31 01:20:02 +010051class Authenticator:
52 """
53 This class should hold all the mechanisms for User Authentication and
54 Authorization. Initially it should support Openstack Keystone as a
55 backend through a plugin model where more backends can be added and a
56 RBAC model to manage permissions on operations.
57 """
Eduardo Sousa2f988212018-07-26 01:04:11 +010058
Eduardo Sousa29933fc2018-11-14 06:36:35 +000059 periodin_db_pruning = 60 * 30 # for the internal backend only. every 30 minutes expired tokens will be pruned
tierno0ea204e2019-01-25 14:16:24 +000060
Eduardo Sousad1b525d2018-10-04 04:24:18 +010061 def __init__(self):
Eduardo Sousa819d34c2018-07-31 01:20:02 +010062 """
63 Authenticator initializer. Setup the initial state of the object,
64 while it waits for the config dictionary and database initialization.
Eduardo Sousa819d34c2018-07-31 01:20:02 +010065 """
Eduardo Sousa819d34c2018-07-31 01:20:02 +010066 self.backend = None
67 self.config = None
68 self.db = None
tierno0ea204e2019-01-25 14:16:24 +000069 self.tokens_cache = dict()
70 self.next_db_prune_time = 0 # time when next cleaning of expired tokens must be done
Eduardo Sousa29933fc2018-11-14 06:36:35 +000071 self.resources_to_operations_file = None
72 self.roles_to_operations_file = None
73 self.resources_to_operations_mapping = {}
74 self.operation_to_allowed_roles = {}
Eduardo Sousa819d34c2018-07-31 01:20:02 +010075 self.logger = logging.getLogger("nbi.authenticator")
76
77 def start(self, config):
78 """
79 Method to configure the Authenticator object. This method should be called
80 after object creation. It is responsible by initializing the selected backend,
81 as well as the initialization of the database connection.
82
83 :param config: dictionary containing the relevant parameters for this object.
84 """
85 self.config = config
86
87 try:
Eduardo Sousa819d34c2018-07-31 01:20:02 +010088 if not self.db:
Eduardo Sousad1b525d2018-10-04 04:24:18 +010089 if config["database"]["driver"] == "mongo":
90 self.db = dbmongo.DbMongo()
91 self.db.db_connect(config["database"])
92 elif config["database"]["driver"] == "memory":
93 self.db = dbmemory.DbMemory()
94 self.db.db_connect(config["database"])
95 else:
96 raise AuthException("Invalid configuration param '{}' at '[database]':'driver'"
97 .format(config["database"]["driver"]))
tierno0ea204e2019-01-25 14:16:24 +000098 if not self.backend:
99 if config["authentication"]["backend"] == "keystone":
100 self.backend = AuthconnKeystone(self.config["authentication"])
101 elif config["authentication"]["backend"] == "internal":
102 self._internal_tokens_prune()
103 else:
104 raise AuthException("Unknown authentication backend: {}"
105 .format(config["authentication"]["backend"]))
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000106 if not self.resources_to_operations_file:
107 if "resources_to_operations" in config["rbac"]:
108 self.resources_to_operations_file = config["rbac"]["resources_to_operations"]
109 else:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100110 possible_paths = (
111 __file__[:__file__.rfind("auth.py")] + "resources_to_operations.yml",
112 "./resources_to_operations.yml"
113 )
114 for config_file in possible_paths:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000115 if path.isfile(config_file):
116 self.resources_to_operations_file = config_file
117 break
118 if not self.resources_to_operations_file:
119 raise AuthException("Invalid permission configuration: resources_to_operations file missing")
120 if not self.roles_to_operations_file:
121 if "roles_to_operations" in config["rbac"]:
122 self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
123 else:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100124 possible_paths = (
125 __file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml",
126 "./roles_to_operations.yml"
127 )
128 for config_file in possible_paths:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000129 if path.isfile(config_file):
130 self.roles_to_operations_file = config_file
131 break
132 if not self.roles_to_operations_file:
133 raise AuthException("Invalid permission configuration: roles_to_operations file missing")
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100134 except Exception as e:
135 raise AuthException(str(e))
136
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100137 def stop(self):
138 try:
139 if self.db:
140 self.db.db_disconnect()
141 except DbException as e:
142 raise AuthException(str(e), http_code=e.http_code)
143
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000144 def init_db(self, target_version='1.0'):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100145 """
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000146 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 +0100147 and insert the predefined mappings between roles and permissions.
148
149 :param target_version: schema version that should be present in the database.
150 :return: None if OK, exception if error or version is different.
151 """
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000152 # Always reads operation to resource mapping from file (this is static, no need to store it in MongoDB)
153 # Operations encoding: "<METHOD> <URL>"
154 # Note: it is faster to rewrite the value than to check if it is already there or not
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100155 if self.config["authentication"]["backend"] == "internal":
156 return
157
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000158 operations = []
159 with open(self.resources_to_operations_file, "r") as stream:
160 resources_to_operations_yaml = yaml.load(stream)
161
162 for resource, operation in resources_to_operations_yaml["resources_to_operations"].items():
163 operation_key = operation.replace(".", ":")
164 if operation_key not in operations:
165 operations.append(operation_key)
166 self.resources_to_operations_mapping[resource] = operation_key
167
168 records = self.db.get_list("roles_operations")
169
170 # Loading permissions to MongoDB. If there are permissions already in MongoDB, do nothing.
171 if len(records) == 0:
172 with open(self.roles_to_operations_file, "r") as stream:
173 roles_to_operations_yaml = yaml.load(stream)
174
175 roles = []
176 for role_with_operations in roles_to_operations_yaml["roles_to_operations"]:
177 # Verifying if role already exists. If it does, send warning to log and ignore it.
178 if role_with_operations["role"] not in roles:
179 roles.append(role_with_operations["role"])
180 else:
181 self.logger.warning("Duplicated role with name: {0}. Role definition is ignored."
182 .format(role_with_operations["role"]))
183 continue
184
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000185 role_ops = {}
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000186 root = None
187
188 if not role_with_operations["operations"]:
189 continue
190
191 for operation, is_allowed in role_with_operations["operations"].items():
192 if not isinstance(is_allowed, bool):
193 continue
194
195 if operation == ".":
196 root = is_allowed
197 continue
198
199 if len(operation) != 1 and operation[-1] == ".":
200 self.logger.warning("Invalid operation {0} terminated in '.'. "
201 "Operation will be discarded"
202 .format(operation))
203 continue
204
205 operation_key = operation.replace(".", ":")
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000206 if operation_key not in role_ops.keys():
207 role_ops[operation_key] = is_allowed
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000208 else:
209 self.logger.info("In role {0}, the operation {1} with the value {2} was discarded due to "
210 "repetition.".format(role_with_operations["role"], operation, is_allowed))
211
212 if not root:
213 root = False
214 self.logger.info("Root for role {0} not defined. Default value 'False' applied."
215 .format(role_with_operations["role"]))
216
217 now = time()
218 operation_to_roles_item = {
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000219 "_admin": {
220 "created": now,
221 "modified": now,
222 },
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100223 "name": role_with_operations["role"],
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000224 "root": root
225 }
226
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000227 for operation, value in role_ops.items():
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000228 operation_to_roles_item[operation] = value
229
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100230 if self.config["authentication"]["backend"] != "internal" and \
231 role_with_operations["role"] != "anonymous":
232 keystone_id = self.backend.create_role(role_with_operations["role"])
233 operation_to_roles_item["_id"] = keystone_id["_id"]
234
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000235 self.db.create("roles_operations", operation_to_roles_item)
236
237 permissions = {oper: [] for oper in operations}
238 records = self.db.get_list("roles_operations")
239
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100240 ignore_fields = ["_id", "_admin", "name", "root"]
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000241 for record in records:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000242 record_permissions = {oper: record["root"] for oper in operations}
243 operations_joined = [(oper, value) for oper, value in record.items() if oper not in ignore_fields]
244 operations_joined.sort(key=lambda x: x[0].count(":"))
245
246 for oper in operations_joined:
247 match = list(filter(lambda x: x.find(oper[0]) == 0, record_permissions.keys()))
248
249 for m in match:
250 record_permissions[m] = oper[1]
251
252 allowed_operations = [k for k, v in record_permissions.items() if v is True]
253
254 for allowed_op in allowed_operations:
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100255 permissions[allowed_op].append(record["name"])
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000256
257 for oper, role_list in permissions.items():
258 self.operation_to_allowed_roles[oper] = role_list
259
260 if self.config["authentication"]["backend"] != "internal":
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000261 self.backend.assign_role_to_user("admin", "admin", "system_admin")
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100262
Eduardo Sousa2f988212018-07-26 01:04:11 +0100263 def authorize(self):
264 token = None
265 user_passwd64 = None
266 try:
267 # 1. Get token Authorization bearer
268 auth = cherrypy.request.headers.get("Authorization")
269 if auth:
270 auth_list = auth.split(" ")
271 if auth_list[0].lower() == "bearer":
272 token = auth_list[-1]
273 elif auth_list[0].lower() == "basic":
274 user_passwd64 = auth_list[-1]
275 if not token:
276 if cherrypy.session.get("Authorization"):
277 # 2. Try using session before request a new token. If not, basic authentication will generate
278 token = cherrypy.session.get("Authorization")
279 if token == "logout":
tierno0ea204e2019-01-25 14:16:24 +0000280 token = None # force Unauthorized response to insert user password again
Eduardo Sousa2f988212018-07-26 01:04:11 +0100281 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
282 # 3. Get new token from user password
283 user = None
284 passwd = None
285 try:
286 user_passwd = standard_b64decode(user_passwd64).decode()
287 user, _, passwd = user_passwd.partition(":")
288 except Exception:
289 pass
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100290 outdata = self.new_token(None, {"username": user, "password": passwd})
Eduardo Sousa2f988212018-07-26 01:04:11 +0100291 token = outdata["id"]
292 cherrypy.session['Authorization'] = token
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100293 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100294 return self._internal_authorize(token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100295 else:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000296 if not token:
297 raise AuthException("Needed a token or Authorization http header",
298 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100299 try:
300 self.backend.validate_token(token)
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000301 self.check_permissions(self.tokens_cache[token], cherrypy.request.path_info,
302 cherrypy.request.method)
tierno0ea204e2019-01-25 14:16:24 +0000303 # TODO: check if this can be avoided. Backend may provide enough information
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000304 return deepcopy(self.tokens_cache[token])
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100305 except AuthException:
306 self.del_token(token)
307 raise
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100308 except AuthException as e:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100309 if cherrypy.session.get('Authorization'):
310 del cherrypy.session['Authorization']
311 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100312 raise AuthException(str(e))
Eduardo Sousa2f988212018-07-26 01:04:11 +0100313
314 def new_token(self, session, indata, remote):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100315 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100316 return self._internal_new_token(session, indata, remote)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100317 else:
318 if indata.get("username"):
319 token, projects = self.backend.authenticate_with_user_password(
320 indata.get("username"), indata.get("password"))
321 elif session:
322 token, projects = self.backend.authenticate_with_token(
323 session.get("id"), indata.get("project_id"))
324 else:
325 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
326 http_code=HTTPStatus.UNAUTHORIZED)
327
328 if indata.get("project_id"):
329 project_id = indata.get("project_id")
330 if project_id not in projects:
331 raise AuthException("Project {} not allowed for this user".format(project_id),
332 http_code=HTTPStatus.UNAUTHORIZED)
333 else:
334 project_id = projects[0]
335
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000336 if not session:
337 token, projects = self.backend.authenticate_with_token(token, project_id)
338
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100339 if project_id == "admin":
340 session_admin = True
341 else:
342 session_admin = reduce(lambda x, y: x or (True if y == "admin" else False),
343 projects, False)
344
345 now = time()
346 new_session = {
347 "_id": token,
348 "id": token,
349 "issued_at": now,
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100350 "expires": now + 3600,
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100351 "project_id": project_id,
352 "username": indata.get("username") if not session else session.get("username"),
353 "remote_port": remote.port,
354 "admin": session_admin
355 }
356
357 if remote.name:
358 new_session["remote_host"] = remote.name
359 elif remote.ip:
360 new_session["remote_host"] = remote.ip
361
tierno0ea204e2019-01-25 14:16:24 +0000362 # TODO: check if this can be avoided. Backend may provide enough information
363 self.tokens_cache[token] = new_session
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100364
365 return deepcopy(new_session)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100366
367 def get_token_list(self, session):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100368 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100369 return self._internal_get_token_list(session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100370 else:
tierno0ea204e2019-01-25 14:16:24 +0000371 # TODO: check if this can be avoided. Backend may provide enough information
372 return [deepcopy(token) for token in self.tokens_cache.values()
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100373 if token["username"] == session["username"]]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100374
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100375 def get_token(self, session, token):
376 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100377 return self._internal_get_token(session, token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100378 else:
tierno0ea204e2019-01-25 14:16:24 +0000379 # TODO: check if this can be avoided. Backend may provide enough information
380 token_value = self.tokens_cache.get(token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100381 if not token_value:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100382 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100383 if token_value["username"] != session["username"] and not session["admin"]:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100384 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100385 return token_value
Eduardo Sousa2f988212018-07-26 01:04:11 +0100386
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100387 def del_token(self, token):
388 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100389 return self._internal_del_token(token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100390 else:
391 try:
392 self.backend.revoke_token(token)
tierno0ea204e2019-01-25 14:16:24 +0000393 del self.tokens_cache[token]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100394 return "token '{}' deleted".format(token)
395 except KeyError:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100396 raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
397
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000398 def check_permissions(self, session, url, method):
399 self.logger.info("Session: {}".format(session))
400 self.logger.info("URL: {}".format(url))
401 self.logger.info("Method: {}".format(method))
402
403 key, parameters = self._normalize_url(url, method)
404
405 # TODO: Check if parameters might be useful for the decision
406
407 operation = self.resources_to_operations_mapping[key]
408 roles_required = self.operation_to_allowed_roles[operation]
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100409 roles_allowed = self.backend.get_user_role_list(session["id"])
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000410
411 if "anonymous" in roles_required:
412 return
413
414 for role in roles_allowed:
415 if role in roles_required:
416 return
417
418 raise AuthException("Access denied: lack of permissions.")
419
Eduardo Sousa5c01e192019-05-08 02:35:47 +0100420 def get_user_list(self):
421 return self.backend.get_user_list()
422
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000423 def _normalize_url(self, url, method):
424 # Removing query strings
425 normalized_url = url if '?' not in url else url[:url.find("?")]
426 normalized_url_splitted = normalized_url.split("/")
427 parameters = {}
428
429 filtered_keys = [key for key in self.resources_to_operations_mapping.keys()
430 if method in key.split()[0]]
431
432 for idx, path_part in enumerate(normalized_url_splitted):
433 tmp_keys = []
434 for tmp_key in filtered_keys:
435 splitted = tmp_key.split()[1].split("/")
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000436 if idx >= len(splitted):
437 continue
438 elif "<" in splitted[idx] and ">" in splitted[idx]:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000439 if splitted[idx] == "<artifactPath>":
440 tmp_keys.append(tmp_key)
441 continue
442 elif idx == len(normalized_url_splitted) - 1 and \
443 len(normalized_url_splitted) != len(splitted):
444 continue
445 else:
446 tmp_keys.append(tmp_key)
447 elif splitted[idx] == path_part:
448 if idx == len(normalized_url_splitted) - 1 and \
449 len(normalized_url_splitted) != len(splitted):
450 continue
451 else:
452 tmp_keys.append(tmp_key)
453 filtered_keys = tmp_keys
454 if len(filtered_keys) == 1 and \
455 filtered_keys[0].split("/")[-1] == "<artifactPath>":
456 break
457
458 if len(filtered_keys) == 0:
459 raise AuthException("Cannot make an authorization decision. URL not found. URL: {0}".format(url))
460 elif len(filtered_keys) > 1:
461 raise AuthException("Cannot make an authorization decision. Multiple URLs found. URL: {0}".format(url))
462
463 filtered_key = filtered_keys[0]
464
465 for idx, path_part in enumerate(filtered_key.split()[1].split("/")):
466 if "<" in path_part and ">" in path_part:
467 if path_part == "<artifactPath>":
468 parameters[path_part[1:-1]] = "/".join(normalized_url_splitted[idx:])
469 else:
470 parameters[path_part[1:-1]] = normalized_url_splitted[idx]
471
472 return filtered_key, parameters
473
tierno0ea204e2019-01-25 14:16:24 +0000474 def _internal_authorize(self, token_id):
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100475 try:
tierno0ea204e2019-01-25 14:16:24 +0000476 if not token_id:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100477 raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
tierno0ea204e2019-01-25 14:16:24 +0000478 # try to get from cache first
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100479 now = time()
tierno0ea204e2019-01-25 14:16:24 +0000480 session = self.tokens_cache.get(token_id)
481 if session and session["expires"] < now:
482 del self.tokens_cache[token_id]
483 session = None
484 if session:
485 return session
486
487 # get from database if not in cache
488 session = self.db.get_one("tokens", {"_id": token_id})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100489 if session["expires"] < now:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100490 raise AuthException("Expired Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
tierno0ea204e2019-01-25 14:16:24 +0000491 self.tokens_cache[token_id] = session
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100492 return session
tierno0ea204e2019-01-25 14:16:24 +0000493 except DbException as e:
494 if e.http_code == HTTPStatus.NOT_FOUND:
495 raise AuthException("Invalid Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
496 else:
497 raise
498
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100499 except AuthException:
500 if self.config["global"].get("test.user_not_authorized"):
501 return {"id": "fake-token-id-for-test",
502 "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
503 "username": self.config["global"]["test.user_not_authorized"]}
504 else:
505 raise
506
507 def _internal_new_token(self, session, indata, remote):
508 now = time()
509 user_content = None
510
511 # Try using username/password
512 if indata.get("username"):
513 user_rows = self.db.get_list("users", {"username": indata.get("username")})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100514 if user_rows:
515 user_content = user_rows[0]
516 salt = user_content["_admin"]["salt"]
517 shadow_password = sha256(indata.get("password", "").encode('utf-8') + salt.encode('utf-8')).hexdigest()
518 if shadow_password != user_content["password"]:
519 user_content = None
520 if not user_content:
521 raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
522 elif session:
523 user_rows = self.db.get_list("users", {"username": session["username"]})
524 if user_rows:
525 user_content = user_rows[0]
526 else:
527 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
528 else:
529 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
530 http_code=HTTPStatus.UNAUTHORIZED)
531
532 token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
533 for _ in range(0, 32))
534 if indata.get("project_id"):
535 project_id = indata.get("project_id")
536 if project_id not in user_content["projects"]:
537 raise AuthException("project {} not allowed for this user"
538 .format(project_id), http_code=HTTPStatus.UNAUTHORIZED)
539 else:
540 project_id = user_content["projects"][0]
541 if project_id == "admin":
542 session_admin = True
543 else:
544 project = self.db.get_one("projects", {"_id": project_id})
545 session_admin = project.get("admin", False)
546 new_session = {"issued_at": now, "expires": now + 3600,
547 "_id": token_id, "id": token_id, "project_id": project_id, "username": user_content["username"],
548 "remote_port": remote.port, "admin": session_admin}
549 if remote.name:
550 new_session["remote_host"] = remote.name
551 elif remote.ip:
552 new_session["remote_host"] = remote.ip
553
tierno0ea204e2019-01-25 14:16:24 +0000554 self.tokens_cache[token_id] = new_session
555 self.db.create("tokens", new_session)
556 # check if database must be prune
557 self._internal_tokens_prune(now)
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100558 return deepcopy(new_session)
559
560 def _internal_get_token_list(self, session):
tierno0ea204e2019-01-25 14:16:24 +0000561 now = time()
562 token_list = self.db.get_list("tokens", {"username": session["username"], "expires.gt": now})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100563 return token_list
564
565 def _internal_get_token(self, session, token_id):
tierno0ea204e2019-01-25 14:16:24 +0000566 token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False)
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100567 if not token_value:
568 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
569 if token_value["username"] != session["username"] and not session["admin"]:
570 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
571 return token_value
572
573 def _internal_del_token(self, token_id):
574 try:
tierno0ea204e2019-01-25 14:16:24 +0000575 self.tokens_cache.pop(token_id, None)
576 self.db.del_one("tokens", {"_id": token_id})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100577 return "token '{}' deleted".format(token_id)
tierno0ea204e2019-01-25 14:16:24 +0000578 except DbException as e:
579 if e.http_code == HTTPStatus.NOT_FOUND:
580 raise AuthException("Token '{}' not found".format(token_id), http_code=HTTPStatus.NOT_FOUND)
581 else:
582 raise
583
584 def _internal_tokens_prune(self, now=None):
585 now = now or time()
586 if not self.next_db_prune_time or self.next_db_prune_time >= now:
587 self.db.del_list("tokens", {"expires.lt": now})
588 self.next_db_prune_time = self.periodin_db_pruning + now
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000589 self.tokens_cache.clear() # force to reload tokens from database