X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fadmin_topics.py;h=ada9c7b11502fc5ce67dc78cf9a663f40d8f467b;hp=efcb0f17d03ee5da52db53542d4974dfe6af230d;hb=refs%2Fchanges%2F06%2F8206%2F1;hpb=01b15d3166ea28266fb3d994d0615e4091c43c08 diff --git a/osm_nbi/admin_topics.py b/osm_nbi/admin_topics.py index efcb0f1..ada9c7b 100644 --- a/osm_nbi/admin_topics.py +++ b/osm_nbi/admin_topics.py @@ -18,16 +18,14 @@ from uuid import uuid4 from hashlib import sha256 from http import HTTPStatus from time import time -from validation import user_new_schema, user_edit_schema, project_new_schema, project_edit_schema -from validation import vim_account_new_schema, vim_account_edit_schema, sdn_new_schema, sdn_edit_schema -from validation import wim_account_new_schema, wim_account_edit_schema, roles_new_schema, roles_edit_schema -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 osm_nbi.validation import user_new_schema, user_edit_schema, project_new_schema, project_edit_schema, \ + vim_account_new_schema, vim_account_edit_schema, sdn_new_schema, sdn_edit_schema, \ + wim_account_new_schema, wim_account_edit_schema, roles_new_schema, roles_edit_schema, \ + k8scluster_new_schema, k8scluster_edit_schema, k8srepo_new_schema, k8srepo_edit_schema, \ + validate_input, ValidationError, is_valid_uuid # To check that User/Project Names don't look like UUIDs +from osm_nbi.base_topic import BaseTopic, EngineException +from osm_nbi.authconn import AuthconnNotFoundException, AuthconnConflictException from osm_common.dbbase import deep_update_rfc7396 -from authconn import AuthconnNotFoundException, AuthconnConflictException -# from authconn_keystone import AuthconnKeystone __author__ = "Alfonso Tierno " @@ -39,8 +37,8 @@ class UserTopic(BaseTopic): schema_edit = user_edit_schema multiproject = False - def __init__(self, db, fs, msg): - BaseTopic.__init__(self, db, fs, msg) + def __init__(self, db, fs, msg, auth): + BaseTopic.__init__(self, db, fs, msg, auth) @staticmethod def _get_project_filter(session): @@ -133,8 +131,8 @@ class ProjectTopic(BaseTopic): schema_edit = project_edit_schema multiproject = False - def __init__(self, db, fs, msg): - BaseTopic.__init__(self, db, fs, msg) + def __init__(self, db, fs, msg, auth): + BaseTopic.__init__(self, db, fs, msg, auth) @staticmethod def _get_project_filter(session): @@ -201,7 +199,7 @@ class ProjectTopic(BaseTopic): class CommonVimWimSdn(BaseTopic): """Common class for VIM, WIM SDN just to unify methods that are equal to all of them""" - config_to_encrypt = () # what keys at config must be encrypted because contains passwords + config_to_encrypt = {} # what keys at config must be encrypted because contains passwords password_to_encrypt = "" # key that contains a password @staticmethod @@ -250,6 +248,7 @@ class CommonVimWimSdn(BaseTopic): :param edit_content: user requested update content :return: operation id """ + super().format_on_edit(final_content, edit_content) # encrypt passwords schema_version = final_content.get("schema_version") @@ -258,8 +257,10 @@ class CommonVimWimSdn(BaseTopic): final_content[self.password_to_encrypt] = self.db.encrypt(edit_content[self.password_to_encrypt], schema_version=schema_version, salt=final_content["_id"]) - if edit_content.get("config") and self.config_to_encrypt: - for p in self.config_to_encrypt: + config_to_encrypt_keys = self.config_to_encrypt.get(schema_version) or self.config_to_encrypt.get("default") + if edit_content.get("config") and config_to_encrypt_keys: + + for p in config_to_encrypt_keys: if edit_content["config"].get(p): final_content["config"][p] = self.db.encrypt(edit_content["config"][p], schema_version=schema_version, @@ -278,15 +279,16 @@ class CommonVimWimSdn(BaseTopic): :return: op_id: operation id on asynchronous operation, None otherwise. In addition content is modified """ super().format_on_new(content, project_id=project_id, make_public=make_public) - content["schema_version"] = schema_version = "1.1" + content["schema_version"] = schema_version = "1.11" # encrypt passwords if content.get(self.password_to_encrypt): content[self.password_to_encrypt] = self.db.encrypt(content[self.password_to_encrypt], schema_version=schema_version, salt=content["_id"]) - if content.get("config") and self.config_to_encrypt: - for p in self.config_to_encrypt: + config_to_encrypt_keys = self.config_to_encrypt.get(schema_version) or self.config_to_encrypt.get("default") + if content.get("config") and config_to_encrypt_keys: + for p in config_to_encrypt_keys: if content["config"].get(p): content["config"][p] = self.db.encrypt(content["config"][p], schema_version=schema_version, @@ -363,7 +365,8 @@ class VimAccountTopic(CommonVimWimSdn): schema_edit = vim_account_edit_schema multiproject = True password_to_encrypt = "vim_password" - config_to_encrypt = ("admin_password", "nsx_password", "vcenter_password") + config_to_encrypt = {"1.1": ("admin_password", "nsx_password", "vcenter_password"), + "default": ("admin_password", "nsx_password", "vcenter_password", "vrops_password")} class WimAccountTopic(CommonVimWimSdn): @@ -373,7 +376,7 @@ class WimAccountTopic(CommonVimWimSdn): schema_edit = wim_account_edit_schema multiproject = True password_to_encrypt = "wim_password" - config_to_encrypt = () + config_to_encrypt = {} class SdnTopic(CommonVimWimSdn): @@ -383,7 +386,41 @@ class SdnTopic(CommonVimWimSdn): schema_edit = sdn_edit_schema multiproject = True password_to_encrypt = "password" - config_to_encrypt = () + config_to_encrypt = {} + + +class K8sClusterTopic(CommonVimWimSdn): + topic = "k8sclusters" + topic_msg = "k8scluster" + schema_new = k8scluster_new_schema + schema_edit = k8scluster_edit_schema + multiproject = True + password_to_encrypt = None + config_to_encrypt = {} + + def format_on_new(self, content, project_id=None, make_public=False): + oid = super().format_on_new(content, project_id, make_public) + self.db.encrypt_decrypt_fields(content["credentials"], 'encrypt', ['password', 'secret'], + schema_version=content["schema_version"], salt=content["_id"]) + return oid + + def format_on_edit(self, final_content, edit_content): + if final_content.get("schema_version") and edit_content.get("credentials"): + self.db.encrypt_decrypt_fields(edit_content["credentials"], 'encrypt', ['password', 'secret'], + schema_version=final_content["schema_version"], salt=final_content["_id"]) + deep_update_rfc7396(final_content["credentials"], edit_content["credentials"]) + oid = super().format_on_edit(final_content, edit_content) + return oid + + +class K8sRepoTopic(CommonVimWimSdn): + topic = "k8srepos" + topic_msg = "k8srepo" + schema_new = k8srepo_new_schema + schema_edit = k8srepo_edit_schema + multiproject = True + password_to_encrypt = None + config_to_encrypt = {} class UserTopicAuth(UserTopic): @@ -393,8 +430,8 @@ class UserTopicAuth(UserTopic): schema_edit = user_edit_schema def __init__(self, db, fs, msg, auth): - UserTopic.__init__(self, db, fs, msg) - self.auth = auth + UserTopic.__init__(self, db, fs, msg, auth) + # self.auth = auth def check_conflict_on_new(self, session, indata): """ @@ -529,7 +566,7 @@ class UserTopicAuth(UserTopic): rollback.append({"topic": self.topic, "_id": _id}) # del content["password"] - # self._send_msg("create", content) + # self._send_msg("created", content) return _id, None except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) @@ -544,10 +581,10 @@ class UserTopicAuth(UserTopic): """ # Allow _id to be a name or uuid filter_q = {self.id_field(self.topic, _id): _id} - users = self.auth.get_user_list(filter_q) - + # users = self.auth.get_user_list(filter_q) + users = self.list(session, filter_q) # To allow default filtering (Bug 853) if len(users) == 1: - return self.format_on_show(users[0]) + return users[0] elif len(users) > 1: raise EngineException("Too many users found", HTTPStatus.CONFLICT) else: @@ -596,6 +633,16 @@ class UserTopicAuth(UserTopic): rid = role[0]["_id"] if "add_project_role_mappings" not in indata: indata["add_project_role_mappings"] = [] + if "remove_project_role_mappings" not in indata: + indata["remove_project_role_mappings"] = [] + if isinstance(indata.get("projects"), dict): + # backward compatible + for k, v in indata["projects"].items(): + if k.startswith("$") and v is None: + indata["remove_project_role_mappings"].append({"project": k[1:]}) + elif k.startswith("$+"): + indata["add_project_role_mappings"].append({"project": v, "role": rid}) + del indata["projects"] for proj in indata.get("projects", []) + indata.get("add_projects", []): indata["add_project_role_mappings"].append({"project": proj, "role": rid}) @@ -665,9 +712,11 @@ class UserTopicAuth(UserTopic): :param filter_q: filter of data to be applied :return: The list, it can be empty if no one match the filter. """ - users = [self.format_on_show(user) for user in self.auth.get_user_list(filter_q)] - - return users + user_list = self.auth.get_user_list(filter_q) + if not session["allow_show_user_project_role"]: + # Bug 853 - Default filtering + user_list = [usr for usr in user_list if usr["username"] == session["username"]] + return user_list def delete(self, session, _id, dry_run=False): """ @@ -696,8 +745,8 @@ class ProjectTopicAuth(ProjectTopic): schema_edit = project_edit_schema def __init__(self, db, fs, msg, auth): - ProjectTopic.__init__(self, db, fs, msg) - self.auth = auth + ProjectTopic.__init__(self, db, fs, msg, auth) + # self.auth = auth def check_conflict_on_new(self, session, indata): """ @@ -731,14 +780,14 @@ class ProjectTopicAuth(ProjectTopic): project_name = edit_content.get("name") if project_name != final_content["name"]: # It is a true renaming if is_valid_uuid(project_name): - raise EngineException("project name '{}' cannot have an uuid format".format(project_name), + raise EngineException("project name '{}' cannot have an uuid format".format(project_name), HTTPStatus.UNPROCESSABLE_ENTITY) if final_content["name"] == "admin": raise EngineException("You cannot rename project 'admin'", http_code=HTTPStatus.CONFLICT) # Check that project name is not used, regardless keystone already checks this - if self.auth.get_project_list(filter_q={"name": project_name}): + if project_name and self.auth.get_project_list(filter_q={"name": project_name}): raise EngineException("project '{}' is already used".format(project_name), HTTPStatus.CONFLICT) def check_conflict_on_del(self, session, _id, db_content): @@ -766,9 +815,10 @@ class ProjectTopicAuth(ProjectTopic): # If any user is using this project, raise CONFLICT exception if not session["force"]: for user in self.auth.get_user_list(): - if _id in [proj["_id"] for proj in user.get("projects", [])]: - raise EngineException("Project '{}' ({}) is being used by user '{}'" - .format(db_content["name"], _id, user["username"]), HTTPStatus.CONFLICT) + for prm in user.get("project_role_mappings"): + if prm["project"] == _id: + raise EngineException("Project '{}' ({}) is being used by user '{}'" + .format(db_content["name"], _id, user["username"]), HTTPStatus.CONFLICT) # If any VNFD, NSD, NST, PDU, etc. is using this project, raise CONFLICT exception if not session["force"]: @@ -800,7 +850,7 @@ class ProjectTopicAuth(ProjectTopic): self.format_on_new(content, project_id=session["project_id"], make_public=session["public"]) _id = self.auth.create_project(content) rollback.append({"topic": self.topic, "_id": _id}) - # self._send_msg("create", content) + # self._send_msg("created", content) return _id, None except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) @@ -815,8 +865,8 @@ class ProjectTopicAuth(ProjectTopic): """ # Allow _id to be a name or uuid filter_q = {self.id_field(self.topic, _id): _id} - projects = self.auth.get_project_list(filter_q=filter_q) - + # projects = self.auth.get_project_list(filter_q=filter_q) + projects = self.list(session, filter_q) # To allow default filtering (Bug 853) if len(projects) == 1: return projects[0] elif len(projects) > 1: @@ -832,7 +882,13 @@ class ProjectTopicAuth(ProjectTopic): :param filter_q: filter of data to be applied :return: The list, it can be empty if no one match the filter. """ - return self.auth.get_project_list(filter_q) + project_list = self.auth.get_project_list(filter_q) + if not session["allow_show_user_project_role"]: + # Bug 853 - Default filtering + user = self.auth.get_user(session["username"]) + projects = [prm["project"] for prm in user["project_role_mappings"]] + project_list = [proj for proj in project_list if proj["_id"] in projects] + return project_list def delete(self, session, _id, dry_run=False): """ @@ -876,8 +932,7 @@ class ProjectTopicAuth(ProjectTopic): self.check_conflict_on_edit(session, content, indata, _id=_id) self.format_on_edit(content, indata) - if "name" in indata: - content["name"] = indata["name"] + deep_update_rfc7396(content, indata) self.auth.update_project(content["_id"], content) except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) @@ -891,8 +946,8 @@ class RoleTopicAuth(BaseTopic): multiproject = False def __init__(self, db, fs, msg, auth, ops): - BaseTopic.__init__(self, db, fs, msg) - self.auth = auth + BaseTopic.__init__(self, db, fs, msg, auth) + # self.auth = auth self.operations = ops # self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles" @@ -956,6 +1011,11 @@ class RoleTopicAuth(BaseTopic): :param indata: data to be inserted :return: None or raises EngineException """ + # check name is not uuid + role_name = indata.get("name") + if is_valid_uuid(role_name): + raise EngineException("role name '{}' cannot have an uuid format".format(role_name), + HTTPStatus.UNPROCESSABLE_ENTITY) # check name not exists name = indata["name"] # if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False): @@ -977,6 +1037,17 @@ class RoleTopicAuth(BaseTopic): if "admin" not in final_content["permissions"]: final_content["permissions"]["admin"] = False + # check name is not uuid + role_name = edit_content.get("name") + if is_valid_uuid(role_name): + raise EngineException("role name '{}' cannot have an uuid format".format(role_name), + HTTPStatus.UNPROCESSABLE_ENTITY) + + # Check renaming of admin roles + role = self.auth.get_role(_id) + if role["name"] in ["system_admin", "project_admin"]: + raise EngineException("You cannot rename role '{}'".format(role["name"]), http_code=HTTPStatus.FORBIDDEN) + # check name not exists if "name" in edit_content: role_name = edit_content["name"] @@ -1000,9 +1071,10 @@ class RoleTopicAuth(BaseTopic): # If any user is using this role, raise CONFLICT exception for user in self.auth.get_user_list(): - if _id in [prl["_id"] for proj in user.get("projects", []) for prl in proj.get("roles", [])]: - raise EngineException("Role '{}' ({}) is being used by user '{}'" - .format(role["name"], _id, user["username"]), HTTPStatus.CONFLICT) + for prm in user.get("project_role_mappings"): + if prm["role"] == _id: + raise EngineException("Role '{}' ({}) is being used by user '{}'" + .format(role["name"], _id, user["username"]), HTTPStatus.CONFLICT) @staticmethod def format_on_new(content, project_id=None, make_public=False): # TO BE REMOVED ? @@ -1059,7 +1131,8 @@ class RoleTopicAuth(BaseTopic): :return: dictionary, raise exception if not found. """ filter_q = {BaseTopic.id_field(self.topic, _id): _id} - roles = self.auth.get_role_list(filter_q) + # roles = self.auth.get_role_list(filter_q) + roles = self.list(session, filter_q) # To allow default filtering (Bug 853) if not roles: raise AuthconnNotFoundException("Not found any role with filter {}".format(filter_q)) elif len(roles) > 1: @@ -1074,7 +1147,13 @@ class RoleTopicAuth(BaseTopic): :param filter_q: filter of data to be applied :return: The list, it can be empty if no one match the filter. """ - return self.auth.get_role_list(filter_q) + role_list = self.auth.get_role_list(filter_q) + if not session["allow_show_user_project_role"]: + # Bug 853 - Default filtering + user = self.auth.get_user(session["username"]) + roles = [prm["role"] for prm in user["project_role_mappings"]] + role_list = [role for role in role_list if role["_id"] in roles] + return role_list def new(self, rollback, session, indata=None, kwargs=None, headers=None): """ @@ -1100,7 +1179,7 @@ class RoleTopicAuth(BaseTopic): content["_id"] = rid # _id = self.db.create(self.topic, content) rollback.append({"topic": self.topic, "_id": rid}) - # self._send_msg("create", content) + # self._send_msg("created", content) return rid, None except ValidationError as e: raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)