X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fauthconn_internal.py;fp=osm_nbi%2Fauthconn_internal.py;h=5e35e8f481b90a7500979ee411a2b2f394ffe0c2;hp=0000000000000000000000000000000000000000;hb=ceb8baf31217c9d50ce7017beb25c59163d79b6e;hpb=ace34903be528e8e10e096d3d059b81df30ddaa9 diff --git a/osm_nbi/authconn_internal.py b/osm_nbi/authconn_internal.py new file mode 100644 index 0000000..5e35e8f --- /dev/null +++ b/osm_nbi/authconn_internal.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- + +# Copyright 2018 Telefonica S.A. +# Copyright 2018 ALTRAN Innovación S.L. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact: esousa@whitestack.com or glavado@whitestack.com +## + +""" +AuthconnInternal implements implements the connector for +OSM Internal Authentication Backend and leverages the RBAC model +""" + +__author__ = "Pedro de la Cruz Ramos " +__date__ = "$06-jun-2019 11:16:08$" + +from authconn import Authconn, AuthException +from osm_common.dbbase import DbException +from base_topic import BaseTopic + +import logging +from time import time +from http import HTTPStatus +from uuid import uuid4 +from hashlib import sha256 +from copy import deepcopy +from random import choice as random_choice + + +class AuthconnInternal(Authconn): + def __init__(self, config, db, token_cache): + Authconn.__init__(self, config) + + self.logger = logging.getLogger("nbi.authenticator.internal") + + # Get Configuration + # self.xxx = config.get("xxx", "default") + + self.db = db + self.token_cache = token_cache + + # To be Confirmed + self.auth = None + self.sess = None + + # def create_token (self, user, password, projects=[], project=None, remote=None): + # Not Required + + # def authenticate_with_user_password(self, user, password, project=None, remote=None): + # Not Required + + # def authenticate_with_token(self, token, project=None, remote=None): + # Not Required + + # def get_user_project_list(self, token): + # Not Required + + # def get_user_role_list(self, token): + # Not Required + + # def create_user(self, user, password): + # Not Required + + # def change_password(self, user, new_password): + # Not Required + + # def delete_user(self, user_id): + # Not Required + + # def get_user_list(self, filter_q={}): + # Not Required + + # def get_project_list(self, filter_q={}): + # Not required + + # def create_project(self, project): + # Not required + + # def delete_project(self, project_id): + # Not required + + # def assign_role_to_user(self, user, project, role): + # Not required in Phase 1 + + # def remove_role_from_user(self, user, project, role): + # Not required in Phase 1 + + def validate_token(self, token): + """ + Check if the token is valid. + + :param token: token to validate + :return: dictionary with information associated with the token: + "_id": token id + "project_id": project id + "project_name": project name + "user_id": user id + "username": user name + "roles": list with dict containing {name, id} + "expires": expiration date + If the token is not valid an exception is raised. + """ + + try: + if not token: + raise AuthException("Needed a token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED) + + # try to get from cache first + now = time() + session = self.token_cache.get(token) + if session and session["expires"] < now: + # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del + self.token_cache.pop(token, None) + session = None + + # get from database if not in cache + if not session: + session = self.db.get_one("tokens", {"_id": token}) + if session["expires"] < now: + raise AuthException("Expired Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED) + + # complete token information + pid = session["project_id"] + prj = self.db.get_one("projects", {BaseTopic.id_field("projects", pid): pid}) + session["project_id"] = prj["_id"] + session["project_name"] = prj["name"] + session["user_id"] = self.db.get_one("users", {"username": session["username"]})["_id"] + + # add token roles - PROVISIONAL + role_id = self.db.get_one("roles", {"name": "system_admin"})["_id"] + session["roles"] = [{"name": "system_admin", "id": role_id}] + + return session + + except DbException as e: + if e.http_code == HTTPStatus.NOT_FOUND: + raise AuthException("Invalid Token or Authorization HTTP header", http_code=HTTPStatus.UNAUTHORIZED) + else: + raise + 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"], "admin": True} + else: + raise + except Exception: + self.logger.exception("Error during token validation using internal backend") + raise AuthException("Error during token validation using internal backend", + http_code=HTTPStatus.UNAUTHORIZED) + + def revoke_token(self, token): + """ + Invalidate a token. + + :param token: token to be revoked + """ + try: + self.token_cache.pop(token, None) + self.db.del_one("tokens", {"_id": token}) + return True + except DbException as e: + if e.http_code == HTTPStatus.NOT_FOUND: + raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND) + else: + # raise + msg = "Error during token revocation using internal backend" + self.logger.exception(msg) + raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED) + + def authenticate(self, user, password, project=None, token=None): + """ + Authenticate a user using username/password or token, plus project + + :param user: user: name, id or None + :param password: password or None + :param project: name, id, or None. If None first found project will be used to get an scope token + :param token: previous token to obtain authorization + :param remote: remote host information + :return: the scoped token info or raises an exception. The token is a dictionary with: + _id: token string id, + username: username, + project_id: scoped_token project_id, + project_name: scoped_token project_name, + expires: epoch time when it expires, + """ + + now = time() + user_content = None + + try: + # Try using username/password + if user: + user_rows = self.db.get_list("users", {BaseTopic.id_field("users", user): user}) + if user_rows: + user_content = user_rows[0] + salt = user_content["_admin"]["salt"] + shadow_password = sha256(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 token: + user_rows = self.db.get_list("users", {"username": token["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)) + project_id = project + + if project_id: + if project_id != "admin": + # To allow project names in project_id + proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id}) + if proj["_id"] not in user_content["projects"] and proj["name"] 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": + token_admin = True + else: + # To allow project names in project_id + proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id}) + token_admin = proj.get("admin", False) + + new_token = {"issued_at": now, "expires": now + 3600, + "_id": token_id, "id": token_id, + "project_id": project_id, + "username": user_content["username"], + "admin": token_admin} + + self.token_cache[token_id] = new_token + self.db.create("tokens", new_token) + # self._internal_tokens_prune(now) # Belongs to Authenticator - REMOVE? + return deepcopy(new_token) + + except Exception as e: + msg = "Error during user authentication using internal backend: {}".format(e) + self.logger.exception(msg) + raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED) + + def get_role_list(self): + """ + Get role list. + + :return: returns the list of roles. + """ + try: + role_list = self.db.get_list("roles") + roles = [{"name": role["name"], "_id": role["_id"]} for role in role_list] # if role.name != "service" ? + return roles + except Exception: + raise AuthException("Error during role listing using internal backend", http_code=HTTPStatus.UNAUTHORIZED) + + def create_role(self, role): + """ + Create a role. + + :param role: role name. + :raises AuthconnOperationException: if role creation failed. + """ + # try: + # TODO: Check that role name does not exist ? + return str(uuid4()) + # except Exception: + # raise AuthconnOperationException("Error during role creation using internal backend") + # except Conflict as ex: + # self.logger.info("Duplicate entry: %s", str(ex)) + + def delete_role(self, role_id): + """ + Delete a role. + + :param role_id: role identifier. + :raises AuthconnOperationException: if role deletion failed. + """ + # try: + # TODO: Check that role exists ? + return True + # except Exception: + # raise AuthconnOperationException("Error during role deletion using internal backend")