From: delacruzramo Date: Fri, 21 Jun 2019 12:25:38 +0000 (+0200) Subject: Fix Bug 748: Changing the scope using an available token (i.e. without reissuing... X-Git-Tag: v6.0.1~3 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=commitdiff_plain;h=ceb8baf31217c9d50ce7017beb25c59163d79b6e Fix Bug 748: Changing the scope using an available token (i.e. without reissuing the username and password) doesn't work RBAC with internal authentication backend Change-Id: Ibfe38fa484c3574fbc6a7e12c6bb855b947b60aa Signed-off-by: delacruzramo --- diff --git a/osm_nbi/admin_topics.py b/osm_nbi/admin_topics.py index 071ed3b..ceb8e89 100644 --- a/osm_nbi/admin_topics.py +++ b/osm_nbi/admin_topics.py @@ -25,6 +25,7 @@ from validation import validate_input from validation import ValidationError from validation import is_valid_uuid # To check that User/Project Names don't look like UUIDs from base_topic import BaseTopic, EngineException +from authconn_keystone import AuthconnKeystone __author__ = "Alfonso Tierno " @@ -58,7 +59,7 @@ class UserTopic(BaseTopic): raise EngineException("username '{}' exists".format(indata["username"]), HTTPStatus.CONFLICT) # check projects if not session["force"]: - for p in indata.get("projects"): + for p in indata.get("projects") or []: # To allow project addressing by Name as well as ID if not self.db.get_one("projects", {BaseTopic.id_field("projects", p): p}, fail_on_empty=False, fail_on_more=False): @@ -393,7 +394,7 @@ class UserTopicAuth(UserTopic): """ username = indata.get("username") if is_valid_uuid(username): - raise EngineException("username '{}' cannot be a uuid format".format(username), + raise EngineException("username '{}' cannot have a uuid format".format(username), HTTPStatus.UNPROCESSABLE_ENTITY) # Check that username is not used, regardless keystone already checks this @@ -418,7 +419,7 @@ class UserTopicAuth(UserTopic): if "username" in edit_content: username = edit_content.get("username") if is_valid_uuid(username): - raise EngineException("username '{}' cannot be an uuid format".format(username), + raise EngineException("username '{}' cannot have an uuid format".format(username), HTTPStatus.UNPROCESSABLE_ENTITY) # Check that username is not used, regardless keystone already checks this @@ -703,7 +704,7 @@ class ProjectTopicAuth(ProjectTopic): """ project_name = indata.get("name") if is_valid_uuid(project_name): - raise EngineException("project name '{}' cannot be an uuid format".format(project_name), + raise EngineException("project name '{}' cannot have an uuid format".format(project_name), HTTPStatus.UNPROCESSABLE_ENTITY) project_list = self.auth.get_project_list(filter_q={"name": project_name}) @@ -857,8 +858,8 @@ class ProjectTopicAuth(ProjectTopic): class RoleTopicAuth(BaseTopic): - topic = "roles_operations" - topic_msg = None # "roles" + topic = "roles" + topic_msg = None # "roles" schema_new = roles_new_schema schema_edit = roles_edit_schema multiproject = False @@ -867,6 +868,7 @@ class RoleTopicAuth(BaseTopic): BaseTopic.__init__(self, db, fs, msg) self.auth = auth self.operations = ops + self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles" @staticmethod def validate_role_definition(operations, role_definitions): @@ -963,10 +965,9 @@ class RoleTopicAuth(BaseTopic): :return: None if ok or raises EngineException with the conflict """ roles = self.auth.get_role_list() - system_admin_role = [role for role in roles - if role["name"] == "system_admin"][0] + system_admin_roles = [role for role in roles if role["name"] == "system_admin"] - if _id == system_admin_role["_id"]: + if system_admin_roles and _id == system_admin_roles[0]["_id"]: raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN) @staticmethod @@ -1033,6 +1034,7 @@ class RoleTopicAuth(BaseTopic): # :return: dictionary, raise exception if not found. # """ # filter_db = {"_id": _id} + # filter_db = { BaseTopic.id_field(self.topic, _id): _id } # To allow role addressing by name # # role = self.db.get_one(self.topic, filter_db) # new_role = dict(role) @@ -1108,7 +1110,8 @@ class RoleTopicAuth(BaseTopic): :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ... """ self.check_conflict_on_del(session, _id, None) - filter_q = {"_id": _id} + # filter_q = {"_id": _id} + filter_q = {BaseTopic.id_field(self.topic, _id): _id} # To allow role addressing by name if not dry_run: self.auth.delete_role(_id) v = self.db.del_one(self.topic, filter_q) diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index 576ae4d..b1f73fe 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -35,19 +35,19 @@ import yaml 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 os import path -from base_topic import BaseTopic # To allow project names in project_id from authconn import AuthException, AuthExceptionUnauthorized from authconn_keystone import AuthconnKeystone +from authconn_internal import AuthconnInternal # Comment out for testing&debugging, uncomment when ready from osm_common import dbmongo from osm_common import dbmemory from osm_common.dbbase import DbException +from uuid import uuid4 # For Role _id with internal authentication backend + class Authenticator: """ @@ -72,6 +72,7 @@ class Authenticator: self.next_db_prune_time = 0 # time when next cleaning of expired tokens must be done self.resources_to_operations_file = None self.roles_to_operations_file = None + self.roles_to_operations_table = None self.resources_to_operations_mapping = {} self.operation_to_allowed_roles = {} self.logger = logging.getLogger("nbi.authenticator") @@ -102,6 +103,7 @@ class Authenticator: if config["authentication"]["backend"] == "keystone": self.backend = AuthconnKeystone(self.config["authentication"]) elif config["authentication"]["backend"] == "internal": + self.backend = AuthconnInternal(self.config["authentication"], self.db, self.tokens_cache) self._internal_tokens_prune() else: raise AuthException("Unknown authentication backend: {}" @@ -134,6 +136,10 @@ class Authenticator: break if not self.roles_to_operations_file: raise AuthException("Invalid permission configuration: roles_to_operations file missing") + if not self.roles_to_operations_table: # PROVISIONAL ? + self.roles_to_operations_table = "roles_operations" \ + if config["authentication"]["backend"] == "keystone" \ + else "roles" except Exception as e: raise AuthException(str(e)) @@ -155,8 +161,10 @@ class Authenticator: # Always reads operation to resource mapping from file (this is static, no need to store it in MongoDB) # Operations encoding: " " # Note: it is faster to rewrite the value than to check if it is already there or not - if self.config["authentication"]["backend"] == "internal": - return + + # PCR 28/05/2019 Commented out to allow initialization for internal backend + # if self.config["authentication"]["backend"] == "internal": + # return with open(self.resources_to_operations_file, "r") as stream: resources_to_operations_yaml = yaml.load(stream) @@ -166,7 +174,7 @@ class Authenticator: self.operations.append(operation) self.resources_to_operations_mapping[resource] = operation - records = self.db.get_list("roles_operations") + records = self.db.get_list(self.roles_to_operations_table) # Loading permissions to MongoDB if there is not any permission. if not records: @@ -208,18 +216,18 @@ class Authenticator: "modified": now, } - if self.config["authentication"]["backend"] != "internal" and \ - role_with_operations["name"] != "anonymous": - - backend_roles = self.backend.get_role_list(filter_q={"name": role_with_operations["name"]}) - - if backend_roles: - backend_id = backend_roles[0]["_id"] - else: - backend_id = self.backend.create_role(role_with_operations["name"]) - role_with_operations["_id"] = backend_id + if self.config["authentication"]["backend"] == "keystone": + if role_with_operations["name"] != "anonymous": + backend_roles = self.backend.get_role_list(filter_q={"name": role_with_operations["name"]}) + if backend_roles: + backend_id = backend_roles[0]["_id"] + else: + backend_id = self.backend.create_role(role_with_operations["name"]) + role_with_operations["_id"] = backend_id + else: + role_with_operations["_id"] = str(uuid4()) - self.db.create("roles_operations", role_with_operations) + self.db.create(self.roles_to_operations_table, role_with_operations) if self.config["authentication"]["backend"] != "internal": self.backend.assign_role_to_user("admin", "admin", "system_admin") @@ -234,7 +242,7 @@ class Authenticator: """ permissions = {oper: [] for oper in self.operations} - records = self.db.get_list("roles_operations") + records = self.db.get_list(self.roles_to_operations_table) ignore_fields = ["_id", "_admin", "name", "default"] for record in records: @@ -286,18 +294,17 @@ class Authenticator: outdata = self.new_token(None, {"username": user, "password": passwd}) token = outdata["id"] cherrypy.session['Authorization'] = token - if self.config["authentication"]["backend"] == "internal": - return self._internal_authorize(token) - else: - if not token: - raise AuthException("Needed a token or Authorization http header", - http_code=HTTPStatus.UNAUTHORIZED) - token_info = self.backend.validate_token(token) - # TODO add to token info remote host, port - - self.check_permissions(token_info, cherrypy.request.path_info, - cherrypy.request.method) - return token_info + + if not token: + raise AuthException("Needed a token or Authorization http header", + http_code=HTTPStatus.UNAUTHORIZED) + token_info = self.backend.validate_token(token) + # TODO add to token info remote host, port + + self.check_permissions(token_info, cherrypy.request.path_info, + cherrypy.request.method) + return token_info + except AuthException as e: if not isinstance(e, AuthExceptionUnauthorized): if cherrypy.session.get('Authorization'): @@ -306,67 +313,39 @@ class Authenticator: raise def new_token(self, session, indata, remote): - if self.config["authentication"]["backend"] == "internal": - return self._internal_new_token(session, indata, remote) - else: - current_token = None - if session: - current_token = session.get("token") - token_info = self.backend.authenticate( - user=indata.get("username"), - password=indata.get("password"), - 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_info["_id"], - "id": token_info["_id"], - "issued_at": now, - "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": True if token_info.get("project_name") == "admin" else False # TODO put admin in RBAC - } - - if remote.name: - new_session["remote_host"] = remote.name - elif remote.ip: - new_session["remote_host"] = remote.ip + current_token = None + if session: + # current_token = session.get("token") + current_token = session.get("_id") if self.config["authentication"]["backend"] == "keystone" \ + else session + token_info = self.backend.authenticate( + user=indata.get("username"), + password=indata.get("password"), + token=current_token, + project=indata.get("project_id") + ) - # TODO: check if this can be avoided. Backend may provide enough information - self.tokens_cache[token_info["_id"]] = new_session + now = time() + new_session = { + "_id": token_info["_id"], + "id": token_info["_id"], + "issued_at": now, + "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": True if token_info.get("project_name") == "admin" else False # TODO put admin in RBAC + } + + if remote.name: + new_session["remote_host"] = remote.name + elif remote.ip: + new_session["remote_host"] = remote.ip - return deepcopy(new_session) + # TODO: check if this can be avoided. Backend may provide enough information + self.tokens_cache[token_info["_id"]] = new_session + + return deepcopy(new_session) def get_token_list(self, session): if self.config["authentication"]["backend"] == "internal": @@ -389,15 +368,12 @@ class Authenticator: 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_cache[token] - return "token '{}' deleted".format(token) - except KeyError: - raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND) + try: + self.backend.revoke_token(token) + self.tokens_cache.pop(token, None) + return "token '{}' deleted".format(token) + except KeyError: + raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND) def check_permissions(self, session, url, method): self.logger.info("Session: {}".format(session)) @@ -482,97 +458,6 @@ class Authenticator: return filtered_key, parameters - def _internal_authorize(self, token_id): - try: - if not token_id: - raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) - # try to get from cache first - now = time() - session = self.tokens_cache.get(token_id) - 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.tokens_cache.pop(token_id, None) - session = None - if session: - return session - - # get from database if not in cache - session = self.db.get_one("tokens", {"_id": token_id}) - if session["expires"] < now: - raise AuthException("Expired Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) - self.tokens_cache[token_id] = session - 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 - - def _internal_new_token(self, session, indata, remote): - now = time() - user_content = None - - # Try using username/password - if indata.get("username"): - user_rows = self.db.get_list("users", {"username": indata.get("username")}) - 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)) - project_id = indata.get("project_id") - 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": - session_admin = True - else: - # To allow project names in project_id - project = self.db.get_one("projects", {BaseTopic.id_field("projects", project_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_cache[token_id] = new_session - self.db.create("tokens", new_session) - # check if database must be prune - self._internal_tokens_prune(now) - return deepcopy(new_session) - def _internal_get_token_list(self, session): now = time() token_list = self.db.get_list("tokens", {"username": session["username"], "expires.gt": now}) @@ -586,17 +471,6 @@ class Authenticator: raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) return token_value - def _internal_del_token(self, token_id): - try: - self.tokens_cache.pop(token_id, None) - self.db.del_one("tokens", {"_id": token_id}) - return "token '{}' deleted".format(token_id) - except DbException as e: - if e.http_code == HTTPStatus.NOT_FOUND: - raise AuthException("Token '{}' not found".format(token_id), http_code=HTTPStatus.NOT_FOUND) - else: - raise - def _internal_tokens_prune(self, now=None): now = now or time() if not self.next_db_prune_time or self.next_db_prune_time >= now: 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") diff --git a/osm_nbi/base_topic.py b/osm_nbi/base_topic.py index 9a48791..c8a7665 100644 --- a/osm_nbi/base_topic.py +++ b/osm_nbi/base_topic.py @@ -61,7 +61,8 @@ class BaseTopic: alt_id_field = { "projects": "name", "users": "username", - "roles": "name" + "roles": "name", + "roles_operations": "name" } def __init__(self, db, fs, msg): @@ -73,7 +74,7 @@ class BaseTopic: @staticmethod def id_field(topic, value): """Returns ID Field for given topic and field value""" - if topic in ["projects", "users"] and not is_valid_uuid(value): + if topic in BaseTopic.alt_id_field.keys() and not is_valid_uuid(value): return BaseTopic.alt_id_field[topic] else: return "_id" diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index d796a93..0ba57cb 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -22,6 +22,7 @@ from osm_common.msgbase import MsgException from http import HTTPStatus from authconn_keystone import AuthconnKeystone +from authconn_internal import AuthconnInternal from base_topic import EngineException, versiontuple from admin_topics import UserTopic, ProjectTopic, VimAccountTopic, WimAccountTopic, SdnTopic from admin_topics import UserTopicAuth, ProjectTopicAuth, RoleTopicAuth @@ -50,6 +51,7 @@ class Engine(object): "sdns": SdnTopic, "users": UserTopic, "projects": ProjectTopic, + "roles": RoleTopicAuth, # Valid for both internal and keystone authentication backends "nsis": NsiTopic, "nsilcmops": NsiLcmOpTopic # [NEW_TOPIC]: add an entry here @@ -117,6 +119,8 @@ class Engine(object): if not self.auth: if config["authentication"]["backend"] == "keystone": self.auth = AuthconnKeystone(config["authentication"]) + else: + self.auth = AuthconnInternal(config["authentication"], self.db, dict()) # TO BE CONFIRMED if not self.operations: if "resources_to_operations" in config["rbac"]: resources_to_operations_file = config["rbac"]["resources_to_operations"] @@ -308,11 +312,10 @@ class Engine(object): users = self.db.get_one("users", fail_on_empty=False, fail_on_more=False) if users: return None - # raise EngineException("Unauthorized. Database users is not empty", HTTPStatus.UNAUTHORIZED) user_desc = {"username": "admin", "password": "admin", "projects": ["admin"]} fake_session = {"project_id": "admin", "username": "admin", "admin": True, "force": True, "public": None} - roolback_list = [] - _id = self.map_topic["users"].new(roolback_list, fake_session, user_desc) + rollback_list = [] + _id = self.map_topic["users"].new(rollback_list, fake_session, user_desc) return _id def create_admin(self): @@ -322,10 +325,10 @@ class Engine(object): """ project_id = self.create_admin_project() user_id = self.create_admin_user() - if not project_id and not user_id: - return None - else: + if project_id or user_id: return {'project_id': project_id, 'user_id': user_id} + else: + return None def upgrade_db(self, current_version, target_version): if target_version not in self.map_target_version_to_int.keys(): @@ -354,7 +357,8 @@ class Engine(object): current_version = "1.0" if current_version in ("1.0", "1.1") and target_version_int >= self.map_target_version_to_int["1.2"]: - self.db.del_list("roles_operations") + table = "roles_operations" if self.config['authentication']['backend'] == "keystone" else "roles" + self.db.del_list(table) version_data = { "_id": "version", @@ -387,8 +391,8 @@ class Engine(object): if db_version != target_version: self.upgrade_db(db_version, target_version) - # create user admin if not exist - if not self.auth: + # create admin project&user if they don't exist + if self.config['authentication']['backend'] == 'internal' or not self.auth: self.create_admin() return