blob: 18986a3ab4d7e64a00280519a495fb37d510ea40 [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"""
Eduardo Sousa29933fc2018-11-14 06:36:35 +000028from os import path
Eduardo Sousa819d34c2018-07-31 01:20:02 +010029
tierno0ea204e2019-01-25 14:16:24 +000030__author__ = "Eduardo Sousa <esousa@whitestack.com>; Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
Eduardo Sousa819d34c2018-07-31 01:20:02 +010031__date__ = "$27-jul-2018 23:59:59$"
32
Eduardo Sousad1b525d2018-10-04 04:24:18 +010033import cherrypy
Eduardo Sousa819d34c2018-07-31 01:20:02 +010034import logging
Eduardo Sousa29933fc2018-11-14 06:36:35 +000035import yaml
Eduardo Sousa2f988212018-07-26 01:04:11 +010036from base64 import standard_b64decode
Eduardo Sousa819d34c2018-07-31 01:20:02 +010037from copy import deepcopy
38from functools import reduce
Eduardo Sousad1b525d2018-10-04 04:24:18 +010039from hashlib import sha256
Eduardo Sousa2f988212018-07-26 01:04:11 +010040from http import HTTPStatus
Eduardo Sousad1b525d2018-10-04 04:24:18 +010041from random import choice as random_choice
Eduardo Sousa819d34c2018-07-31 01:20:02 +010042from time import time
Eduardo Sousa29933fc2018-11-14 06:36:35 +000043from uuid import uuid4
Eduardo Sousa2f988212018-07-26 01:04:11 +010044
Eduardo Sousa819d34c2018-07-31 01:20:02 +010045from authconn import AuthException
46from authconn_keystone import AuthconnKeystone
Eduardo Sousad1b525d2018-10-04 04:24:18 +010047from osm_common import dbmongo
48from osm_common import dbmemory
49from osm_common.dbbase import DbException
Eduardo Sousa2f988212018-07-26 01:04:11 +010050
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.
58 """
Eduardo Sousa2f988212018-07-26 01:04:11 +010059
Eduardo Sousa29933fc2018-11-14 06:36:35 +000060 periodin_db_pruning = 60 * 30 # for the internal backend only. every 30 minutes expired tokens will be pruned
tierno0ea204e2019-01-25 14:16:24 +000061
Eduardo Sousad1b525d2018-10-04 04:24:18 +010062 def __init__(self):
Eduardo Sousa819d34c2018-07-31 01:20:02 +010063 """
64 Authenticator initializer. Setup the initial state of the object,
65 while it waits for the config dictionary and database initialization.
Eduardo Sousa819d34c2018-07-31 01:20:02 +010066 """
Eduardo Sousa819d34c2018-07-31 01:20:02 +010067 self.backend = None
68 self.config = None
69 self.db = None
tierno0ea204e2019-01-25 14:16:24 +000070 self.tokens_cache = dict()
71 self.next_db_prune_time = 0 # time when next cleaning of expired tokens must be done
Eduardo Sousa29933fc2018-11-14 06:36:35 +000072 self.resources_to_operations_file = None
73 self.roles_to_operations_file = None
74 self.resources_to_operations_mapping = {}
75 self.operation_to_allowed_roles = {}
Eduardo Sousa819d34c2018-07-31 01:20:02 +010076 self.logger = logging.getLogger("nbi.authenticator")
77
78 def start(self, config):
79 """
80 Method to configure the Authenticator object. This method should be called
81 after object creation. It is responsible by initializing the selected backend,
82 as well as the initialization of the database connection.
83
84 :param config: dictionary containing the relevant parameters for this object.
85 """
86 self.config = config
87
88 try:
Eduardo Sousa819d34c2018-07-31 01:20:02 +010089 if not self.db:
Eduardo Sousad1b525d2018-10-04 04:24:18 +010090 if config["database"]["driver"] == "mongo":
91 self.db = dbmongo.DbMongo()
92 self.db.db_connect(config["database"])
93 elif config["database"]["driver"] == "memory":
94 self.db = dbmemory.DbMemory()
95 self.db.db_connect(config["database"])
96 else:
97 raise AuthException("Invalid configuration param '{}' at '[database]':'driver'"
98 .format(config["database"]["driver"]))
tierno0ea204e2019-01-25 14:16:24 +000099 if not self.backend:
100 if config["authentication"]["backend"] == "keystone":
101 self.backend = AuthconnKeystone(self.config["authentication"])
102 elif config["authentication"]["backend"] == "internal":
103 self._internal_tokens_prune()
104 else:
105 raise AuthException("Unknown authentication backend: {}"
106 .format(config["authentication"]["backend"]))
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000107 if not self.resources_to_operations_file:
108 if "resources_to_operations" in config["rbac"]:
109 self.resources_to_operations_file = config["rbac"]["resources_to_operations"]
110 else:
111 for config_file in (__file__[:__file__.rfind("auth.py")] + "resources_to_operations.yml",
112 "./resources_to_operations.yml"):
113 if path.isfile(config_file):
114 self.resources_to_operations_file = config_file
115 break
116 if not self.resources_to_operations_file:
117 raise AuthException("Invalid permission configuration: resources_to_operations file missing")
118 if not self.roles_to_operations_file:
119 if "roles_to_operations" in config["rbac"]:
120 self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
121 else:
122 for config_file in (__file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml",
123 "./roles_to_operations.yml"):
124 if path.isfile(config_file):
125 self.roles_to_operations_file = config_file
126 break
127 if not self.roles_to_operations_file:
128 raise AuthException("Invalid permission configuration: roles_to_operations file missing")
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100129 except Exception as e:
130 raise AuthException(str(e))
131
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100132 def stop(self):
133 try:
134 if self.db:
135 self.db.db_disconnect()
136 except DbException as e:
137 raise AuthException(str(e), http_code=e.http_code)
138
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000139 def init_db(self, target_version='1.0'):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100140 """
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000141 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 +0100142 and insert the predefined mappings between roles and permissions.
143
144 :param target_version: schema version that should be present in the database.
145 :return: None if OK, exception if error or version is different.
146 """
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000147 # Always reads operation to resource mapping from file (this is static, no need to store it in MongoDB)
148 # Operations encoding: "<METHOD> <URL>"
149 # Note: it is faster to rewrite the value than to check if it is already there or not
150 operations = []
151 with open(self.resources_to_operations_file, "r") as stream:
152 resources_to_operations_yaml = yaml.load(stream)
153
154 for resource, operation in resources_to_operations_yaml["resources_to_operations"].items():
155 operation_key = operation.replace(".", ":")
156 if operation_key not in operations:
157 operations.append(operation_key)
158 self.resources_to_operations_mapping[resource] = operation_key
159
160 records = self.db.get_list("roles_operations")
161
162 # Loading permissions to MongoDB. If there are permissions already in MongoDB, do nothing.
163 if len(records) == 0:
164 with open(self.roles_to_operations_file, "r") as stream:
165 roles_to_operations_yaml = yaml.load(stream)
166
167 roles = []
168 for role_with_operations in roles_to_operations_yaml["roles_to_operations"]:
169 # Verifying if role already exists. If it does, send warning to log and ignore it.
170 if role_with_operations["role"] not in roles:
171 roles.append(role_with_operations["role"])
172 else:
173 self.logger.warning("Duplicated role with name: {0}. Role definition is ignored."
174 .format(role_with_operations["role"]))
175 continue
176
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000177 role_ops = {}
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000178 root = None
179
180 if not role_with_operations["operations"]:
181 continue
182
183 for operation, is_allowed in role_with_operations["operations"].items():
184 if not isinstance(is_allowed, bool):
185 continue
186
187 if operation == ".":
188 root = is_allowed
189 continue
190
191 if len(operation) != 1 and operation[-1] == ".":
192 self.logger.warning("Invalid operation {0} terminated in '.'. "
193 "Operation will be discarded"
194 .format(operation))
195 continue
196
197 operation_key = operation.replace(".", ":")
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000198 if operation_key not in role_ops.keys():
199 role_ops[operation_key] = is_allowed
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000200 else:
201 self.logger.info("In role {0}, the operation {1} with the value {2} was discarded due to "
202 "repetition.".format(role_with_operations["role"], operation, is_allowed))
203
204 if not root:
205 root = False
206 self.logger.info("Root for role {0} not defined. Default value 'False' applied."
207 .format(role_with_operations["role"]))
208
209 now = time()
210 operation_to_roles_item = {
211 "_id": str(uuid4()),
212 "_admin": {
213 "created": now,
214 "modified": now,
215 },
216 "role": role_with_operations["role"],
217 "root": root
218 }
219
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000220 for operation, value in role_ops.items():
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000221 operation_to_roles_item[operation] = value
222
223 self.db.create("roles_operations", operation_to_roles_item)
224
225 permissions = {oper: [] for oper in operations}
226 records = self.db.get_list("roles_operations")
227
228 ignore_fields = ["_id", "_admin", "role", "root"]
229 roles = []
230 for record in records:
231
232 roles.append(record["role"])
233 record_permissions = {oper: record["root"] for oper in operations}
234 operations_joined = [(oper, value) for oper, value in record.items() if oper not in ignore_fields]
235 operations_joined.sort(key=lambda x: x[0].count(":"))
236
237 for oper in operations_joined:
238 match = list(filter(lambda x: x.find(oper[0]) == 0, record_permissions.keys()))
239
240 for m in match:
241 record_permissions[m] = oper[1]
242
243 allowed_operations = [k for k, v in record_permissions.items() if v is True]
244
245 for allowed_op in allowed_operations:
246 permissions[allowed_op].append(record["role"])
247
248 for oper, role_list in permissions.items():
249 self.operation_to_allowed_roles[oper] = role_list
250
251 if self.config["authentication"]["backend"] != "internal":
252 for role in roles:
253 if role == "anonymous":
254 continue
255 self.backend.create_role(role)
256
257 self.backend.assign_role_to_user("admin", "admin", "system_admin")
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100258
Eduardo Sousa2f988212018-07-26 01:04:11 +0100259 def authorize(self):
260 token = None
261 user_passwd64 = None
262 try:
263 # 1. Get token Authorization bearer
264 auth = cherrypy.request.headers.get("Authorization")
265 if auth:
266 auth_list = auth.split(" ")
267 if auth_list[0].lower() == "bearer":
268 token = auth_list[-1]
269 elif auth_list[0].lower() == "basic":
270 user_passwd64 = auth_list[-1]
271 if not token:
272 if cherrypy.session.get("Authorization"):
273 # 2. Try using session before request a new token. If not, basic authentication will generate
274 token = cherrypy.session.get("Authorization")
275 if token == "logout":
tierno0ea204e2019-01-25 14:16:24 +0000276 token = None # force Unauthorized response to insert user password again
Eduardo Sousa2f988212018-07-26 01:04:11 +0100277 elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
278 # 3. Get new token from user password
279 user = None
280 passwd = None
281 try:
282 user_passwd = standard_b64decode(user_passwd64).decode()
283 user, _, passwd = user_passwd.partition(":")
284 except Exception:
285 pass
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100286 outdata = self.new_token(None, {"username": user, "password": passwd})
Eduardo Sousa2f988212018-07-26 01:04:11 +0100287 token = outdata["id"]
288 cherrypy.session['Authorization'] = token
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100289 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100290 return self._internal_authorize(token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100291 else:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000292 if not token:
293 raise AuthException("Needed a token or Authorization http header",
294 http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100295 try:
296 self.backend.validate_token(token)
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000297 self.check_permissions(self.tokens_cache[token], cherrypy.request.path_info,
298 cherrypy.request.method)
tierno0ea204e2019-01-25 14:16:24 +0000299 # TODO: check if this can be avoided. Backend may provide enough information
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000300 return deepcopy(self.tokens_cache[token])
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100301 except AuthException:
302 self.del_token(token)
303 raise
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100304 except AuthException as e:
Eduardo Sousa2f988212018-07-26 01:04:11 +0100305 if cherrypy.session.get('Authorization'):
306 del cherrypy.session['Authorization']
307 cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100308 raise AuthException(str(e))
Eduardo Sousa2f988212018-07-26 01:04:11 +0100309
310 def new_token(self, session, indata, remote):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100311 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100312 return self._internal_new_token(session, indata, remote)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100313 else:
314 if indata.get("username"):
315 token, projects = self.backend.authenticate_with_user_password(
316 indata.get("username"), indata.get("password"))
317 elif session:
318 token, projects = self.backend.authenticate_with_token(
319 session.get("id"), indata.get("project_id"))
320 else:
321 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
322 http_code=HTTPStatus.UNAUTHORIZED)
323
324 if indata.get("project_id"):
325 project_id = indata.get("project_id")
326 if project_id not in projects:
327 raise AuthException("Project {} not allowed for this user".format(project_id),
328 http_code=HTTPStatus.UNAUTHORIZED)
329 else:
330 project_id = projects[0]
331
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000332 if not session:
333 token, projects = self.backend.authenticate_with_token(token, project_id)
334
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100335 if project_id == "admin":
336 session_admin = True
337 else:
338 session_admin = reduce(lambda x, y: x or (True if y == "admin" else False),
339 projects, False)
340
341 now = time()
342 new_session = {
343 "_id": token,
344 "id": token,
345 "issued_at": now,
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100346 "expires": now + 3600,
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100347 "project_id": project_id,
348 "username": indata.get("username") if not session else session.get("username"),
349 "remote_port": remote.port,
350 "admin": session_admin
351 }
352
353 if remote.name:
354 new_session["remote_host"] = remote.name
355 elif remote.ip:
356 new_session["remote_host"] = remote.ip
357
tierno0ea204e2019-01-25 14:16:24 +0000358 # TODO: check if this can be avoided. Backend may provide enough information
359 self.tokens_cache[token] = new_session
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100360
361 return deepcopy(new_session)
Eduardo Sousa2f988212018-07-26 01:04:11 +0100362
363 def get_token_list(self, session):
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100364 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100365 return self._internal_get_token_list(session)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100366 else:
tierno0ea204e2019-01-25 14:16:24 +0000367 # TODO: check if this can be avoided. Backend may provide enough information
368 return [deepcopy(token) for token in self.tokens_cache.values()
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100369 if token["username"] == session["username"]]
Eduardo Sousa2f988212018-07-26 01:04:11 +0100370
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100371 def get_token(self, session, token):
372 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100373 return self._internal_get_token(session, token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100374 else:
tierno0ea204e2019-01-25 14:16:24 +0000375 # TODO: check if this can be avoided. Backend may provide enough information
376 token_value = self.tokens_cache.get(token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100377 if not token_value:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100378 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100379 if token_value["username"] != session["username"] and not session["admin"]:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100380 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100381 return token_value
Eduardo Sousa2f988212018-07-26 01:04:11 +0100382
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100383 def del_token(self, token):
384 if self.config["authentication"]["backend"] == "internal":
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100385 return self._internal_del_token(token)
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100386 else:
387 try:
388 self.backend.revoke_token(token)
tierno0ea204e2019-01-25 14:16:24 +0000389 del self.tokens_cache[token]
Eduardo Sousa819d34c2018-07-31 01:20:02 +0100390 return "token '{}' deleted".format(token)
391 except KeyError:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100392 raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
393
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000394 def check_permissions(self, session, url, method):
395 self.logger.info("Session: {}".format(session))
396 self.logger.info("URL: {}".format(url))
397 self.logger.info("Method: {}".format(method))
398
399 key, parameters = self._normalize_url(url, method)
400
401 # TODO: Check if parameters might be useful for the decision
402
403 operation = self.resources_to_operations_mapping[key]
404 roles_required = self.operation_to_allowed_roles[operation]
405 roles_allowed = self.backend.get_role_list(session["id"])
406
407 if "anonymous" in roles_required:
408 return
409
410 for role in roles_allowed:
411 if role in roles_required:
412 return
413
414 raise AuthException("Access denied: lack of permissions.")
415
416 def _normalize_url(self, url, method):
417 # Removing query strings
418 normalized_url = url if '?' not in url else url[:url.find("?")]
419 normalized_url_splitted = normalized_url.split("/")
420 parameters = {}
421
422 filtered_keys = [key for key in self.resources_to_operations_mapping.keys()
423 if method in key.split()[0]]
424
425 for idx, path_part in enumerate(normalized_url_splitted):
426 tmp_keys = []
427 for tmp_key in filtered_keys:
428 splitted = tmp_key.split()[1].split("/")
Eduardo Sousacc02e9a2019-03-20 17:32:36 +0000429 if idx >= len(splitted):
430 continue
431 elif "<" in splitted[idx] and ">" in splitted[idx]:
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000432 if splitted[idx] == "<artifactPath>":
433 tmp_keys.append(tmp_key)
434 continue
435 elif 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 elif splitted[idx] == path_part:
441 if idx == len(normalized_url_splitted) - 1 and \
442 len(normalized_url_splitted) != len(splitted):
443 continue
444 else:
445 tmp_keys.append(tmp_key)
446 filtered_keys = tmp_keys
447 if len(filtered_keys) == 1 and \
448 filtered_keys[0].split("/")[-1] == "<artifactPath>":
449 break
450
451 if len(filtered_keys) == 0:
452 raise AuthException("Cannot make an authorization decision. URL not found. URL: {0}".format(url))
453 elif len(filtered_keys) > 1:
454 raise AuthException("Cannot make an authorization decision. Multiple URLs found. URL: {0}".format(url))
455
456 filtered_key = filtered_keys[0]
457
458 for idx, path_part in enumerate(filtered_key.split()[1].split("/")):
459 if "<" in path_part and ">" in path_part:
460 if path_part == "<artifactPath>":
461 parameters[path_part[1:-1]] = "/".join(normalized_url_splitted[idx:])
462 else:
463 parameters[path_part[1:-1]] = normalized_url_splitted[idx]
464
465 return filtered_key, parameters
466
tierno0ea204e2019-01-25 14:16:24 +0000467 def _internal_authorize(self, token_id):
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100468 try:
tierno0ea204e2019-01-25 14:16:24 +0000469 if not token_id:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100470 raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
tierno0ea204e2019-01-25 14:16:24 +0000471 # try to get from cache first
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100472 now = time()
tierno0ea204e2019-01-25 14:16:24 +0000473 session = self.tokens_cache.get(token_id)
474 if session and session["expires"] < now:
475 del self.tokens_cache[token_id]
476 session = None
477 if session:
478 return session
479
480 # get from database if not in cache
481 session = self.db.get_one("tokens", {"_id": token_id})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100482 if session["expires"] < now:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100483 raise AuthException("Expired Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
tierno0ea204e2019-01-25 14:16:24 +0000484 self.tokens_cache[token_id] = session
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100485 return session
tierno0ea204e2019-01-25 14:16:24 +0000486 except DbException as e:
487 if e.http_code == HTTPStatus.NOT_FOUND:
488 raise AuthException("Invalid Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
489 else:
490 raise
491
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100492 except AuthException:
493 if self.config["global"].get("test.user_not_authorized"):
494 return {"id": "fake-token-id-for-test",
495 "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
496 "username": self.config["global"]["test.user_not_authorized"]}
497 else:
498 raise
499
500 def _internal_new_token(self, session, indata, remote):
501 now = time()
502 user_content = None
503
504 # Try using username/password
505 if indata.get("username"):
506 user_rows = self.db.get_list("users", {"username": indata.get("username")})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100507 if user_rows:
508 user_content = user_rows[0]
509 salt = user_content["_admin"]["salt"]
510 shadow_password = sha256(indata.get("password", "").encode('utf-8') + salt.encode('utf-8')).hexdigest()
511 if shadow_password != user_content["password"]:
512 user_content = None
513 if not user_content:
514 raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
515 elif session:
516 user_rows = self.db.get_list("users", {"username": session["username"]})
517 if user_rows:
518 user_content = user_rows[0]
519 else:
520 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
521 else:
522 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
523 http_code=HTTPStatus.UNAUTHORIZED)
524
525 token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
526 for _ in range(0, 32))
527 if indata.get("project_id"):
528 project_id = indata.get("project_id")
529 if project_id not in user_content["projects"]:
530 raise AuthException("project {} not allowed for this user"
531 .format(project_id), http_code=HTTPStatus.UNAUTHORIZED)
532 else:
533 project_id = user_content["projects"][0]
534 if project_id == "admin":
535 session_admin = True
536 else:
537 project = self.db.get_one("projects", {"_id": project_id})
538 session_admin = project.get("admin", False)
539 new_session = {"issued_at": now, "expires": now + 3600,
540 "_id": token_id, "id": token_id, "project_id": project_id, "username": user_content["username"],
541 "remote_port": remote.port, "admin": session_admin}
542 if remote.name:
543 new_session["remote_host"] = remote.name
544 elif remote.ip:
545 new_session["remote_host"] = remote.ip
546
tierno0ea204e2019-01-25 14:16:24 +0000547 self.tokens_cache[token_id] = new_session
548 self.db.create("tokens", new_session)
549 # check if database must be prune
550 self._internal_tokens_prune(now)
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100551 return deepcopy(new_session)
552
553 def _internal_get_token_list(self, session):
tierno0ea204e2019-01-25 14:16:24 +0000554 now = time()
555 token_list = self.db.get_list("tokens", {"username": session["username"], "expires.gt": now})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100556 return token_list
557
558 def _internal_get_token(self, session, token_id):
tierno0ea204e2019-01-25 14:16:24 +0000559 token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False)
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100560 if not token_value:
561 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
562 if token_value["username"] != session["username"] and not session["admin"]:
563 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
564 return token_value
565
566 def _internal_del_token(self, token_id):
567 try:
tierno0ea204e2019-01-25 14:16:24 +0000568 self.tokens_cache.pop(token_id, None)
569 self.db.del_one("tokens", {"_id": token_id})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100570 return "token '{}' deleted".format(token_id)
tierno0ea204e2019-01-25 14:16:24 +0000571 except DbException as e:
572 if e.http_code == HTTPStatus.NOT_FOUND:
573 raise AuthException("Token '{}' not found".format(token_id), http_code=HTTPStatus.NOT_FOUND)
574 else:
575 raise
576
577 def _internal_tokens_prune(self, now=None):
578 now = now or time()
579 if not self.next_db_prune_time or self.next_db_prune_time >= now:
580 self.db.del_list("tokens", {"expires.lt": now})
581 self.next_db_prune_time = self.periodin_db_pruning + now
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000582 self.tokens_cache.clear() # force to reload tokens from database