From: tierno Date: Thu, 13 Jun 2019 22:37:04 +0000 (+0000) Subject: bug 767: uniform role format X-Git-Tag: v6.0.0~3 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=refs%2Fchanges%2F66%2F7666%2F2;p=osm%2FNBI.git bug 767: uniform role format Change-Id: I5ca0982c20ca018f787071ea9f82857afcc24e5e Signed-off-by: tierno --- diff --git a/osm_nbi/admin_topics.py b/osm_nbi/admin_topics.py index cbe2f4c..071ed3b 100644 --- a/osm_nbi/admin_topics.py +++ b/osm_nbi/admin_topics.py @@ -858,7 +858,7 @@ class ProjectTopicAuth(ProjectTopic): class RoleTopicAuth(BaseTopic): topic = "roles_operations" - topic_msg = "roles" + topic_msg = None # "roles" schema_new = roles_new_schema schema_edit = roles_edit_schema multiproject = False @@ -878,25 +878,19 @@ class RoleTopicAuth(BaseTopic): :param role_definitions: role definition to test :return: None if ok, raises ValidationError exception on error """ - ignore_fields = ["_id", "_admin", "name"] - for role_def in role_definitions.keys(): + if not role_definitions.get("permissions"): + return + ignore_fields = ["admin", "default"] + for role_def in role_definitions["permissions"].keys(): if role_def in ignore_fields: continue - if role_def == "root": - if isinstance(role_definitions[role_def], bool): - continue - else: - raise ValidationError("Operation authorization \".\" should be True/False.") if role_def[-1] == ":": - raise ValidationError("Operation cannot end with \".\"") + raise ValidationError("Operation cannot end with ':'") role_def_matches = [op for op in operations if op.startswith(role_def)] if len(role_def_matches) == 0: - raise ValidationError("No matching operation found.") - - if not isinstance(role_definitions[role_def], bool): - raise ValidationError("Operation authorization {} should be True/False.".format(role_def)) + raise ValidationError("Invalid permission '{}'".format(role_def)) def _validate_input_new(self, input, force=False): """ @@ -934,11 +928,9 @@ class RoleTopicAuth(BaseTopic): :param indata: data to be inserted :return: None or raises EngineException """ - role = indata.get("name") - role_list = list(map(lambda x: x["name"], self.auth.get_role_list())) - - if role in role_list: - raise EngineException("role '{}' exists".format(role), HTTPStatus.CONFLICT) + # check name not exists + if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False): + raise EngineException("role name '{}' exists".format(indata["name"]), HTTPStatus.CONFLICT) def check_conflict_on_edit(self, session, final_content, edit_content, _id): """ @@ -950,12 +942,16 @@ class RoleTopicAuth(BaseTopic): :param _id: internal _id :return: None or raises EngineException """ - roles = self.auth.get_role_list() - system_admin_role = [role for role in roles - if role["name"] == "system_admin"][0] + if "default" not in final_content["permissions"]: + final_content["permissions"]["default"] = False + if "admin" not in final_content["permissions"]: + final_content["permissions"]["admin"] = False - if _id == system_admin_role["_id"]: - raise EngineException("You cannot edit system_admin role", http_code=HTTPStatus.FORBIDDEN) + # check name not exists + if "name" in edit_content: + role_name = edit_content["name"] + if self.db.get_one(self.topic, {"name": role_name, "_id.ne": _id}, fail_on_empty=False, fail_on_more=False): + raise EngineException("role name '{}' exists".format(role_name), HTTPStatus.CONFLICT) def check_conflict_on_del(self, session, _id, db_content): """ @@ -990,12 +986,13 @@ class RoleTopicAuth(BaseTopic): content["_admin"]["created"] = now content["_admin"]["modified"] = now - if ":" in content.keys(): - content["root"] = content[":"] - del content[":"] + if "permissions" not in content: + content["permissions"] = {} - if "root" not in content.keys(): - content["root"] = False + if "default" not in content["permissions"]: + content["permissions"]["default"] = False + if "admin" not in content["permissions"]: + content["permissions"]["admin"] = False @staticmethod def format_on_edit(final_content, edit_content): @@ -1008,78 +1005,69 @@ class RoleTopicAuth(BaseTopic): """ final_content["_admin"]["modified"] = time() - ignore_fields = ["_id", "name", "_admin"] - delete_keys = [key for key in final_content.keys() if key not in ignore_fields] - - for key in delete_keys: - del final_content[key] - - # Saving the role definition - for role_def, value in edit_content.items(): - final_content[role_def] = value - - if ":" in final_content.keys(): - final_content["root"] = final_content[":"] - del final_content[":"] - - if "root" not in final_content.keys(): - final_content["root"] = False - - @staticmethod - def format_on_show(content): - """ - Modifies the content of the role information to separate the role - metadata from the role definition. Eases the reading process of the - role definition. - - :param definition: role definition to be processed - """ - content["_id"] = str(content["_id"]) - - def show(self, session, _id): - """ - Get complete information on an topic - - :param session: contains "username", "admin", "force", "public", "project_id", "set_project" - :param _id: server internal id - :return: dictionary, raise exception if not found. - """ - filter_db = {"_id": _id} - - role = self.db.get_one(self.topic, filter_db) - new_role = dict(role) - self.format_on_show(new_role) - - return new_role - - def list(self, session, filter_q=None): - """ - Get a list of the topic that matches a filter - - :param session: contains "username", "admin", "force", "public", "project_id", "set_project" - :param filter_q: filter of data to be applied - :return: The list, it can be empty if no one match the filter. - """ - if not filter_q: - filter_q = {} - - if ":" in filter_q: - filter_q["root"] = filter_q[":"] - - for key in filter_q.keys(): - if key == "name": - continue - filter_q[key] = filter_q[key] in ["True", "true"] + if "permissions" not in final_content: + final_content["permissions"] = {} - roles = self.db.get_list(self.topic, filter_q) - new_roles = [] + if "default" not in final_content["permissions"]: + final_content["permissions"]["default"] = False + if "admin" not in final_content["permissions"]: + final_content["permissions"]["admin"] = False - for role in roles: - new_role = dict(role) - self.format_on_show(new_role) - new_roles.append(new_role) + # @staticmethod + # def format_on_show(content): + # """ + # Modifies the content of the role information to separate the role + # metadata from the role definition. Eases the reading process of the + # role definition. + # + # :param definition: role definition to be processed + # """ + # content["_id"] = str(content["_id"]) + # + # def show(self, session, _id): + # """ + # Get complete information on an topic + # + # :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + # :param _id: server internal id + # :return: dictionary, raise exception if not found. + # """ + # filter_db = {"_id": _id} + # + # role = self.db.get_one(self.topic, filter_db) + # new_role = dict(role) + # self.format_on_show(new_role) + # + # return new_role - return new_roles + # def list(self, session, filter_q=None): + # """ + # Get a list of the topic that matches a filter + # + # :param session: contains "username", "admin", "force", "public", "project_id", "set_project" + # :param filter_q: filter of data to be applied + # :return: The list, it can be empty if no one match the filter. + # """ + # if not filter_q: + # filter_q = {} + # + # if ":" in filter_q: + # filter_q["root"] = filter_q[":"] + # + # for key in filter_q.keys(): + # if key == "name": + # continue + # filter_q[key] = filter_q[key] in ["True", "true"] + # + # roles = self.db.get_list(self.topic, filter_q) + # new_roles = [] + # + # for role in roles: + # new_role = dict(role) + # self.format_on_show(new_role) + # new_roles.append(new_role) + # + # return new_roles def new(self, rollback, session, indata=None, kwargs=None, headers=None): """ @@ -1093,16 +1081,16 @@ class RoleTopicAuth(BaseTopic): :return: _id: identity of the inserted data. """ try: - content = BaseTopic._remove_envelop(indata) + content = self._remove_envelop(indata) # Override descriptor with query string kwargs - BaseTopic._update_input_with_kwargs(content, kwargs) + self._update_input_with_kwargs(content, kwargs) content = self._validate_input_new(content, session["force"]) self.check_conflict_on_new(session, content) self.format_on_new(content, project_id=session["project_id"], make_public=session["public"]) role_name = content["name"] - role = self.auth.create_role(role_name) - content["_id"] = role["_id"] + role_id = self.auth.create_role(role_name) + content["_id"] = role_id _id = self.db.create(self.topic, content) rollback.append({"topic": self.topic, "_id": _id}) # self._send_msg("create", content) @@ -1138,19 +1126,6 @@ class RoleTopicAuth(BaseTopic): :param content: :return: _id: identity of the inserted data. """ - indata = self._remove_envelop(indata) - - # Override descriptor with query string kwargs - if kwargs: - self._update_input_with_kwargs(indata, kwargs) - try: - indata = self._validate_input_edit(indata, force=session["force"]) - - if not content: - content = self.show(session, _id) - self.check_conflict_on_edit(session, content, indata, _id=_id) - self.format_on_edit(content, indata) - self.db.replace(self.topic, _id, content) - return id - except ValidationError as e: - raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY) + _id = super().edit(session, _id, indata, kwargs, content) + if indata.get("name"): + self.auth.update_role(_id, name=indata.get("name")) diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index f7e4844..e9a6b4e 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -75,6 +75,7 @@ class Authenticator: self.resources_to_operations_mapping = {} self.operation_to_allowed_roles = {} self.logger = logging.getLogger("nbi.authenticator") + self.operations = [] def start(self, config): """ @@ -157,95 +158,88 @@ class Authenticator: 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(): - if operation not in operations: - operations.append(operation) + if operation not in self.operations: + self.operations.append(operation) self.resources_to_operations_mapping[resource] = operation records = self.db.get_list("roles_operations") - # Loading permissions to MongoDB. If there are permissions already in MongoDB, do nothing. - if len(records) == 0: + # Loading permissions to MongoDB if there is not any permission. + if not records: with open(self.roles_to_operations_file, "r") as stream: roles_to_operations_yaml = yaml.load(stream) - roles = [] - for role_with_operations in roles_to_operations_yaml["roles_to_operations"]: - # Verifying if role already exists. If it does, send warning to log and ignore it. - if role_with_operations["role"] not in roles: - roles.append(role_with_operations["role"]) + role_names = [] + for role_with_operations in roles_to_operations_yaml["roles"]: + # Verifying if role already exists. If it does, raise exception + if role_with_operations["name"] not in role_names: + role_names.append(role_with_operations["name"]) else: - self.logger.warning("Duplicated role with name: {0}. Role definition is ignored." - .format(role_with_operations["role"])) - continue - - role_ops = {} - root = None + raise AuthException("Duplicated role name '{}' at file '{}''" + .format(role_with_operations["name"], self.roles_to_operations_file)) - if not role_with_operations["operations"]: + if not role_with_operations["permissions"]: continue - for operation, is_allowed in role_with_operations["operations"].items(): + for permission, is_allowed in role_with_operations["permissions"].items(): if not isinstance(is_allowed, bool): - continue + raise AuthException("Invalid value for permission '{}' at role '{}'; at file '{}'" + .format(permission, role_with_operations["name"], + self.roles_to_operations_file)) - if operation == ":": - root = is_allowed - continue + # TODO chek permission is ok + if permission[-1] == ":": + raise AuthException("Invalid permission '{}' terminated in ':' for role '{}'; at file {}" + .format(permission, role_with_operations["name"], + self.roles_to_operations_file)) - if len(operation) != 1 and operation[-1] == ":": - self.logger.warning("Invalid operation {0} terminated in ':'. " - "Operation will be discarded" - .format(operation)) - continue - - 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)) - - if not root: - root = False - self.logger.info("Root for role {0} not defined. Default value 'False' applied." - .format(role_with_operations["role"])) + if "default" not in role_with_operations["permissions"]: + role_with_operations["permissions"]["default"] = False + if "admin" not in role_with_operations["permissions"]: + role_with_operations["permissions"]["admin"] = False now = time() - operation_to_roles_item = { - "_admin": { - "created": now, - "modified": now, - }, - "name": role_with_operations["role"], - "root": root + role_with_operations["_admin"] = { + "created": now, + "modified": now, } - 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] + 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: - keystone_id = self.backend.create_role(role_with_operations["role"]) - operation_to_roles_item["_id"] = keystone_id["_id"] + backend_id = self.backend.create_role(role_with_operations["name"]) + role_with_operations["_id"] = backend_id - self.db.create("roles_operations", operation_to_roles_item) + self.db.create("roles_operations", role_with_operations) - permissions = {oper: [] for oper in operations} + if self.config["authentication"]["backend"] != "internal": + self.backend.assign_role_to_user("admin", "admin", "system_admin") + + self.load_operation_to_allowed_roles() + + def load_operation_to_allowed_roles(self): + """ + Fills the internal self.operation_to_allowed_roles based on database role content and self.operations + :return: None + """ + + permissions = {oper: [] for oper in self.operations} records = self.db.get_list("roles_operations") - ignore_fields = ["_id", "_admin", "name", "root"] + ignore_fields = ["_id", "_admin", "name", "default", "admin"] for record in records: - 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] + record_permissions = {oper: record["permissions"].get("default", False) for oper in self.operations} + 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(":")) for oper in operations_joined: @@ -262,9 +256,6 @@ class Authenticator: for oper, role_list in permissions.items(): self.operation_to_allowed_roles[oper] = role_list - if self.config["authentication"]["backend"] != "internal": - self.backend.assign_role_to_user("admin", "admin", "system_admin") - def authorize(self): token = None user_passwd64 = None @@ -421,7 +412,7 @@ class Authenticator: operation = self.resources_to_operations_mapping[key] roles_required = self.operation_to_allowed_roles[operation] - roles_allowed = self.backend.get_user_role_list(session["id"]) + roles_allowed = self.backend.get_user_role_list(session["_id"]) if "anonymous" in roles_required: return diff --git a/osm_nbi/authconn.py b/osm_nbi/authconn.py index 42707fe..b408052 100644 --- a/osm_nbi/authconn.py +++ b/osm_nbi/authconn.py @@ -87,6 +87,14 @@ class AuthconnNotFoundException(AuthconnException): super().__init__(message, http_code) +class AuthconnConflictException(AuthconnException): + """ + The operation has conflicts. + """ + def __init__(self, message, http_code=HTTPStatus.CONFLICT): + super().__init__(message, http_code) + + class Authconn: """ Abstract base class for all the Auth backend connector plugins. @@ -226,14 +234,24 @@ class Authconn: """ raise AuthconnNotImplementedException("Should have implemented this") - def get_role_list(self): + def get_role_list(self, filter_q=None): """ Get all the roles. + :param filter_q: dictionary to filter role list by _id and/or name. :return: list of roles """ raise AuthconnNotImplementedException("Should have implemented this") + def update_role(self, role, new_name): + """ + Change the name of a role + :param role: role name or id to be changed + :param new_name: new name + :return: None + """ + raise AuthconnNotImplementedException("Should have implemented this") + def create_project(self, project): """ Create a project. diff --git a/osm_nbi/authconn_keystone.py b/osm_nbi/authconn_keystone.py index bb69381..23b07e3 100644 --- a/osm_nbi/authconn_keystone.py +++ b/osm_nbi/authconn_keystone.py @@ -28,7 +28,8 @@ it for OSM. __author__ = "Eduardo Sousa " __date__ = "$27-jul-2018 23:59:59$" -from authconn import Authconn, AuthException, AuthconnOperationException, AuthconnNotFoundException +from authconn import Authconn, AuthException, AuthconnOperationException, AuthconnNotFoundException, \ + AuthconnConflictException import logging import requests @@ -364,20 +365,27 @@ class AuthconnKeystone(Authconn): self.logger.exception("Error during user listing using keystone: {}".format(e)) raise AuthconnOperationException("Error during user listing using Keystone: {}".format(e)) - def get_role_list(self): + def get_role_list(self, filter_q=None): """ Get role list. + :param filter_q: dictionary to filter role list by _id and/or name. :return: returns the list of roles. """ try: - roles_list = self.keystone.roles.list() + filter_name = None + if filter_q: + filter_name = filter_q.get("name") + roles_list = self.keystone.roles.list(name=filter_name) roles = [{ "name": role.name, "_id": role.id } for role in roles_list if role.name != "service"] + if filter_q and filter_q.get("_id"): + roles = [role for role in roles if filter_q["_id"] == role["_id"]] + return roles except ClientException as e: self.logger.exception("Error during user role listing using keystone: {}".format(e)) @@ -393,9 +401,9 @@ class AuthconnKeystone(Authconn): """ try: result = self.keystone.roles.create(role) - return {"name": result.name, "_id": result.id} + return result.id except Conflict as ex: - self.logger.info("Duplicate entry: %s", str(ex)) + raise AuthconnConflictException(str(ex)) except ClientException as e: self.logger.exception("Error during role creation using keystone: {}".format(e)) raise AuthconnOperationException("Error during role creation using Keystone: {}".format(e)) @@ -408,9 +416,7 @@ class AuthconnKeystone(Authconn): :raises AuthconnOperationException: if role deletion failed. """ try: - roles = self.keystone.roles.list() - role_obj = [role for role in roles if role.id == role_id][0] - result, detail = self.keystone.roles.delete(role_obj) + result, detail = self.keystone.roles.delete(role_id) if result.status_code != 204: raise ClientException("error {} {}".format(result.status_code, detail)) @@ -420,6 +426,26 @@ class AuthconnKeystone(Authconn): self.logger.exception("Error during role deletion using keystone: {}".format(e)) raise AuthconnOperationException("Error during role deletion using Keystone: {}".format(e)) + def update_role(self, role, new_name): + """ + Change the name of a role + :param role: role name or id to be changed + :param new_name: new name + :return: None + """ + try: + if is_valid_uuid(role): + role_id = role + else: + role_obj_list = self.keystone.roles.list(name=role) + if not role_obj_list: + raise AuthconnNotFoundException("Role '{}' not found".format(role)) + role_id = role_obj_list[0].id + self.keystone.roles.update(role_id, name=new_name) + except ClientException as e: + # self.logger.exception("Error during role update using keystone: {}".format(e)) + raise AuthconnOperationException("Error during role updating using Keystone: {}".format(e)) + def get_project_list(self, filter_q=None): """ Get all the projects. diff --git a/osm_nbi/base_topic.py b/osm_nbi/base_topic.py index 7c6c990..9a48791 100644 --- a/osm_nbi/base_topic.py +++ b/osm_nbi/base_topic.py @@ -192,7 +192,10 @@ class BaseTopic: :param _id: If not None, ignore this entry that are going to change :return: None or raises EngineException """ - _filter = self._get_project_filter(session) + if not self.multiproject: + _filter = {} + else: + _filter = self._get_project_filter(session) _filter["name"] = name if _id: _filter["_id.neq"] = _id @@ -288,7 +291,10 @@ class BaseTopic: :param _id: server internal id :return: dictionary, raise exception if not found. """ - filter_db = self._get_project_filter(session) + if not self.multiproject: + filter_db = {} + else: + filter_db = self._get_project_filter(session) # To allow project&user addressing by name AS WELL AS _id filter_db[BaseTopic.id_field(self.topic, _id)] = _id return self.db.get_one(self.topic, filter_db) @@ -315,8 +321,8 @@ class BaseTopic: """ if not filter_q: filter_q = {} - - filter_q.update(self._get_project_filter(session)) + if self.multiproject: + filter_q.update(self._get_project_filter(session)) # TODO transform data for SOL005 URL requests. Transform filtering # TODO implement "field-type" query string SOL005 @@ -371,7 +377,8 @@ class BaseTopic: # TODO add admin to filter, validate rights if not filter_q: filter_q = {} - filter_q.update(self._get_project_filter(session)) + if self.multiproject: + filter_q.update(self._get_project_filter(session)) return self.db.del_list(self.topic, filter_q) def delete_extra(self, session, _id, db_content): @@ -405,7 +412,8 @@ class BaseTopic: if dry_run: return None - filter_q.update(self._get_project_filter(session)) + if self.multiproject: + filter_q.update(self._get_project_filter(session)) if self.multiproject and session["project_id"]: # remove reference from project_read. If not last delete self.db.set_one(self.topic, filter_q, update_dict=None, diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index d8409ea..d796a93 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -58,7 +58,8 @@ class Engine(object): map_target_version_to_int = { "1.0": 1000, - "1.1": 1001 + "1.1": 1001, + "1.2": 1002, # Add new versions here } @@ -328,8 +329,7 @@ class Engine(object): def upgrade_db(self, current_version, target_version): if target_version not in self.map_target_version_to_int.keys(): - raise EngineException("Wrong database version '{}'. Expected '{}'" - ". It cannot be up/down-grade".format(current_version, target_version), + raise EngineException("Cannot upgrade to version '{}' with this version of code".format(target_version), http_code=HTTPStatus.INTERNAL_SERVER_ERROR) if current_version == target_version: @@ -353,19 +353,19 @@ class Engine(object): self.db.set_secret_key(serial) current_version = "1.0" - if current_version == "1.0" and target_version_int >= self.map_target_version_to_int["1.1"]: + 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") version_data = { "_id": "version", - "version_int": 1001, - "version": "1.1", - "date": "2019-05-24", + "version_int": 1002, + "version": "1.2", + "date": "2019-06-11", "description": "set new format for roles_operations" } self.db.set_one("admin", {"_id": "version"}, version_data) - current_version = "1.1" + current_version = "1.2" # TODO add future migrations here def init_db(self, target_version='1.0'): diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 269af33..9d22df7 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -40,7 +40,7 @@ __author__ = "Alfonso Tierno " __version__ = "0.1.3" version_date = "Jan 2019" -database_version = '1.1' +database_version = '1.2' auth_database_version = '1.0' nbi_server = None # instance of Server class subscription_thread = None # instance of SubscriptionThread class diff --git a/osm_nbi/roles_to_operations.yml b/osm_nbi/roles_to_operations.yml index bfac28f..db79876 100644 --- a/osm_nbi/roles_to_operations.yml +++ b/osm_nbi/roles_to_operations.yml @@ -17,50 +17,50 @@ ## --- -roles_to_operations: +roles: ## -# This file defines the mapping between user roles and operation permission. +# This file defines the mapping between user roles and operation permissions. # It uses the following pattern: # -# - role: -# operations: +# - name: +# permissions: # "": true | false # # defines the name of the role. This name will be matched with an -# existing role in the RBAC system. +# existing role in the RBAC system (e.g. keystone). # # NOTE: The role will only be used if there is an existing match. If there # isn't a role in the system that can be matched, the operation permissions # won't yield any result. # -# operations: is a list of operation permissions for the role. An operation +# permissions: is a dictionary of operation permissions for the role. An operation # permission is defined using the following pattern: # # "": true | false # # The operations are defined using an hierarchical tree. For this purpose, an # tag can represents the path for the following: -# - Root -# - Node -# - Leaf +# - default: what action to be taken by default, allow or deny +# - admin: allow or deny usin querey string ADMIN to act on behalf of other project +# - colon separated hierarchical tree # -# The root tag is defined using "." and the default value is false. +# The default and admin tag is considered false if missing. # When you use this tag, all the operation permissions will be set to the value # assigned. # NOTE 1: The default value is false. So if a value isn't specified, it will # default to false. -# NOTE 2: The root tag can be overridden by using more specific tags +# NOTE 2: The default tag can be overridden by using more specific tags # with a different value. # # The node tag is defined by using an internal node of the tree, i.e. -# "nsds", "users.id". A node tag will affect all the nodes and leafs -# beneath it. It can be used to override a root tag. +# "nsds", "users:id". A node tag will affect all the nodes and leafs +# beneath it. It can be used to override a default tag. # NOTE 1: It can be overridden by using a more specific tag, such as a node which # is beneath it or a leaf. # -# The leaf tag is defined by using a leaf of the tree, i.e. "users.post", -# "ns_instances.get", "vim_accounts.id.get". A leaf tag will override all +# The leaf tag is defined by using a leaf of the tree, i.e. "users:post", +# "ns_instances:get", "vim_accounts:id:get". A leaf tag will override all # the values defined by the parent nodes, since it is the more specific tag that can # exist. # @@ -68,70 +68,73 @@ roles_to_operations: # - In order to find which tags are in use, check the resources_to_operations.yml. # - In order to find which roles are in use, check the RBAC system. # - Non existing tags will be ignored. -# - Tags finishing in a dot (excluding the root tag) will be ignored. +# - Tags finishing in a colon will be ignored. # - The anonymous role allows to bypass the role definition for paths that # shouldn't be verified. ## - - role: "system_admin" - operations: - ":": true + - name: "system_admin" + permissions: + default: true + admin: true - - role: "account_manager" - operations: - ":": false - "tokens": true - "users": true - "projects": true - "roles": true + - name: "account_manager" + permissions: + default: false + admin: false + tokens: true + users: true + projects: true + roles: true - - role: "project_admin" - operations: - ":": true - # Users - "users:post": false - "users:id:post": false - "users:id:delete": false - # Projects - "projects": false - # Roles - "roles": false + - name: "project_admin" + permissions: + default: true + # Users + users:post: false + users:id:post: false + users:id:delete: false + users:id:put: false + # Projects + projects: false + # Roles + roles: false - - role: "project_user" - operations: - ":": true - # NS Instances - "ns_instances": false - "ns_instances:get": true - # VNF Instances - "vnf_instances": false - # Users - "users": false - "users:id:get": true - "users:id:put": true - "users:id:patch": true - # Projects - "projects": false - # VIMs - "vims": false - "vims:get": true - "vims:id:get": true - # VIM Accounts - "vim_accounts": false - "vim_accounts:get": true - "vim_accounts:id:get": true - # SDN Controllers - "sdn_controllers": false - "sdn_controllers:get": true - "sdn_controllers:id:get": true - # WIMs - "wims": false - "wims:get": true - "wims:id:get": true - # WIM Accounts - "wim_accounts": false - "wim_accounts:get": true - "wim_accounts:id:get": true + - name: "project_user" + permissions: + default: true + # NS Instances + ns_instances: false + ns_instances:get: true + # VNF Instances + vnf_instances: false + # Users + users: false + users:id:get: true + users:id:put: true + users:id:patch: true + # Projects + projects: false + # VIMs + vims: false + vims:get: true + vims:id:get: true + # VIM Accounts + vim_accounts: false + vim_accounts:get: true + vim_accounts:id:get: true + # SDN Controllers + sdn_controllers: false + sdn_controllers:get: true + sdn_controllers:id:get: true + # WIMs + wims: false + wims:get: true + wims:id:get: true + # WIM Accounts + wim_accounts: false + wim_accounts:get: true + wim_accounts:id:get: true - - role: "anonymous" - operations: + - name: "anonymous" + permissions: diff --git a/osm_nbi/validation.py b/osm_nbi/validation.py index a081ffa..b6ef64c 100644 --- a/osm_nbi/validation.py +++ b/osm_nbi/validation.py @@ -661,20 +661,34 @@ roles_new_schema = { "type": "object", "properties": { "name": shortname_schema, - "root": bool_schema, + "permissions": { + "type": "object", + "patternProperties": { + ".": bool_schema, + }, + # "minProperties": 1, + } }, - "required": ["name", "root"], - "additionalProperties": True + "required": ["name"], + "additionalProperties": False } roles_edit_schema = { "$schema": "http://json-schema.org/draft-04/schema#", "title": "Roles edit schema for administrators", "type": "object", "properties": { - "root": bool_schema, + "name": shortname_schema, + "permissions": { + "type": "object", + "patternProperties": { + ".": { + "oneOf": [bool_schema, null_schema] + } + }, + # "minProperties": 1, + } }, - "required": ["root"], - "additionalProperties": True, + "additionalProperties": False, "minProperties": 1 }