X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fauth.py;h=d91618919f95f809abbcc7f6f1be1478aa09f83f;hp=18986a3ab4d7e64a00280519a495fb37d510ea40;hb=126af57f9b2b09f1ab72c6a7f6dfce4e79d0480f;hpb=cc02e9ae712026b0c07389f66304fc9c1fde86e1 diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index 18986a3..d916189 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -25,7 +25,6 @@ Authenticator is responsible for authenticating the users, create the tokens unscoped and scoped, retrieve the role list inside the projects that they are inserted """ -from os import path __author__ = "Eduardo Sousa ; Alfonso Tierno " __date__ = "$27-jul-2018 23:59:59$" @@ -40,7 +39,8 @@ from hashlib import sha256 from http import HTTPStatus from random import choice as random_choice from time import time -from uuid import uuid4 +from os import path +from base_topic import BaseTopic # To allow project names in project_id from authconn import AuthException from authconn_keystone import AuthconnKeystone @@ -55,6 +55,7 @@ class Authenticator: Authorization. Initially it should support Openstack Keystone as a backend through a plugin model where more backends can be added and a RBAC model to manage permissions on operations. + This class must be threading safe """ periodin_db_pruning = 60 * 30 # for the internal backend only. every 30 minutes expired tokens will be pruned @@ -108,8 +109,11 @@ class Authenticator: if "resources_to_operations" in config["rbac"]: self.resources_to_operations_file = config["rbac"]["resources_to_operations"] else: - for config_file in (__file__[:__file__.rfind("auth.py")] + "resources_to_operations.yml", - "./resources_to_operations.yml"): + 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 @@ -119,8 +123,11 @@ class Authenticator: if "roles_to_operations" in config["rbac"]: self.roles_to_operations_file = config["rbac"]["roles_to_operations"] else: - for config_file in (__file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml", - "./roles_to_operations.yml"): + possible_paths = ( + __file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml", + "./roles_to_operations.yml" + ) + for config_file in possible_paths: if path.isfile(config_file): self.roles_to_operations_file = config_file break @@ -147,15 +154,17 @@ 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 + operations = [] 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(): - operation_key = operation.replace(".", ":") - if operation_key not in operations: - operations.append(operation_key) - self.resources_to_operations_mapping[resource] = operation_key + if operation not in operations: + operations.append(operation) + self.resources_to_operations_mapping[resource] = operation records = self.db.get_list("roles_operations") @@ -184,19 +193,18 @@ class Authenticator: if not isinstance(is_allowed, bool): continue - if operation == ".": + if operation == ":": root = is_allowed continue - if len(operation) != 1 and operation[-1] == ".": - self.logger.warning("Invalid operation {0} terminated in '.'. " + if len(operation) != 1 and operation[-1] == ":": + self.logger.warning("Invalid operation {0} terminated in ':'. " "Operation will be discarded" .format(operation)) continue - operation_key = operation.replace(".", ":") - if operation_key not in role_ops.keys(): - role_ops[operation_key] = is_allowed + if operation not in role_ops.keys(): + role_ops[operation] = is_allowed else: self.logger.info("In role {0}, the operation {1} with the value {2} was discarded due to " "repetition.".format(role_with_operations["role"], operation, is_allowed)) @@ -208,28 +216,34 @@ class Authenticator: now = time() operation_to_roles_item = { - "_id": str(uuid4()), "_admin": { "created": now, "modified": now, }, - "role": role_with_operations["role"], + "name": role_with_operations["role"], "root": root } for operation, value in role_ops.items(): operation_to_roles_item[operation] = value + if self.config["authentication"]["backend"] != "internal" and \ + role_with_operations["role"] != "anonymous": + keystone_id = [role for role in self.backend.get_role_list() + if role["name"] == role_with_operations["role"]] + if keystone_id: + keystone_id = keystone_id[0] + else: + keystone_id = self.backend.create_role(role_with_operations["role"]) + operation_to_roles_item["_id"] = keystone_id["_id"] + self.db.create("roles_operations", operation_to_roles_item) permissions = {oper: [] for oper in operations} records = self.db.get_list("roles_operations") - ignore_fields = ["_id", "_admin", "role", "root"] - roles = [] + ignore_fields = ["_id", "_admin", "name", "root"] for record in records: - - roles.append(record["role"]) record_permissions = {oper: record["root"] for oper in operations} operations_joined = [(oper, value) for oper, value in record.items() if oper not in ignore_fields] operations_joined.sort(key=lambda x: x[0].count(":")) @@ -243,17 +257,12 @@ class Authenticator: allowed_operations = [k for k, v in record_permissions.items() if v is True] for allowed_op in allowed_operations: - permissions[allowed_op].append(record["role"]) + permissions[allowed_op].append(record["name"]) for oper, role_list in permissions.items(): self.operation_to_allowed_roles[oper] = role_list if self.config["authentication"]["backend"] != "internal": - for role in roles: - if role == "anonymous": - continue - self.backend.create_role(role) - self.backend.assign_role_to_user("admin", "admin", "system_admin") def authorize(self): @@ -402,7 +411,7 @@ class Authenticator: operation = self.resources_to_operations_mapping[key] roles_required = self.operation_to_allowed_roles[operation] - roles_allowed = self.backend.get_role_list(session["id"]) + roles_allowed = self.backend.get_user_role_list(session["id"]) if "anonymous" in roles_required: return @@ -413,6 +422,9 @@ class Authenticator: raise AuthException("Access denied: lack of permissions.") + def get_user_list(self): + return self.backend.get_user_list() + def _normalize_url(self, url, method): # Removing query strings normalized_url = url if '?' not in url else url[:url.find("?")] @@ -472,7 +484,8 @@ class Authenticator: now = time() session = self.tokens_cache.get(token_id) if session and session["expires"] < now: - del self.tokens_cache[token_id] + # 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 @@ -493,7 +506,7 @@ class Authenticator: 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"]} + "username": self.config["global"]["test.user_not_authorized"], "admin": True} else: raise @@ -524,17 +537,21 @@ class Authenticator: token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(0, 32)) - if indata.get("project_id"): - project_id = indata.get("project_id") - if project_id not in user_content["projects"]: - raise AuthException("project {} not allowed for this user" - .format(project_id), http_code=HTTPStatus.UNAUTHORIZED) + 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: - project = self.db.get_one("projects", {"_id": project_id}) + # 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"],