From 38dcfeb4a5d8c8da65b9ee2d2c2f58bc6164f6bf Mon Sep 17 00:00:00 2001 From: tierno Date: Mon, 10 Jun 2019 16:44:00 +0000 Subject: [PATCH] fix bug 748: provide a proper error when user is not valid upon new token allow authenticate with both project_id and project_name, user_id and username Change-Id: I463e2aaa469fea8ad656407dd7b48ed5e28aff28 Signed-off-by: tierno --- osm_nbi/admin_topics.py | 8 +- osm_nbi/auth.py | 78 +++++++++------- osm_nbi/authconn.py | 43 +++++---- osm_nbi/authconn_keystone.py | 172 +++++++++++++++++++++++------------ 4 files changed, 186 insertions(+), 115 deletions(-) diff --git a/osm_nbi/admin_topics.py b/osm_nbi/admin_topics.py index 7c98a34..9facd8b 100644 --- a/osm_nbi/admin_topics.py +++ b/osm_nbi/admin_topics.py @@ -655,11 +655,11 @@ class ProjectTopicAuth(ProjectTopic): :param db_content: The database content of this item _id :return: None if ok or raises EngineException with the conflict """ - projects = self.auth.get_project_list() - current_project = [project for project in projects - if project["name"] in session["project_id"]][0] + # projects = self.auth.get_project_list() + # current_project = [project for project in projects + # if project["name"] in session["project_id"]][0] - if _id == current_project["_id"]: + if _id == session["project_id"]: raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT) def new(self, rollback, session, indata=None, kwargs=None, headers=None): diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index d916189..f7e4844 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -34,7 +34,7 @@ import logging import yaml from base64 import standard_b64decode from copy import deepcopy -from functools import reduce +# from functools import reduce from hashlib import sha256 from http import HTTPStatus from random import choice as random_choice @@ -320,43 +320,53 @@ class Authenticator: 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 not session: - token, projects = self.backend.authenticate_with_token(token, project_id) - - if project_id == "admin": - session_admin = True - else: - session_admin = reduce(lambda x, y: x or (True if y == "admin" else False), - projects, False) + current_token = None + if session: + current_token = session.get("token") + token_info = self.backend.authenticate( + user=indata.get("username"), + password=indata.get("username"), + token=current_token, + project=indata.get("project_id") + ) + + # 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 not session: + # token, projects = self.backend.authenticate_with_token(token, project_id) + # + # 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, + "_id": token_info["_id"], + "id": token_info["_id"], "issued_at": now, - "expires": now + 3600, - "project_id": project_id, - "username": indata.get("username") if not session else session.get("username"), + "expires": token_info.get("expires", now + 3600), + "project_id": token_info["project_id"], + "username": token_info.get("username") or session.get("username"), "remote_port": remote.port, - "admin": session_admin + "admin": True if token_info.get("project_name") == "admin" else False # TODO put admin in RBAC } if remote.name: @@ -365,7 +375,7 @@ class Authenticator: new_session["remote_host"] = remote.ip # TODO: check if this can be avoided. Backend may provide enough information - self.tokens_cache[token] = new_session + self.tokens_cache[token_info["_id"]] = new_session return deepcopy(new_session) diff --git a/osm_nbi/authconn.py b/osm_nbi/authconn.py index 0df8911..2780d59 100644 --- a/osm_nbi/authconn.py +++ b/osm_nbi/authconn.py @@ -96,27 +96,34 @@ class Authconn: """ self.config = config - def authenticate_with_user_password(self, user, password): + def authenticate(self, user, password, project=None, token=None): """ - Authenticate a user using username and password. + 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 + :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, - :param user: username - :param password: password - :return: an unscoped token that grants access to project list """ raise AuthconnNotImplementedException("Should have implemented this") - def authenticate_with_token(self, token, project=None): - """ - Authenticate a user using a token. Can be used to revalidate the token - or to get a scoped token. - - :param token: a valid token. - :param project: (optional) project for a scoped token. - :return: return a revalidated token, scoped if a project was passed or - the previous token was already scoped. - """ - raise AuthconnNotImplementedException("Should have implemented this") + # def authenticate_with_token(self, token, project=None): + # """ + # Authenticate a user using a token. Can be used to revalidate the token + # or to get a scoped token. + # + # :param token: a valid token. + # :param project: (optional) project for a scoped token. + # :return: return a revalidated token, scoped if a project was passed or + # the previous token was already scoped. + # """ + # raise AuthconnNotImplementedException("Should have implemented this") def validate_token(self, token): """ @@ -237,11 +244,11 @@ class Authconn: """ raise AuthconnNotImplementedException("Should have implemented this") - def get_project_list(self, filter_q={}): + def get_project_list(self, filter_q=None): """ Get all the projects. - :param filter_q: dictionary to filter project list. + :param filter_q: dictionary to filter project list, by "name" and/or "_id" :return: list of projects """ raise AuthconnNotImplementedException("Should have implemented this") diff --git a/osm_nbi/authconn_keystone.py b/osm_nbi/authconn_keystone.py index c8d6811..ff54130 100644 --- a/osm_nbi/authconn_keystone.py +++ b/osm_nbi/authconn_keystone.py @@ -77,58 +77,110 @@ class AuthconnKeystone(Authconn): self.sess = session.Session(auth=self.auth) self.keystone = client.Client(session=self.sess) - def authenticate_with_user_password(self, user, password): + def authenticate(self, user, password, project=None, token=None): """ - Authenticate a user using username and password. + 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 + :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, - :param user: username - :param password: password - :return: an unscoped token that grants access to project list """ try: - user_id = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0].id - project_names = [project.name for project in self.keystone.projects.list(user=user_id)] + username = None + user_id = None + project_id = None + project_name = None + + if user: + if is_valid_uuid(user): + user_id = user + else: + username = user + + # get an unscoped token firstly + unscoped_token = self.keystone.get_raw_token_from_identity_service( + auth_url=self.auth_url, + user_id=user_id, + username=username, + password=password, + user_domain_name=self.user_domain_name, + project_domain_name=self.project_domain_name) + elif token: + unscoped_token = self.keystone.tokens.validate(token=token) + else: + raise AuthException("Provide credentials: username/password or Authorization Bearer token", + http_code=HTTPStatus.UNAUTHORIZED) + + if not project: + # get first project for the user + project_list = self.keystone.projects.list(user=unscoped_token["user"]["id"]) + if not project_list: + raise AuthException("The user {} has not any project and cannot be used for authentication". + format(user), http_code=HTTPStatus.UNAUTHORIZED) + project_id = project_list[0].id + else: + if is_valid_uuid(project): + project_id = project + else: + project_name = project - token = self.keystone.get_raw_token_from_identity_service( + scoped_token = self.keystone.get_raw_token_from_identity_service( auth_url=self.auth_url, - username=user, - password=password, + project_name=project_name, + project_id=project_id, user_domain_name=self.user_domain_name, - project_domain_name=self.project_domain_name) - - return token["auth_token"], project_names + project_domain_name=self.project_domain_name, + token=unscoped_token["auth_token"]) + + auth_token = { + "_id": scoped_token.auth_token, + "username": scoped_token.username, + "project_id": scoped_token.project_id, + "project_name": scoped_token.project_name, + "expires": scoped_token.expires.timestamp(), + } + + return auth_token except ClientException as e: self.logger.exception("Error during user authentication using keystone. Method: basic: {}".format(e)) raise AuthException("Error during user authentication using Keystone: {}".format(e), http_code=HTTPStatus.UNAUTHORIZED) - def authenticate_with_token(self, token, project=None): - """ - Authenticate a user using a token. Can be used to revalidate the token - or to get a scoped token. - - :param token: a valid token. - :param project: (optional) project for a scoped token. - :return: return a revalidated token, scoped if a project was passed or - the previous token was already scoped. - """ - try: - token_info = self.keystone.tokens.validate(token=token) - projects = self.keystone.projects.list(user=token_info["user"]["id"]) - project_names = [project.name for project in projects] - - new_token = self.keystone.get_raw_token_from_identity_service( - auth_url=self.auth_url, - token=token, - project_name=project, - user_domain_name=self.user_domain_name, - project_domain_name=self.project_domain_name) - - return new_token["auth_token"], project_names - except ClientException as e: - self.logger.exception("Error during user authentication using keystone. Method: bearer: {}".format(e)) - raise AuthException("Error during user authentication using Keystone: {}".format(e), - http_code=HTTPStatus.UNAUTHORIZED) + # def authenticate_with_token(self, token, project=None): + # """ + # Authenticate a user using a token. Can be used to revalidate the token + # or to get a scoped token. + # + # :param token: a valid token. + # :param project: (optional) project for a scoped token. + # :return: return a revalidated token, scoped if a project was passed or + # the previous token was already scoped. + # """ + # try: + # token_info = self.keystone.tokens.validate(token=token) + # projects = self.keystone.projects.list(user=token_info["user"]["id"]) + # project_names = [project.name for project in projects] + # + # new_token = self.keystone.get_raw_token_from_identity_service( + # auth_url=self.auth_url, + # token=token, + # project_name=project, + # project_id=None, + # user_domain_name=self.user_domain_name, + # project_domain_name=self.project_domain_name) + # + # return new_token["auth_token"], project_names + # except ClientException as e: + # self.logger.exception("Error during user authentication using keystone. Method: bearer: {}".format(e)) + # raise AuthException("Error during user authentication using Keystone: {}".format(e), + # http_code=HTTPStatus.UNAUTHORIZED) def validate_token(self, token): """ @@ -243,12 +295,13 @@ class AuthconnKeystone(Authconn): :raises AuthconnOperationException: if user deletion failed. """ try: - users = self.keystone.users.list() - user_obj = [user for user in users if user.id == user_id][0] - result, _ = self.keystone.users.delete(user_obj) + # users = self.keystone.users.list() + # user_obj = [user for user in users if user.id == user_id][0] + # result, _ = self.keystone.users.delete(user_obj) + result, detail = self.keystone.users.delete(user_id) if result.status_code != 204: - raise ClientException("User was not deleted") + raise ClientException("error {} {}".format(result.status_code, detail)) return True except ClientException as e: @@ -348,17 +401,17 @@ class AuthconnKeystone(Authconn): try: roles = self.keystone.roles.list() role_obj = [role for role in roles if role.id == role_id][0] - result, _ = self.keystone.roles.delete(role_obj) + result, detail = self.keystone.roles.delete(role_obj) if result.status_code != 204: - raise ClientException("Role was not deleted") + raise ClientException("error {} {}".format(result.status_code, detail)) return True except ClientException as e: self.logger.exception("Error during role deletion using keystone: {}".format(e)) raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e)) - def get_project_list(self, filter_q={}): + def get_project_list(self, filter_q=None): """ Get all the projects. @@ -366,19 +419,19 @@ class AuthconnKeystone(Authconn): :return: list of projects """ try: - projects = self.keystone.projects.list() + filter_name = None + if filter_q: + filter_name = filter_q.get("name") + projects = self.keystone.projects.list(name=filter_name) + projects = [{ "name": project.name, "_id": project.id - } for project in projects if project.name != self.admin_project] - - allowed_fields = ["_id", "name"] - for key in filter_q.keys(): - if key not in allowed_fields: - continue + } for project in projects] + if filter_q and filter_q.get("_id"): projects = [project for project in projects - if filter_q[key] == project[key]] + if filter_q["_id"] == project["_id"]] return projects except ClientException as e: @@ -409,12 +462,13 @@ class AuthconnKeystone(Authconn): :raises AuthconnOperationException: if project deletion failed. """ try: - projects = self.keystone.projects.list() - project_obj = [project for project in projects if project.id == project_id][0] - result, _ = self.keystone.projects.delete(project_obj) + # projects = self.keystone.projects.list() + # project_obj = [project for project in projects if project.id == project_id][0] + # result, _ = self.keystone.projects.delete(project_obj) + result, detail = self.keystone.projects.delete(project_id) if result.status_code != 204: - raise ClientException("Project was not deleted") + raise ClientException("error {} {}".format(result.status_code, detail)) return True except ClientException as e: -- 2.17.1