X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fauth.py;h=3d74d8959464d7bad960aed3e5ab0b86307ccb5d;hp=b1f73fee0a7fb28cab8c317fedb9eb68a24a98bc;hb=refs%2Fchanges%2F35%2F7735%2F1;hpb=ceb8baf31217c9d50ce7017beb25c59163d79b6e diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index b1f73fe..3d74d89 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -45,6 +45,7 @@ from authconn_internal import AuthconnInternal # Comment out for testing&debug from osm_common import dbmongo from osm_common import dbmemory from osm_common.dbbase import DbException +from itertools import chain from uuid import uuid4 # For Role _id with internal authentication backend @@ -60,7 +61,7 @@ class Authenticator: periodin_db_pruning = 60 * 30 # for the internal backend only. every 30 minutes expired tokens will be pruned - def __init__(self): + def __init__(self, valid_methods, valid_query_string): """ Authenticator initializer. Setup the initial state of the object, while it waits for the config dictionary and database initialization. @@ -70,13 +71,14 @@ class Authenticator: self.db = None self.tokens_cache = dict() 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") - self.operations = [] + self.role_permissions = [] + self.valid_methods = valid_methods + self.valid_query_string = valid_query_string def start(self, config): """ @@ -108,20 +110,7 @@ class Authenticator: else: raise AuthException("Unknown authentication backend: {}" .format(config["authentication"]["backend"])) - if not self.resources_to_operations_file: - if "resources_to_operations" in config["rbac"]: - self.resources_to_operations_file = config["rbac"]["resources_to_operations"] - else: - possible_paths = ( - __file__[:__file__.rfind("auth.py")] + "resources_to_operations.yml", - "./resources_to_operations.yml" - ) - for config_file in possible_paths: - if path.isfile(config_file): - self.resources_to_operations_file = config_file - break - if not self.resources_to_operations_file: - raise AuthException("Invalid permission configuration: resources_to_operations file missing") + if not self.roles_to_operations_file: if "roles_to_operations" in config["rbac"]: self.roles_to_operations_file = config["rbac"]["roles_to_operations"] @@ -134,12 +123,34 @@ class Authenticator: if path.isfile(config_file): self.roles_to_operations_file = config_file break - if not self.roles_to_operations_file: - raise AuthException("Invalid permission configuration: roles_to_operations file missing") + 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" + + # load role_permissions + def load_role_permissions(method_dict): + for k in method_dict: + if k == "ROLE_PERMISSION": + for method in chain(method_dict.get("METHODS", ()), method_dict.get("TODO", ())): + permission = method_dict["ROLE_PERMISSION"] + method.lower() + if permission not in self.role_permissions: + self.role_permissions.append(permission) + elif k in ("TODO", "METHODS"): + continue + else: + load_role_permissions(method_dict[k]) + + load_role_permissions(self.valid_methods) + for query_string in self.valid_query_string: + for method in ("get", "put", "patch", "post", "delete"): + permission = query_string.lower() + ":" + method + if permission not in self.role_permissions: + self.role_permissions.append(permission) + except Exception as e: raise AuthException(str(e)) @@ -158,22 +169,11 @@ class Authenticator: :param target_version: schema version that should be present in the database. :return: None if OK, exception if error or version is different. """ - # 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 # 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) - - for resource, operation in resources_to_operations_yaml["resources_to_operations"].items(): - if operation not in self.operations: - self.operations.append(operation) - self.resources_to_operations_mapping[resource] = operation - records = self.db.get_list(self.roles_to_operations_table) # Loading permissions to MongoDB if there is not any permission. @@ -228,6 +228,7 @@ class Authenticator: role_with_operations["_id"] = str(uuid4()) self.db.create(self.roles_to_operations_table, role_with_operations) + self.logger.info("Role '{}' created at database".format(role_with_operations["name"])) if self.config["authentication"]["backend"] != "internal": self.backend.assign_role_to_user("admin", "admin", "system_admin") @@ -236,17 +237,17 @@ class Authenticator: def load_operation_to_allowed_roles(self): """ - Fills the internal self.operation_to_allowed_roles based on database role content and self.operations + Fills the internal self.operation_to_allowed_roles based on database role content and self.role_permissions It works in a shadow copy and replace at the end to allow other threads working with the old copy :return: None """ - permissions = {oper: [] for oper in self.operations} + permissions = {oper: [] for oper in self.role_permissions} records = self.db.get_list(self.roles_to_operations_table) ignore_fields = ["_id", "_admin", "name", "default"] for record in records: - record_permissions = {oper: record["permissions"].get("default", False) for oper in self.operations} + record_permissions = {oper: record["permissions"].get("default", False) for oper in self.role_permissions} operations_joined = [(oper, value) for oper, value in record["permissions"].items() if oper not in ignore_fields] operations_joined.sort(key=lambda x: x[0].count(":")) @@ -264,7 +265,7 @@ class Authenticator: self.operation_to_allowed_roles = permissions - def authorize(self): + def authorize(self, role_permission=None, query_string_operations=None): token = None user_passwd64 = None try: @@ -292,7 +293,7 @@ class Authenticator: except Exception: pass outdata = self.new_token(None, {"username": user, "password": passwd}) - token = outdata["id"] + token = outdata["_id"] cherrypy.session['Authorization'] = token if not token: @@ -301,10 +302,10 @@ class Authenticator: 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) + if role_permission: + self.check_permissions(token_info, cherrypy.request.method, role_permission, + query_string_operations) return token_info - except AuthException as e: if not isinstance(e, AuthExceptionUnauthorized): if cherrypy.session.get('Authorization'): @@ -312,58 +313,48 @@ class Authenticator: cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e) raise - def new_token(self, session, indata, remote): - 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( + def new_token(self, token_info, indata, remote): + new_token_info = self.backend.authenticate( user=indata.get("username"), password=indata.get("password"), - token=current_token, + token_info=token_info, project=indata.get("project_id") ) - 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 - } + new_token_info["remote_port"] = remote.port + if not new_token_info.get("expires"): + new_token_info["expires"] = time() + 3600 + if not new_token_info.get("admin"): + new_token_info["admin"] = True if new_token_info.get("project_name") == "admin" else False + # TODO put admin in RBAC if remote.name: - new_session["remote_host"] = remote.name + new_token_info["remote_host"] = remote.name elif remote.ip: - new_session["remote_host"] = remote.ip + new_token_info["remote_host"] = remote.ip - # TODO: check if this can be avoided. Backend may provide enough information - self.tokens_cache[token_info["_id"]] = new_session + self.tokens_cache[new_token_info["_id"]] = new_token_info - return deepcopy(new_session) + # TODO call self._internal_tokens_prune(now) ? + return deepcopy(new_token_info) - def get_token_list(self, session): + def get_token_list(self, token_info): if self.config["authentication"]["backend"] == "internal": - return self._internal_get_token_list(session) + return self._internal_get_token_list(token_info) else: # TODO: check if this can be avoided. Backend may provide enough information return [deepcopy(token) for token in self.tokens_cache.values() - if token["username"] == session["username"]] + if token["username"] == token_info["username"]] - def get_token(self, session, token): + def get_token(self, token_info, token): if self.config["authentication"]["backend"] == "internal": - return self._internal_get_token(session, token) + return self._internal_get_token(token_info, token) else: # TODO: check if this can be avoided. Backend may provide enough information token_value = self.tokens_cache.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"]: + if token_value["username"] != token_info["username"] and not token_info["admin"]: raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) return token_value @@ -375,39 +366,54 @@ class Authenticator: 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)) - self.logger.info("URL: {}".format(url)) - self.logger.info("Method: {}".format(method)) - - key, parameters = self._normalize_url(url, method) - - # TODO: Check if parameters might be useful for the decision + def check_permissions(self, token_info, method, role_permission=None, query_string_operations=None): + """ + Checks that operation has permissions to be done, base on the assigned roles to this user project + :param token_info: Dictionary that contains "roles" with a list of assigned roles. + This method fills the token_info["admin"] with True or False based on assigned tokens, if any allows admin + This will be used among others to hide or not the _admin content of topics + :param method: GET,PUT, POST, ... + :param role_permission: role permission name of the operation required + :param query_string_operations: list of possible admin query strings provided by user. It is checked that the + assigned role allows this query string for this method + :return: None if granted, exception if not allowed + """ - operation = self.resources_to_operations_mapping[key] - roles_required = self.operation_to_allowed_roles[operation] - roles_allowed = [role["name"] for role in session["roles"]] + roles_required = self.operation_to_allowed_roles[role_permission] + roles_allowed = [role["name"] for role in token_info["roles"]] - # fills session["admin"] if some roles allows it - session["admin"] = False + # fills token_info["admin"] if some roles allows it + token_info["admin"] = False for role in roles_allowed: - if role in self.operation_to_allowed_roles["admin"]: - session["admin"] = True + if role in self.operation_to_allowed_roles["admin:" + method.lower()]: + token_info["admin"] = True break if "anonymous" in roles_required: return - + operation_allowed = False for role in roles_allowed: if role in roles_required: - return + operation_allowed = True + # if query_string operations, check if this role allows it + if not query_string_operations: + return + for query_string_operation in query_string_operations: + if role not in self.operation_to_allowed_roles[query_string_operation]: + break + else: + return - raise AuthExceptionUnauthorized("Access denied: lack of permissions.") + if not operation_allowed: + raise AuthExceptionUnauthorized("Access denied: lack of permissions.") + else: + raise AuthExceptionUnauthorized("Access denied: You have not permissions to use these admin query string") def get_user_list(self): return self.backend.get_user_list() def _normalize_url(self, url, method): + # DEPRECATED !!! # Removing query strings normalized_url = url if '?' not in url else url[:url.find("?")] normalized_url_splitted = normalized_url.split("/") @@ -458,16 +464,16 @@ class Authenticator: return filtered_key, parameters - def _internal_get_token_list(self, session): + def _internal_get_token_list(self, token_info): now = time() - token_list = self.db.get_list("tokens", {"username": session["username"], "expires.gt": now}) + token_list = self.db.get_list("tokens", {"username": token_info["username"], "expires.gt": now}) return token_list - def _internal_get_token(self, session, token_id): + def _internal_get_token(self, token_info, token_id): token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False) if not token_value: raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND) - if token_value["username"] != session["username"] and not session["admin"]: + if token_value["username"] != token_info["username"] and not token_info["admin"]: raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) return token_value