X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fauthconn_internal.py;h=40d32152f0f5ada08b54b272e42d6691ed74df58;hp=5e35e8f481b90a7500979ee411a2b2f394ffe0c2;hb=1546f2a46d99a4741b23857e6ceb4b813223e297;hpb=ceb8baf31217c9d50ce7017beb25c59163d79b6e diff --git a/osm_nbi/authconn_internal.py b/osm_nbi/authconn_internal.py index 5e35e8f..40d3215 100644 --- a/osm_nbi/authconn_internal.py +++ b/osm_nbi/authconn_internal.py @@ -27,11 +27,12 @@ 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 authconn import Authconn, AuthException # , AuthconnOperationException from osm_common.dbbase import DbException from base_topic import BaseTopic import logging +import re from time import time from http import HTTPStatus from uuid import uuid4 @@ -42,7 +43,7 @@ from random import choice as random_choice class AuthconnInternal(Authconn): def __init__(self, config, db, token_cache): - Authconn.__init__(self, config) + Authconn.__init__(self, config, db, token_cache) self.logger = logging.getLogger("nbi.authenticator.internal") @@ -56,48 +57,6 @@ class AuthconnInternal(Authconn): 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. @@ -120,30 +79,19 @@ class AuthconnInternal(Authconn): # try to get from cache first now = time() - session = self.token_cache.get(token) - if session and session["expires"] < now: + token_info = self.token_cache.get(token) + if token_info and token_info["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 + token_info = None # get from database if not in cache - if not session: - session = self.db.get_one("tokens", {"_id": token}) - if session["expires"] < now: + if not token_info: + token_info = self.db.get_one("tokens", {"_id": token}) + if token_info["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 + return token_info except DbException as e: if e.http_code == HTTPStatus.NOT_FOUND: @@ -181,14 +129,14 @@ class AuthconnInternal(Authconn): self.logger.exception(msg) raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED) - def authenticate(self, user, password, project=None, token=None): + def authenticate(self, user, password, project=None, token_info=None): """ - Authenticate a user using username/password or token, plus project + Authenticate a user using username/password or previous token_info plus project; its creates a new token :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 token_info: previous token_info 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, @@ -201,92 +149,105 @@ class AuthconnInternal(Authconn): 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) + # 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_info: + user_rows = self.db.get_list("users", {"username": token_info["username"]}) + if user_rows: + user_content = user_rows[0] 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] + raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED) + else: + raise AuthException("Provide credentials: username/password or Authorization Bearer token", + http_code=HTTPStatus.UNAUTHORIZED) - 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): + token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') + for _ in range(0, 32)) + + # projects = user_content.get("projects", []) + prm_list = user_content.get("project_role_mappings", []) + + if not project: + project = prm_list[0]["project"] if prm_list else None + if not project: + raise AuthException("can't find a default project for this user", http_code=HTTPStatus.UNAUTHORIZED) + + projects = [prm["project"] for prm in prm_list] + + proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project): project}) + project_name = proj["name"] + project_id = proj["_id"] + if project_name not in projects and project_id not in projects: + raise AuthException("project {} not allowed for this user".format(project), + http_code=HTTPStatus.UNAUTHORIZED) + + # TODO remove admin, this vill be used by roles RBAC + if project_name == "admin": + token_admin = True + else: + token_admin = proj.get("admin", False) + + # add token roles + roles = [] + roles_list = [] + for prm in prm_list: + if prm["project"] in [project_id, project_name]: + role = self.db.get_one("roles", {BaseTopic.id_field("roles", prm["role"]): prm["role"]}) + rid = role["_id"] + if rid not in roles: + rnm = role["name"] + roles.append(rid) + roles_list.append({"name": rnm, "id": rid}) + if not roles_list: + rid = self.db.get_one("roles", {"name": "project_admin"})["_id"] + roles_list = [{"name": "project_admin", "id": rid}] + + new_token = {"issued_at": now, + "expires": now + 3600, + "_id": token_id, + "id": token_id, + "project_id": proj["_id"], + "project_name": proj["name"], + "username": user_content["username"], + "user_id": user_content["_id"], + "admin": token_admin, + "roles": roles_list, + } + + self.token_cache[token_id] = new_token + self.db.create("tokens", new_token) + return deepcopy(new_token) + + def get_role_list(self, filter_q={}): """ 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) + return self.db.get_list("roles", filter_q) - def create_role(self, role): + def create_role(self, role_info): """ Create a role. - :param role: role name. + :param role_info: full role info. + :return: returns the role id. :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)) + rid = str(uuid4()) + role_info["_id"] = rid + rid = self.db.create("roles", role_info) + return rid def delete_role(self, role_id): """ @@ -295,8 +256,178 @@ class AuthconnInternal(Authconn): :param role_id: role identifier. :raises AuthconnOperationException: if role deletion failed. """ - # try: - # TODO: Check that role exists ? + return self.db.del_one("roles", {"_id": role_id}) + + def update_role(self, role_info): + """ + Update a role. + + :param role_info: full role info. + :return: returns the role name and id. + :raises AuthconnOperationException: if user creation failed. + """ + rid = role_info["_id"] + self.db.set_one("roles", {"_id": rid}, role_info) # CONFIRM + return {"_id": rid, "name": role_info["name"]} + + def create_user(self, user_info): + """ + Create a user. + + :param user_info: full user info. + :return: returns the username and id of the user. + """ + BaseTopic.format_on_new(user_info, make_public=False) + salt = uuid4().hex + user_info["_admin"]["salt"] = salt + if "password" in user_info: + user_info["password"] = sha256(user_info["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest() + # "projects" are not stored any more + if "projects" in user_info: + del user_info["projects"] + self.db.create("users", user_info) + return {"username": user_info["username"], "_id": user_info["_id"]} + + def update_user(self, user_info): + """ + Change the user name and/or password. + + :param user_info: user info modifications + """ + uid = user_info["_id"] + user_data = self.db.get_one("users", {BaseTopic.id_field("users", uid): uid}) + BaseTopic.format_on_edit(user_data, user_info) + # User Name + usnm = user_info.get("username") + if usnm: + user_data["username"] = usnm + # If password is given and is not already encripted + pswd = user_info.get("password") + if pswd and (len(pswd) != 64 or not re.match('[a-fA-F0-9]*', pswd)): # TODO: Improve check? + salt = uuid4().hex + if "_admin" not in user_data: + user_data["_admin"] = {} + user_data["_admin"]["salt"] = salt + user_data["password"] = sha256(pswd.encode('utf-8') + salt.encode('utf-8')).hexdigest() + # Project-Role Mappings + # TODO: Check that user_info NEVER includes "project_role_mappings" + if "project_role_mappings" not in user_data: + user_data["project_role_mappings"] = [] + for prm in user_info.get("add_project_role_mappings", []): + user_data["project_role_mappings"].append(prm) + for prm in user_info.get("remove_project_role_mappings", []): + for pidf in ["project", "project_name"]: + for ridf in ["role", "role_name"]: + try: + user_data["project_role_mappings"].remove({"role": prm[ridf], "project": prm[pidf]}) + except KeyError: + pass + except ValueError: + pass + self.db.set_one("users", {BaseTopic.id_field("users", uid): uid}, user_data) # CONFIRM + + def delete_user(self, user_id): + """ + Delete user. + + :param user_id: user identifier. + :raises AuthconnOperationException: if user deletion failed. + """ + self.db.del_one("users", {"_id": user_id}) return True - # except Exception: - # raise AuthconnOperationException("Error during role deletion using internal backend") + + def get_user_list(self, filter_q=None): + """ + Get user list. + + :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id + :return: returns a list of users. + """ + filt = filter_q or {} + if "name" in filt: + filt["username"] = filt["name"] + del filt["name"] + users = self.db.get_list("users", filt) + project_id_name = {} + role_id_name = {} + for user in users: + prms = user.get("project_role_mappings") + projects = user.get("projects") + if prms: + projects = [] + # add project_name and role_name. Generate projects for backward compatibility + for prm in prms: + project_id = prm["project"] + if project_id not in project_id_name: + pr = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id}, + fail_on_empty=False) + project_id_name[project_id] = pr["name"] if pr else None + prm["project_name"] = project_id_name[project_id] + if prm["project_name"] not in projects: + projects.append(prm["project_name"]) + + role_id = prm["role"] + if role_id not in role_id_name: + role = self.db.get_one("roles", {BaseTopic.id_field("roles", role_id): role_id}, + fail_on_empty=False) + role_id_name[role_id] = role["name"] if role else None + prm["role_name"] = role_id_name[role_id] + user["projects"] = projects # for backward compatibility + elif projects: + # user created with an old version. Create a project_role mapping with role project_admin + user["project_role_mappings"] = [] + role = self.db.get_one("roles", {BaseTopic.id_field("roles", "project_admin"): "project_admin"}) + for p_id_name in projects: + pr = self.db.get_one("projects", {BaseTopic.id_field("projects", p_id_name): p_id_name}) + prm = {"project": pr["_id"], + "project_name": pr["name"], + "role_name": "project_admin", + "role": role["_id"] + } + user["project_role_mappings"].append(prm) + else: + user["projects"] = [] + user["project_role_mappings"] = [] + + return users + + def get_project_list(self, filter_q={}): + """ + Get role list. + + :return: returns the list of projects. + """ + return self.db.get_list("projects", filter_q) + + def create_project(self, project_info): + """ + Create a project. + + :param project: full project info. + :return: the internal id of the created project + :raises AuthconnOperationException: if project creation failed. + """ + pid = self.db.create("projects", project_info) + return pid + + def delete_project(self, project_id): + """ + Delete a project. + + :param project_id: project identifier. + :raises AuthconnOperationException: if project deletion failed. + """ + filter_q = {BaseTopic.id_field("projects", project_id): project_id} + r = self.db.del_one("projects", filter_q) + return r + + def update_project(self, project_id, project_info): + """ + Change the name of a project + + :param project_id: project to be changed + :param project_info: full project info + :return: None + :raises AuthconnOperationException: if project update failed. + """ + self.db.set_one("projects", {BaseTopic.id_field("projects", project_id): project_id}, project_info)