X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fauth.py;h=90fc1e22d8a63d07fcf743ec7f956c1bd4171b12;hp=572ab88f01e87b8b2ee29275ccd59fe8cb4676a5;hb=b24258aa9716c1e375fde230a817f7c9faaf6c2a;hpb=2f98821b1da7d26fd54f631330bc8e1aa1e8f631 diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index 572ab88..90fc1e2 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -1,24 +1,98 @@ +# -*- coding: utf-8 -*- + +""" +Authenticator is responsible for authenticating the users, +create the tokens unscoped and scoped, retrieve the role +list inside the projects that they are inserted +""" + +__author__ = "Eduardo Sousa " +__date__ = "$27-jul-2018 23:59:59$" + import cherrypy +import logging from base64 import standard_b64decode +from copy import deepcopy +from functools import reduce +from hashlib import sha256 from http import HTTPStatus +from random import choice as random_choice +from time import time +from authconn import AuthException +from authconn_keystone import AuthconnKeystone +from osm_common import dbmongo +from osm_common import dbmemory +from osm_common.dbbase import DbException -from engine import EngineException -__author__ = "Eduardo Sousa " +class Authenticator: + """ + This class should hold all the mechanisms for User Authentication and + Authorization. Initially it should support Openstack Keystone as a + backend through a plugin model where more backends can be added and a + RBAC model to manage permissions on operations. + """ + def __init__(self): + """ + Authenticator initializer. Setup the initial state of the object, + while it waits for the config dictionary and database initialization. + """ + self.backend = None + self.config = None + self.db = None + self.tokens = dict() + self.logger = logging.getLogger("nbi.authenticator") -class AuthenticatorException(Exception): - def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED): - self.http_code = http_code - Exception.__init__(self, message) + def start(self, config): + """ + Method to configure the Authenticator object. This method should be called + after object creation. It is responsible by initializing the selected backend, + as well as the initialization of the database connection. + + :param config: dictionary containing the relevant parameters for this object. + """ + self.config = config + + try: + if not self.backend: + if config["authentication"]["backend"] == "keystone": + self.backend = AuthconnKeystone(self.config["authentication"]) + elif config["authentication"]["backend"] == "internal": + pass + else: + raise AuthException("Unknown authentication backend: {}" + .format(config["authentication"]["backend"])) + if not self.db: + if config["database"]["driver"] == "mongo": + self.db = dbmongo.DbMongo() + self.db.db_connect(config["database"]) + elif config["database"]["driver"] == "memory": + self.db = dbmemory.DbMemory() + self.db.db_connect(config["database"]) + else: + raise AuthException("Invalid configuration param '{}' at '[database]':'driver'" + .format(config["database"]["driver"])) + except Exception as e: + raise AuthException(str(e)) + def stop(self): + try: + if self.db: + self.db.db_disconnect() + except DbException as e: + raise AuthException(str(e), http_code=e.http_code) -class Authenticator(object): - def __init__(self, engine): - super().__init__() + def init_db(self, target_version='1.0'): + """ + Check if the database has been initialized. If not, create the required tables + and insert the predefined mappings between roles and permissions. - self.engine = engine + :param target_version: schema version that should be present in the database. + :return: None if OK, exception if error or version is different. + """ + pass def authorize(self): token = None @@ -47,29 +121,191 @@ class Authenticator(object): user, _, passwd = user_passwd.partition(":") except Exception: pass - outdata = self.engine.new_token(None, {"username": user, "password": passwd}) + outdata = self.new_token(None, {"username": user, "password": passwd}) token = outdata["id"] cherrypy.session['Authorization'] = token - # 4. Get token from cookie - # if not token: - # auth_cookie = cherrypy.request.cookie.get("Authorization") - # if auth_cookie: - # token = auth_cookie.value - return self.engine.authorize(token) - except EngineException as e: + if self.config["authentication"]["backend"] == "internal": + return self._internal_authorize(token) + else: + try: + self.backend.validate_token(token) + return self.tokens[token] + except AuthException: + self.del_token(token) + raise + except AuthException as e: if cherrypy.session.get('Authorization'): del cherrypy.session['Authorization'] cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e) - raise AuthenticatorException(str(e)) + raise AuthException(str(e)) def new_token(self, session, indata, remote): - return self.engine.new_token(session, indata, remote) + if self.config["authentication"]["backend"] == "internal": + return self._internal_new_token(session, indata, remote) + else: + if indata.get("username"): + token, projects = self.backend.authenticate_with_user_password( + indata.get("username"), indata.get("password")) + elif session: + token, projects = self.backend.authenticate_with_token( + session.get("id"), indata.get("project_id")) + else: + raise AuthException("Provide credentials: username/password or Authorization Bearer token", + http_code=HTTPStatus.UNAUTHORIZED) + + if indata.get("project_id"): + project_id = indata.get("project_id") + if project_id not in projects: + raise AuthException("Project {} not allowed for this user".format(project_id), + http_code=HTTPStatus.UNAUTHORIZED) + else: + project_id = projects[0] + + if project_id == "admin": + session_admin = True + else: + session_admin = reduce(lambda x, y: x or (True if y == "admin" else False), + projects, False) + + now = time() + new_session = { + "_id": token, + "id": token, + "issued_at": now, + "expires": now + 3600, + "project_id": project_id, + "username": indata.get("username") if not session else session.get("username"), + "remote_port": remote.port, + "admin": session_admin + } + + if remote.name: + new_session["remote_host"] = remote.name + elif remote.ip: + new_session["remote_host"] = remote.ip + + self.tokens[token] = new_session + + return deepcopy(new_session) def get_token_list(self, session): - return self.engine.get_token_list(session) + if self.config["authentication"]["backend"] == "internal": + return self._internal_get_token_list(session) + else: + return [deepcopy(token) for token in self.tokens.values() + if token["username"] == session["username"]] + + def get_token(self, session, token): + if self.config["authentication"]["backend"] == "internal": + return self._internal_get_token(session, token) + else: + token_value = self.tokens.get(token) + if not token_value: + raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND) + if token_value["username"] != session["username"] and not session["admin"]: + raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) + return token_value + + def del_token(self, token): + if self.config["authentication"]["backend"] == "internal": + return self._internal_del_token(token) + else: + try: + self.backend.revoke_token(token) + del self.tokens[token] + return "token '{}' deleted".format(token) + except KeyError: + raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND) + + def _internal_authorize(self, token): + try: + if not token: + raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) + if token not in self.tokens: + raise AuthException("Invalid token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) + session = self.tokens[token] + now = time() + if session["expires"] < now: + del self.tokens[token] + raise AuthException("Expired Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) + return session + except AuthException: + if self.config["global"].get("test.user_not_authorized"): + return {"id": "fake-token-id-for-test", + "project_id": self.config["global"].get("test.project_not_authorized", "admin"), + "username": self.config["global"]["test.user_not_authorized"]} + else: + raise - def get_token(self, session, token_id): - return self.engine.get_token(session, token_id) + def _internal_new_token(self, session, indata, remote): + now = time() + user_content = None - def del_token(self, token_id): - return self.engine.del_token(token_id) + # Try using username/password + if indata.get("username"): + user_rows = self.db.get_list("users", {"username": indata.get("username")}) + user_content = None + if user_rows: + user_content = user_rows[0] + salt = user_content["_admin"]["salt"] + shadow_password = sha256(indata.get("password", "").encode('utf-8') + salt.encode('utf-8')).hexdigest() + if shadow_password != user_content["password"]: + user_content = None + if not user_content: + raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED) + elif session: + user_rows = self.db.get_list("users", {"username": session["username"]}) + if user_rows: + user_content = user_rows[0] + else: + raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED) + else: + raise AuthException("Provide credentials: username/password or Authorization Bearer token", + http_code=HTTPStatus.UNAUTHORIZED) + + token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') + for _ in range(0, 32)) + if indata.get("project_id"): + project_id = indata.get("project_id") + if project_id not in user_content["projects"]: + raise AuthException("project {} not allowed for this user" + .format(project_id), http_code=HTTPStatus.UNAUTHORIZED) + else: + project_id = user_content["projects"][0] + if project_id == "admin": + session_admin = True + else: + project = self.db.get_one("projects", {"_id": project_id}) + session_admin = project.get("admin", False) + new_session = {"issued_at": now, "expires": now + 3600, + "_id": token_id, "id": token_id, "project_id": project_id, "username": user_content["username"], + "remote_port": remote.port, "admin": session_admin} + if remote.name: + new_session["remote_host"] = remote.name + elif remote.ip: + new_session["remote_host"] = remote.ip + + self.tokens[token_id] = new_session + return deepcopy(new_session) + + def _internal_get_token_list(self, session): + token_list = [] + for token_id, token_value in self.tokens.items(): + if token_value["username"] == session["username"]: + token_list.append(deepcopy(token_value)) + return token_list + + def _internal_get_token(self, session, token_id): + token_value = self.tokens.get(token_id) + if not token_value: + raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND) + if token_value["username"] != session["username"] and not session["admin"]: + raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) + return token_value + + def _internal_del_token(self, token_id): + try: + del self.tokens[token_id] + return "token '{}' deleted".format(token_id) + except KeyError: + raise AuthException("Token '{}' not found".format(token_id), http_code=HTTPStatus.NOT_FOUND)