bug 767: uniform role format 66/7666/2
authortierno <alfonso.tiernosepulveda@telefonica.com>
Thu, 13 Jun 2019 22:37:04 +0000 (22:37 +0000)
committertierno <alfonso.tiernosepulveda@telefonica.com>
Fri, 14 Jun 2019 09:52:00 +0000 (09:52 +0000)
Change-Id: I5ca0982c20ca018f787071ea9f82857afcc24e5e
Signed-off-by: tierno <alfonso.tiernosepulveda@telefonica.com>
osm_nbi/admin_topics.py
osm_nbi/auth.py
osm_nbi/authconn.py
osm_nbi/authconn_keystone.py
osm_nbi/base_topic.py
osm_nbi/engine.py
osm_nbi/nbi.py
osm_nbi/roles_to_operations.yml
osm_nbi/validation.py

index cbe2f4c..071ed3b 100644 (file)
@@ -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"))
index f7e4844..e9a6b4e 100644 (file)
@@ -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
index 42707fe..b408052 100644 (file)
@@ -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.
index bb69381..23b07e3 100644 (file)
@@ -28,7 +28,8 @@ it for OSM.
 __author__ = "Eduardo Sousa <esousa@whitestack.com>"
 __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.
index 7c6c990..9a48791 100644 (file)
@@ -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,
index d8409ea..d796a93 100644 (file)
@@ -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'):
index 269af33..9d22df7 100644 (file)
@@ -40,7 +40,7 @@ __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 
 __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
index bfac28f..db79876 100644 (file)
 ##
 
 ---
-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: <ROLE_NAME>
-#      operations:
+#    - name: <ROLE_NAME>
+#      permissions:
 #        "<OPERATION>": true | false
 #
 # <ROLE_NAME> 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:
 #
 #    "<OPERATION>": true | false
 #
 # The operations are defined using an hierarchical tree. For this purpose, an
 # <OPERATION> 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 <OPERATION> tag is defined using "." and the default value is false.
+# The default and admin <OPERATION> 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 <OPERATION> tag can be overridden by using more specific tags
+# NOTE 2: The default <OPERATION> tag can be overridden by using more specific tags
 #         with a different value.
 #
 # The node <OPERATION> tag is defined by using an internal node of the tree, i.e.
-# "nsds", "users.id". A node <OPERATION> tag will affect all the nodes and leafs
-# beneath it. It can be used to override a root <OPERATION> tag.
+# "nsds", "users:id". A node <OPERATION> tag will affect all the nodes and leafs
+# beneath it. It can be used to override a default <OPERATION> 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 <OPERATION> tag is defined by using a leaf of the tree, i.e. "users.post",
-# "ns_instances.get", "vim_accounts.id.get". A leaf <OPERATION> tag will override all
+# The leaf <OPERATION> tag is defined by using a leaf of the tree, i.e. "users:post",
+# "ns_instances:get", "vim_accounts:id:get". A leaf <OPERATION> 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 <OPERATION> 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:
index a081ffa..b6ef64c 100644 (file)
@@ -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
 }