blob: 44eaa9438df3048cb976db0b830da3b899f9c282 [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
177 operations = {}
178 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(".", ":")
198 if operation_key not in operations.keys():
199 operations[operation_key] = is_allowed
200 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
220 for operation, value in operations.items():
221 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("/")
429 if "<" in splitted[idx] and ">" in splitted[idx]:
430 if splitted[idx] == "<artifactPath>":
431 tmp_keys.append(tmp_key)
432 continue
433 elif idx == len(normalized_url_splitted) - 1 and \
434 len(normalized_url_splitted) != len(splitted):
435 continue
436 else:
437 tmp_keys.append(tmp_key)
438 elif splitted[idx] == path_part:
439 if idx == len(normalized_url_splitted) - 1 and \
440 len(normalized_url_splitted) != len(splitted):
441 continue
442 else:
443 tmp_keys.append(tmp_key)
444 filtered_keys = tmp_keys
445 if len(filtered_keys) == 1 and \
446 filtered_keys[0].split("/")[-1] == "<artifactPath>":
447 break
448
449 if len(filtered_keys) == 0:
450 raise AuthException("Cannot make an authorization decision. URL not found. URL: {0}".format(url))
451 elif len(filtered_keys) > 1:
452 raise AuthException("Cannot make an authorization decision. Multiple URLs found. URL: {0}".format(url))
453
454 filtered_key = filtered_keys[0]
455
456 for idx, path_part in enumerate(filtered_key.split()[1].split("/")):
457 if "<" in path_part and ">" in path_part:
458 if path_part == "<artifactPath>":
459 parameters[path_part[1:-1]] = "/".join(normalized_url_splitted[idx:])
460 else:
461 parameters[path_part[1:-1]] = normalized_url_splitted[idx]
462
463 return filtered_key, parameters
464
tierno0ea204e2019-01-25 14:16:24 +0000465 def _internal_authorize(self, token_id):
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100466 try:
tierno0ea204e2019-01-25 14:16:24 +0000467 if not token_id:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100468 raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
tierno0ea204e2019-01-25 14:16:24 +0000469 # try to get from cache first
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100470 now = time()
tierno0ea204e2019-01-25 14:16:24 +0000471 session = self.tokens_cache.get(token_id)
472 if session and session["expires"] < now:
473 del self.tokens_cache[token_id]
474 session = None
475 if session:
476 return session
477
478 # get from database if not in cache
479 session = self.db.get_one("tokens", {"_id": token_id})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100480 if session["expires"] < now:
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100481 raise AuthException("Expired Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
tierno0ea204e2019-01-25 14:16:24 +0000482 self.tokens_cache[token_id] = session
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100483 return session
tierno0ea204e2019-01-25 14:16:24 +0000484 except DbException as e:
485 if e.http_code == HTTPStatus.NOT_FOUND:
486 raise AuthException("Invalid Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED)
487 else:
488 raise
489
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100490 except AuthException:
491 if self.config["global"].get("test.user_not_authorized"):
492 return {"id": "fake-token-id-for-test",
493 "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
494 "username": self.config["global"]["test.user_not_authorized"]}
495 else:
496 raise
497
498 def _internal_new_token(self, session, indata, remote):
499 now = time()
500 user_content = None
501
502 # Try using username/password
503 if indata.get("username"):
504 user_rows = self.db.get_list("users", {"username": indata.get("username")})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100505 if user_rows:
506 user_content = user_rows[0]
507 salt = user_content["_admin"]["salt"]
508 shadow_password = sha256(indata.get("password", "").encode('utf-8') + salt.encode('utf-8')).hexdigest()
509 if shadow_password != user_content["password"]:
510 user_content = None
511 if not user_content:
512 raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
513 elif session:
514 user_rows = self.db.get_list("users", {"username": session["username"]})
515 if user_rows:
516 user_content = user_rows[0]
517 else:
518 raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
519 else:
520 raise AuthException("Provide credentials: username/password or Authorization Bearer token",
521 http_code=HTTPStatus.UNAUTHORIZED)
522
523 token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
524 for _ in range(0, 32))
525 if indata.get("project_id"):
526 project_id = indata.get("project_id")
527 if project_id not in user_content["projects"]:
528 raise AuthException("project {} not allowed for this user"
529 .format(project_id), http_code=HTTPStatus.UNAUTHORIZED)
530 else:
531 project_id = user_content["projects"][0]
532 if project_id == "admin":
533 session_admin = True
534 else:
535 project = self.db.get_one("projects", {"_id": project_id})
536 session_admin = project.get("admin", False)
537 new_session = {"issued_at": now, "expires": now + 3600,
538 "_id": token_id, "id": token_id, "project_id": project_id, "username": user_content["username"],
539 "remote_port": remote.port, "admin": session_admin}
540 if remote.name:
541 new_session["remote_host"] = remote.name
542 elif remote.ip:
543 new_session["remote_host"] = remote.ip
544
tierno0ea204e2019-01-25 14:16:24 +0000545 self.tokens_cache[token_id] = new_session
546 self.db.create("tokens", new_session)
547 # check if database must be prune
548 self._internal_tokens_prune(now)
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100549 return deepcopy(new_session)
550
551 def _internal_get_token_list(self, session):
tierno0ea204e2019-01-25 14:16:24 +0000552 now = time()
553 token_list = self.db.get_list("tokens", {"username": session["username"], "expires.gt": now})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100554 return token_list
555
556 def _internal_get_token(self, session, token_id):
tierno0ea204e2019-01-25 14:16:24 +0000557 token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False)
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100558 if not token_value:
559 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
560 if token_value["username"] != session["username"] and not session["admin"]:
561 raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
562 return token_value
563
564 def _internal_del_token(self, token_id):
565 try:
tierno0ea204e2019-01-25 14:16:24 +0000566 self.tokens_cache.pop(token_id, None)
567 self.db.del_one("tokens", {"_id": token_id})
Eduardo Sousad1b525d2018-10-04 04:24:18 +0100568 return "token '{}' deleted".format(token_id)
tierno0ea204e2019-01-25 14:16:24 +0000569 except DbException as e:
570 if e.http_code == HTTPStatus.NOT_FOUND:
571 raise AuthException("Token '{}' not found".format(token_id), http_code=HTTPStatus.NOT_FOUND)
572 else:
573 raise
574
575 def _internal_tokens_prune(self, now=None):
576 now = now or time()
577 if not self.next_db_prune_time or self.next_db_prune_time >= now:
578 self.db.del_list("tokens", {"expires.lt": now})
579 self.next_db_prune_time = self.periodin_db_pruning + now
Eduardo Sousa29933fc2018-11-14 06:36:35 +0000580 self.tokens_cache.clear() # force to reload tokens from database