Adding User, Projects and Roles (Keystone) to NBI API 80/7380/20
authorEduardo Sousa <eduardo.sousa@canonical.com>
Wed, 8 May 2019 01:35:47 +0000 (02:35 +0100)
committerEduardo Sousa <eduardo.sousa@canonical.com>
Fri, 10 May 2019 01:10:08 +0000 (02:10 +0100)
Change-Id: Id8c65e5d076fefc329340ca195c268004ecb4a4e
Signed-off-by: Eduardo Sousa <eduardo.sousa@canonical.com>
.gitignore-common
osm_nbi/admin_topics.py
osm_nbi/auth.py
osm_nbi/authconn.py
osm_nbi/authconn_keystone.py
osm_nbi/engine.py
osm_nbi/nbi.py
osm_nbi/validation.py

index bfe5786..157e9ef 100644 (file)
@@ -46,3 +46,6 @@ build
 dist
 *.egg-info
 .eggs
 dist
 *.egg-info
 .eggs
+
+#vscode
+.vscode
\ No newline at end of file
index afa50d8..ea79cd4 100644 (file)
 from uuid import uuid4
 from hashlib import sha256
 from http import HTTPStatus
 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 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
+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 base_topic import BaseTopic, EngineException
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 from base_topic import BaseTopic, EngineException
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
@@ -313,3 +316,660 @@ class SdnTopic(BaseTopic):
             self.db.set_one("sdns", {"_id": _id}, {"_admin.to_delete": True})  # TODO change status
             self._send_msg("delete", {"_id": _id})
             return v   # TODO indicate an offline operation to return 202 ACCEPTED
             self.db.set_one("sdns", {"_id": _id}, {"_admin.to_delete": True})  # TODO change status
             self._send_msg("delete", {"_id": _id})
             return v   # TODO indicate an offline operation to return 202 ACCEPTED
+
+
+class UserTopicAuth(UserTopic):
+    topic = "users"
+    topic_msg = "users"
+    schema_new = user_new_schema
+    schema_edit = user_edit_schema
+
+    def __init__(self, db, fs, msg, auth):
+        UserTopic.__init__(self, db, fs, msg)
+        self.auth = auth
+
+    def check_conflict_on_new(self, session, indata, force=False):
+        """
+        Check that the data to be inserted is valid
+
+        :param session: contains "username", if user is "admin" and the working "project_id"
+        :param indata: data to be inserted
+        :param force: boolean. With force it is more tolerant
+        :return: None or raises EngineException
+        """
+        username = indata.get("username")
+        user_list = list(map(lambda x: x["username"], self.auth.get_user_list()))
+
+        if username in user_list:
+            raise EngineException("username '{}' exists".format(username), HTTPStatus.CONFLICT)
+
+    def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False):
+        """
+        Check that the data to be edited/uploaded is valid
+
+        :param session: contains "username", if user is "admin" and the working "project_id"
+        :param final_content: data once modified
+        :param edit_content: incremental data that contains the modifications to apply
+        :param _id: internal _id
+        :param force: boolean. With force it is more tolerant
+        :return: None or raises EngineException
+        """
+        users = self.auth.get_user_list()
+        admin_user = [user for user in users if user["name"] == "admin"][0]
+
+        if _id == admin_user["_id"] and edit_content["project_role_mappings"]:
+            elem = {
+                "project": "admin",
+                "role": "system_admin"
+            }
+            if elem not in edit_content:
+                raise EngineException("You cannot remove system_admin role from admin user", 
+                                      http_code=HTTPStatus.FORBIDDEN)
+
+    def check_conflict_on_del(self, session, _id, force=False):
+        """
+        Check if deletion can be done because of dependencies if it is not force. To override
+
+        :param session: contains "username", if user is "admin" and the working "project_id"
+        :param _id: internal _id
+        :param force: Avoid this checking
+        :return: None if ok or raises EngineException with the conflict
+        """
+        if _id == session["username"]:
+            raise EngineException("You cannot delete your own user", http_code=HTTPStatus.CONFLICT)
+
+    @staticmethod
+    def format_on_new(content, project_id=None, make_public=False):
+        """
+        Modifies content descriptor to include _id.
+
+        NOTE: No password salt required because the authentication backend
+        should handle these security concerns.
+
+        :param content: descriptor to be modified
+        :param make_public: if included it is generated as public for reading.
+        :return: None, but content is modified
+        """
+        BaseTopic.format_on_new(content, make_public=False)
+        content["_id"] = content["username"]
+        content["password"] = content["password"]
+
+    @staticmethod
+    def format_on_edit(final_content, edit_content):
+        """
+        Modifies final_content descriptor to include the modified date.
+
+        NOTE: No password salt required because the authentication backend
+        should handle these security concerns.
+
+        :param final_content: final descriptor generated
+        :param edit_content: alterations to be include
+        :return: None, but final_content is modified
+        """
+        BaseTopic.format_on_edit(final_content, edit_content)
+        if "password" in edit_content:
+            final_content["password"] = edit_content["password"]
+        else:
+            final_content["project_role_mappings"] = edit_content["project_role_mappings"]
+
+    def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False):
+        """
+        Creates a new entry into the authentication backend.
+
+        NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
+
+        :param rollback: list to append created items at database in case a rollback may to be done
+        :param session: contains the used login username and working project
+        :param indata: data to be inserted
+        :param kwargs: used to override the indata descriptor
+        :param headers: http request headers
+        :param force: If True avoid some dependence checks
+        :param make_public: Make the created item public to all projects
+        :return: _id: identity of the inserted data.
+        """
+        try:
+            content = BaseTopic._remove_envelop(indata)
+
+            # Override descriptor with query string kwargs
+            BaseTopic._update_input_with_kwargs(content, kwargs)
+            content = self._validate_input_new(content, force)
+            self.check_conflict_on_new(session, content, force=force)
+            self.format_on_new(content, project_id=session["project_id"], make_public=make_public)
+            _id = self.auth.create_user(content["username"], content["password"])
+            rollback.append({"topic": self.topic, "_id": _id})
+            del content["password"]
+            # self._send_msg("create", content)
+            return _id
+        except ValidationError as e:
+            raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
+
+    def show(self, session, _id):
+        """
+        Get complete information on an topic
+
+        :param session: contains the used login username and working project
+        :param _id: server internal id
+        :return: dictionary, raise exception if not found.
+        """
+        users = [user for user in self.auth.get_user_list() if user["_id"] == _id]
+
+        if len(users) == 1:
+            return users[0]
+        elif len(users) > 1:
+            raise EngineException("Too many users found", HTTPStatus.CONFLICT)
+        else:
+            raise EngineException("User not found", HTTPStatus.NOT_FOUND)
+
+    def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None):
+        """
+        Updates an user entry.
+
+        :param session: contains the used login username and working project
+        :param _id:
+        :param indata: data to be inserted
+        :param kwargs: used to override the indata descriptor
+        :param force: If True avoid some dependence checks
+        :param content:
+        :return: _id: identity of the inserted data.
+        """
+        indata = self._remove_envelop(indata)
+
+        # Override descriptor with query string kwargs
+        if kwargs:
+            BaseTopic._update_input_with_kwargs(indata, kwargs)
+        try:
+            indata = self._validate_input_edit(indata, force=force)
+
+            if not content:
+                content = self.show(session, _id)
+            self.check_conflict_on_edit(session, content, indata, _id=_id, force=force)
+            self.format_on_edit(content, indata)
+
+            if "password" in content:
+                self.auth.change_password(content["name"], content["password"])
+            else:
+                users = self.auth.get_user_list()
+                user = [user for user in users if user["_id"] == content["_id"]][0]
+                original_mapping = []
+                edit_mapping = content["project_role_mappings"]
+
+                for project in user["projects"]:
+                    for role in project["roles"]:
+                        original_mapping += {
+                            "project": project["name"],
+                            "role": role["name"]
+                        }
+                
+                mappings_to_remove = [mapping for mapping in original_mapping 
+                                      if mapping not in edit_mapping]
+                
+                mappings_to_add = [mapping for mapping in edit_mapping
+                                   if mapping not in original_mapping]
+                
+                for mapping in mappings_to_remove:
+                    self.auth.remove_role_from_user(
+                        user["name"], 
+                        mapping["project"],
+                        mapping["role"]
+                    )
+                
+                for mapping in mappings_to_add:
+                    self.auth.assign_role_to_user(
+                        user["name"], 
+                        mapping["project"],
+                        mapping["role"]
+                    )
+
+            return content["_id"]
+        except ValidationError as e:
+            raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
+
+    def list(self, session, filter_q=None):
+        """
+        Get a list of the topic that matches a filter
+        :param session: contains the used login username and working project
+        :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_user_list()
+
+    def delete(self, session, _id, force=False, dry_run=False):
+        """
+        Delete item by its internal _id
+
+        :param session: contains the used login username, working project, and admin rights
+        :param _id: server internal id
+        :param force: indicates if deletion must be forced in case of conflict
+        :param dry_run: make checking but do not delete
+        :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
+        """
+        self.check_conflict_on_del(session, _id, force)
+        if not dry_run:
+            v = self.auth.delete_user(_id)
+            return v
+        return None
+
+
+class ProjectTopicAuth(ProjectTopic):
+    topic = "projects"
+    topic_msg = "projects"
+    schema_new = project_new_schema
+    schema_edit = project_edit_schema
+
+    def __init__(self, db, fs, msg, auth):
+        ProjectTopic.__init__(self, db, fs, msg)
+        self.auth = auth
+
+    def check_conflict_on_new(self, session, indata, force=False):
+        """
+        Check that the data to be inserted is valid
+
+        :param session: contains "username", if user is "admin" and the working "project_id"
+        :param indata: data to be inserted
+        :param force: boolean. With force it is more tolerant
+        :return: None or raises EngineException
+        """
+        project = indata.get("name")
+        project_list = list(map(lambda x: x["name"], self.auth.get_project_list()))
+
+        if project in project_list:
+            raise EngineException("project '{}' exists".format(project), HTTPStatus.CONFLICT)
+
+    def check_conflict_on_del(self, session, _id, force=False):
+        """
+        Check if deletion can be done because of dependencies if it is not force. To override
+
+        :param session: contains "username", if user is "admin" and the working "project_id"
+        :param _id: internal _id
+        :param force: Avoid this checking
+        :return: None if ok or raises EngineException with the conflict
+        """
+        projects = self.auth.get_project_list()
+        current_project = [project for project in projects
+                           if project["name"] == session["project_id"]][0]
+
+        if _id == current_project["_id"]:
+            raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
+
+    def new(self, rollback, session, indata=None, kwargs=None, headers=None, force=False, make_public=False):
+        """
+        Creates a new entry into the authentication backend.
+
+        NOTE: Overrides BaseTopic functionality because it doesn't require access to database.
+
+        :param rollback: list to append created items at database in case a rollback may to be done
+        :param session: contains the used login username and working project
+        :param indata: data to be inserted
+        :param kwargs: used to override the indata descriptor
+        :param headers: http request headers
+        :param force: If True avoid some dependence checks
+        :param make_public: Make the created item public to all projects
+        :return: _id: identity of the inserted data.
+        """
+        try:
+            content = BaseTopic._remove_envelop(indata)
+
+            # Override descriptor with query string kwargs
+            BaseTopic._update_input_with_kwargs(content, kwargs)
+            content = self._validate_input_new(content, force)
+            self.check_conflict_on_new(session, content, force=force)
+            self.format_on_new(content, project_id=session["project_id"], make_public=make_public)
+            _id = self.auth.create_project(content["name"])
+            rollback.append({"topic": self.topic, "_id": _id})
+            # self._send_msg("create", content)
+            return _id
+        except ValidationError as e:
+            raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
+
+    def show(self, session, _id):
+        """
+        Get complete information on an topic
+
+        :param session: contains the used login username and working project
+        :param _id: server internal id
+        :return: dictionary, raise exception if not found.
+        """
+        projects = [project for project in self.auth.get_project_list() if project["_id"] == _id]
+
+        if len(projects) == 1:
+            return projects[0]
+        elif len(projects) > 1:
+            raise EngineException("Too many projects found", HTTPStatus.CONFLICT)
+        else:
+            raise EngineException("Project not found", HTTPStatus.NOT_FOUND)
+
+    def list(self, session, filter_q=None):
+        """
+        Get a list of the topic that matches a filter
+
+        :param session: contains the used login username and working project
+        :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()
+
+    def delete(self, session, _id, force=False, dry_run=False):
+        """
+        Delete item by its internal _id
+
+        :param session: contains the used login username, working project, and admin rights
+        :param _id: server internal id
+        :param force: indicates if deletion must be forced in case of conflict
+        :param dry_run: make checking but do not delete
+        :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
+        """
+        self.check_conflict_on_del(session, _id, force)
+        if not dry_run:
+            v = self.auth.delete_project(_id)
+            return v
+        return None
+
+
+class RoleTopicAuth(BaseTopic):
+    topic = "roles_operations"
+    topic_msg = "roles"
+    schema_new = roles_new_schema
+    schema_edit = roles_edit_schema
+
+    def __init__(self, db, fs, msg, auth, ops):
+        BaseTopic.__init__(self, db, fs, msg)
+        self.auth = auth
+        self.operations = ops
+
+    @staticmethod
+    def validate_role_definition(operations, role_definitions):
+        """
+        Validates the role definition against the operations defined in
+        the resources to operations files.
+
+        :param operations: operations list
+        :param role_definitions: role definition to test
+        :return: None if ok, raises ValidationError exception on error
+        """
+        for role_def in role_definitions.keys():
+            if role_def == ".":
+                continue
+            if role_def[-1] == ".":
+                raise ValidationError("Operation cannot end with \".\"")
+            
+            role_def_matches = [op for op in operations if op.starswith(role_def)]
+
+            if len(role_def_matches) == 0:
+                raise ValidationError("No matching operation found.")
+
+    def _validate_input_new(self, input, force=False):
+        """
+        Validates input user content for a new entry.
+
+        :param input: user input content for the new topic
+        :param force: may be used for being more tolerant
+        :return: The same input content, or a changed version of it.
+        """
+        if self.schema_new:
+            validate_input(input, self.schema_new)
+        if "definition" in input and input["definition"]:
+            self.validate_role_definition(self.operations, input["definition"])
+        return input
+
+    def _validate_input_edit(self, input, force=False):
+        """
+        Validates input user content for updating an entry.
+
+        :param input: user input content for the new topic
+        :param force: may be used for being more tolerant
+        :return: The same input content, or a changed version of it.
+        """
+        if self.schema_edit:
+            validate_input(input, self.schema_edit)
+        if "definition" in input and input["definition"]:
+            self.validate_role_definition(self.operations, input["definition"])
+        return input
+
+    def check_conflict_on_new(self, session, indata, force=False):
+        """
+        Check that the data to be inserted is valid
+
+        :param session: contains "username", if user is "admin" and the working "project_id"
+        :param indata: data to be inserted
+        :param force: boolean. With force it is more tolerant
+        :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)
+
+    def check_conflict_on_edit(self, session, final_content, edit_content, _id, force=False):
+        """
+        Check that the data to be edited/uploaded is valid
+
+        :param session: contains "username", if user is "admin" and the working "project_id"
+        :param final_content: data once modified
+        :param edit_content: incremental data that contains the modifications to apply
+        :param _id: internal _id
+        :param force: boolean. With force it is more tolerant
+        :return: None or raises EngineException
+        """
+        roles = self.auth.get_role_list()
+        system_admin_role = [role for role in roles
+                             if roles["name"] == "system_admin"][0]
+
+        if _id == system_admin_role["_id"]:
+            raise EngineException("You cannot edit system_admin role", http_code=HTTPStatus.FORBIDDEN)
+
+    def check_conflict_on_del(self, session, _id, force=False):
+        """
+        Check if deletion can be done because of dependencies if it is not force. To override
+
+        :param session: contains "username", if user is "admin" and the working "project_id"
+        :param _id: internal _id
+        :param force: Avoid this checking
+        :return: None if ok or raises EngineException with the conflict
+        """
+        roles = self.auth.get_role_list()
+        system_admin_role = [role for role in roles
+                             if roles["name"] == "system_admin"][0]
+
+        if _id == system_admin_role["_id"]:
+            raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
+
+    @staticmethod
+    def format_on_new(content, project_id=None, make_public=False):
+        """
+        Modifies content descriptor to include _admin
+
+        :param content: descriptor to be modified
+        :param project_id: if included, it add project read/write permissions
+        :param make_public: if included it is generated as public for reading.
+        :return: None, but content is modified
+        """
+        now = time()
+        if "_admin" not in content:
+            content["_admin"] = {}
+        if not content["_admin"].get("created"):
+            content["_admin"]["created"] = now
+        content["_admin"]["modified"] = now
+        content["root"] = False
+
+        # Saving the role definition
+        if "definition" in content and content["definition"]:
+            for role_def, value in content["definition"].items():
+                if role_def == ".":
+                    content["root"] = value
+                else:
+                    content[role_def.replace(".", ":")] = value
+
+        # Cleaning undesired values
+        if "definition" in content:
+            del content["definition"]
+
+    @staticmethod
+    def format_on_edit(final_content, edit_content):
+        """
+        Modifies final_content descriptor to include the modified date.
+
+        :param final_content: final descriptor generated
+        :param edit_content: alterations to be include
+        :return: None, but final_content is modified
+        """
+        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
+        if "definition" in edit_content and edit_content["definition"]:
+            for role_def, value in edit_content["definition"].items():
+                if role_def == ".":
+                    final_content["root"] = value
+                else:
+                    final_content[role_def.replace(".", ":")] = value
+
+        if "root" not in final_content:
+            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
+        """
+        ignore_fields = ["_admin", "_id", "name", "root"]
+        content_keys = list(content.keys())
+        definition = dict(content)
+        
+        for key in content_keys:
+            if key in ignore_fields:
+                del definition[key]
+            if ":" not in key:
+                del content[key]
+                continue
+            definition[key.replace(":", ".")] = definition[key]
+            del definition[key]
+            del content[key]
+        
+        content["definition"] = definition
+
+    def show(self, session, _id):
+        """
+        Get complete information on an topic
+
+        :param session: contains the used login username and working project
+        :param _id: server internal id
+        :return: dictionary, raise exception if not found.
+        """
+        filter_db = self._get_project_filter(session, write=False, show_all=True)
+        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 the used login username and working 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 = {}
+
+        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, force=False, make_public=False):
+        """
+        Creates a new entry into database.
+
+        :param rollback: list to append created items at database in case a rollback may to be done
+        :param session: contains the used login username and working project
+        :param indata: data to be inserted
+        :param kwargs: used to override the indata descriptor
+        :param headers: http request headers
+        :param force: If True avoid some dependence checks
+        :param make_public: Make the created item public to all projects
+        :return: _id: identity of the inserted data.
+        """
+        try:
+            content = BaseTopic._remove_envelop(indata)
+
+            # Override descriptor with query string kwargs
+            BaseTopic._update_input_with_kwargs(content, kwargs)
+            content = self._validate_input_new(content, force)
+            self.check_conflict_on_new(session, content, force=force)
+            self.format_on_new(content, project_id=session["project_id"], make_public=make_public)
+            role_name = content["name"]
+            role = 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)
+            return _id
+        except ValidationError as e:
+            raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
+
+    def delete(self, session, _id, force=False, dry_run=False):
+        """
+        Delete item by its internal _id
+
+        :param session: contains the used login username, working project, and admin rights
+        :param _id: server internal id
+        :param force: indicates if deletion must be forced in case of conflict
+        :param dry_run: make checking but do not delete
+        :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
+        """
+        self.check_conflict_on_del(session, _id, force)
+        filter_q = self._get_project_filter(session, write=True, show_all=True)
+        filter_q["_id"] = _id
+        if not dry_run:
+            self.auth.delete_role(_id)
+            v = self.db.del_one(self.topic, filter_q)
+            return v
+        return None
+
+    def edit(self, session, _id, indata=None, kwargs=None, force=False, content=None):
+        """
+        Updates a role entry.
+
+        :param session: contains the used login username and working project
+        :param _id:
+        :param indata: data to be inserted
+        :param kwargs: used to override the indata descriptor
+        :param force: If True avoid some dependence checks
+        :param content:
+        :return: _id: identity of the inserted data.
+        """
+        indata = self._remove_envelop(indata)
+
+        # Override descriptor with query string kwargs
+        if kwargs:
+            BaseTopic._update_input_with_kwargs(indata, kwargs)
+        try:
+            indata = self._validate_input_edit(indata, force=force)
+
+            if not content:
+                content = self.show(session, _id)
+            self.check_conflict_on_edit(session, content, indata, _id=_id, force=force)
+            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)
index 18986a3..751dd90 100644 (file)
@@ -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
 """
 create the tokens unscoped and scoped, retrieve the role
 list inside the projects that they are inserted
 """
-from os import path
 
 __author__ = "Eduardo Sousa <esousa@whitestack.com>; Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 __date__ = "$27-jul-2018 23:59:59$"
 
 __author__ = "Eduardo Sousa <esousa@whitestack.com>; Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 __date__ = "$27-jul-2018 23:59:59$"
@@ -40,7 +39,7 @@ from hashlib import sha256
 from http import HTTPStatus
 from random import choice as random_choice
 from time import time
 from http import HTTPStatus
 from random import choice as random_choice
 from time import time
-from uuid import uuid4
+from os import path
 
 from authconn import AuthException
 from authconn_keystone import AuthconnKeystone
 
 from authconn import AuthException
 from authconn_keystone import AuthconnKeystone
@@ -108,8 +107,11 @@ class Authenticator:
                 if "resources_to_operations" in config["rbac"]:
                     self.resources_to_operations_file = config["rbac"]["resources_to_operations"]
                 else:
                 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
                         if path.isfile(config_file):
                             self.resources_to_operations_file = config_file
                             break
@@ -119,8 +121,11 @@ class Authenticator:
                 if "roles_to_operations" in config["rbac"]:
                     self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
                 else:
                 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
                         if path.isfile(config_file):
                             self.roles_to_operations_file = config_file
                             break
@@ -147,6 +152,9 @@ class Authenticator:
         # Always reads operation to resource mapping from file (this is static, no need to store it in MongoDB)
         # Operations encoding: "<METHOD> <URL>"
         # Note: it is faster to rewrite the value than to check if it is already there or not
         # Always reads operation to resource mapping from file (this is static, no need to store it in MongoDB)
         # Operations encoding: "<METHOD> <URL>"
         # 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)
         operations = []
         with open(self.resources_to_operations_file, "r") as stream:
             resources_to_operations_yaml = yaml.load(stream)
@@ -208,28 +216,29 @@ class Authenticator:
 
                 now = time()
                 operation_to_roles_item = {
 
                 now = time()
                 operation_to_roles_item = {
-                    "_id": str(uuid4()),
                     "_admin": {
                         "created": now,
                         "modified": now,
                     },
                     "_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
 
                     "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 = 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")
 
                 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:
         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(":"))
             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 +252,12 @@ class Authenticator:
             allowed_operations = [k for k, v in record_permissions.items() if v is True]
 
             for allowed_op in allowed_operations:
             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 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):
             self.backend.assign_role_to_user("admin", "admin", "system_admin")
 
     def authorize(self):
@@ -402,7 +406,7 @@ class Authenticator:
 
         operation = self.resources_to_operations_mapping[key]
         roles_required = self.operation_to_allowed_roles[operation]
 
         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
 
         if "anonymous" in roles_required:
             return
@@ -413,6 +417,9 @@ class Authenticator:
 
         raise AuthException("Access denied: lack of permissions.")
 
 
         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("?")]
     def _normalize_url(self, url, method):
         # Removing query strings
         normalized_url = url if '?' not in url else url[:url.find("?")]
index 90f0096..0ffaad8 100644 (file)
@@ -34,8 +34,8 @@ class AuthException(Exception):
     Authentication error.
     """
     def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
     Authentication error.
     """
     def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
+        super(AuthException, self).__init__(message)
         self.http_code = http_code
         self.http_code = http_code
-        Exception.__init__(self, message)
 
 
 class AuthconnException(Exception):
 
 
 class AuthconnException(Exception):
@@ -43,7 +43,7 @@ class AuthconnException(Exception):
     Common and base class Exception for all authconn exceptions.
     """
     def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
     Common and base class Exception for all authconn exceptions.
     """
     def __init__(self, message, http_code=HTTPStatus.UNAUTHORIZED):
-        Exception.__init__(message)
+        super(AuthconnException, self).__init__(message)
         self.http_code = http_code
 
 
         self.http_code = http_code
 
 
@@ -52,7 +52,7 @@ class AuthconnConnectionException(AuthconnException):
     Connectivity error with Auth backend.
     """
     def __init__(self, message, http_code=HTTPStatus.BAD_GATEWAY):
     Connectivity error with Auth backend.
     """
     def __init__(self, message, http_code=HTTPStatus.BAD_GATEWAY):
-        AuthconnException.__init__(self, message, http_code)
+        super(AuthconnConnectionException, self).__init__(message, http_code)
 
 
 class AuthconnNotSupportedException(AuthconnException):
 
 
 class AuthconnNotSupportedException(AuthconnException):
@@ -60,7 +60,7 @@ class AuthconnNotSupportedException(AuthconnException):
     The request is not supported by the Auth backend.
     """
     def __init__(self, message, http_code=HTTPStatus.NOT_IMPLEMENTED):
     The request is not supported by the Auth backend.
     """
     def __init__(self, message, http_code=HTTPStatus.NOT_IMPLEMENTED):
-        AuthconnException.__init__(self, message, http_code)
+        super(AuthconnNotSupportedException, self).__init__(message, http_code)
 
 
 class AuthconnNotImplementedException(AuthconnException):
 
 
 class AuthconnNotImplementedException(AuthconnException):
@@ -68,7 +68,7 @@ class AuthconnNotImplementedException(AuthconnException):
     The method is not implemented by the Auth backend.
     """
     def __init__(self, message, http_code=HTTPStatus.NOT_IMPLEMENTED):
     The method is not implemented by the Auth backend.
     """
     def __init__(self, message, http_code=HTTPStatus.NOT_IMPLEMENTED):
-        AuthconnException.__init__(self, message, http_code)
+        super(AuthconnNotImplementedException, self).__init__(message, http_code)
 
 
 class AuthconnOperationException(AuthconnException):
 
 
 class AuthconnOperationException(AuthconnException):
@@ -76,7 +76,7 @@ class AuthconnOperationException(AuthconnException):
     The operation executed failed.
     """
     def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR):
     The operation executed failed.
     """
     def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR):
-        AuthconnException.__init__(self, message, http_code)
+        super(AuthconnOperationException, self).__init__(message, http_code)
 
 
 class Authconn:
 
 
 class Authconn:
@@ -136,7 +136,7 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def get_project_list(self, token):
+    def get_user_project_list(self, token):
         """
         Get all the projects associated with a user.
 
         """
         Get all the projects associated with a user.
 
@@ -145,7 +145,7 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def get_role_list(self, token):
+    def get_user_role_list(self, token):
         """
         Get role list for a scoped project.
 
         """
         Get role list for a scoped project.
 
@@ -175,15 +175,22 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def delete_user(self, user):
+    def delete_user(self, user_id):
         """
         Delete user.
 
         """
         Delete user.
 
-        :param user: username.
+        :param user_id: user identifier.
         :raises AuthconnOperationException: if user deletion failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         :raises AuthconnOperationException: if user deletion failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
+    def get_user_list(self):
+        """
+        Get user list.
+
+        :return: returns a list of users.
+        """
+
     def create_role(self, role):
         """
         Create a role.
     def create_role(self, role):
         """
         Create a role.
@@ -193,15 +200,23 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def delete_role(self, role):
+    def delete_role(self, role_id):
         """
         Delete a role.
 
         """
         Delete a role.
 
-        :param role: role name.
+        :param role_id: role identifier.
         :raises AuthconnOperationException: if user deletion failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         :raises AuthconnOperationException: if user deletion failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
+    def get_role_list(self):
+        """
+        Get all the roles.
+
+        :return: list of roles
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
     def create_project(self, project):
         """
         Create a project.
     def create_project(self, project):
         """
         Create a project.
@@ -211,15 +226,23 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def delete_project(self, project):
+    def delete_project(self, project_id):
         """
         Delete a project.
 
         """
         Delete a project.
 
-        :param project: project name.
+        :param project_id: project identifier.
         :raises AuthconnOperationException: if project deletion failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         :raises AuthconnOperationException: if project deletion failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
+    def get_project_list(self):
+        """
+        Get all the projects.
+
+        :return: list of projects
+        """
+        raise AuthconnNotImplementedException("Should have implemented this")
+
     def assign_role_to_user(self, user, project, role):
         """
         Assigning a role to a user in a project.
     def assign_role_to_user(self, user, project, role):
         """
         Assigning a role to a user in a project.
index 518f47f..54442c8 100644 (file)
@@ -160,7 +160,7 @@ class AuthconnKeystone(Authconn):
             self.logger.exception("Error during token revocation using keystone")
             raise AuthException("Error during token revocation using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
 
             self.logger.exception("Error during token revocation using keystone")
             raise AuthException("Error during token revocation using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
 
-    def get_project_list(self, token):
+    def get_user_project_list(self, token):
         """
         Get all the projects associated with a user.
 
         """
         Get all the projects associated with a user.
 
@@ -177,7 +177,7 @@ class AuthconnKeystone(Authconn):
             self.logger.exception("Error during user project listing using keystone")
             raise AuthException("Error during user project listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
 
             self.logger.exception("Error during user project listing using keystone")
             raise AuthException("Error during user project listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
 
-    def get_role_list(self, token):
+    def get_user_role_list(self, token):
         """
         Get role list for a scoped project.
 
         """
         Get role list for a scoped project.
 
@@ -203,9 +203,11 @@ class AuthconnKeystone(Authconn):
         :param user: username.
         :param password: password.
         :raises AuthconnOperationException: if user creation failed.
         :param user: username.
         :param password: password.
         :raises AuthconnOperationException: if user creation failed.
+        :return: returns the id of the user in keystone.
         """
         try:
         """
         try:
-            self.keystone.users.create(user, password=password, domain=self.user_domain_name)
+            new_user = self.keystone.users.create(user, password=password, domain=self.user_domain_name)
+            return {"username": new_user.name, "_id": new_user.id}
         except ClientException:
             self.logger.exception("Error during user creation using keystone")
             raise AuthconnOperationException("Error during user creation using Keystone")
         except ClientException:
             self.logger.exception("Error during user creation using keystone")
             raise AuthconnOperationException("Error during user creation using Keystone")
@@ -225,20 +227,81 @@ class AuthconnKeystone(Authconn):
             self.logger.exception("Error during user password update using keystone")
             raise AuthconnOperationException("Error during user password update using Keystone")
 
             self.logger.exception("Error during user password update using keystone")
             raise AuthconnOperationException("Error during user password update using Keystone")
 
-    def delete_user(self, user):
+    def delete_user(self, user_id):
         """
         Delete user.
 
         """
         Delete user.
 
-        :param user: username.
+        :param user_id: user identifier.
         :raises AuthconnOperationException: if user deletion failed.
         """
         try:
         :raises AuthconnOperationException: if user deletion failed.
         """
         try:
-            user_obj = list(filter(lambda x: x.name == user, self.keystone.users.list()))[0]
-            self.keystone.users.delete(user_obj)
+            users = self.keystone.users.list()
+            user_obj = [user for user in users if user.id == user_id][0]
+            result, _ = self.keystone.users.delete(user_obj)
+
+            if result.status_code != 204:
+                raise ClientException("User was not deleted")
+
+            return True
         except ClientException:
             self.logger.exception("Error during user deletion using keystone")
             raise AuthconnOperationException("Error during user deletion using Keystone")
 
         except ClientException:
             self.logger.exception("Error during user deletion using keystone")
             raise AuthconnOperationException("Error during user deletion using Keystone")
 
+    def get_user_list(self):
+        """
+        Get user list.
+
+        :return: returns a list of users.
+        """
+        try:
+            users = self.keystone.users.list()
+            users = [{
+                "username": user.name,
+                "_id": user.id
+            } for user in users if user.name != self.admin_username]
+
+            for user in users:
+                projects = self.keystone.projects.list(user=user["_id"])
+                projects = [{
+                    "name": project.name,
+                    "_id": project.id
+                } for project in projects]
+
+                for project in projects:
+                    roles = self.keystone.roles.list(user=user["_id"], project=project["_id"])
+                    roles = [{
+                        "name": role.name,
+                        "_id": role.id
+                    } for role in roles]
+                    project["roles"] = roles
+
+                user["projects"] = projects
+
+            return users
+        except ClientException:
+            self.logger.exception("Error during user listing using keystone")
+            raise AuthconnOperationException("Error during user listing using Keystone")
+
+    def get_role_list(self):
+        """
+        Get role list.
+
+        :return: returns the list of roles for the user in that project. If
+        the token is unscoped it returns None.
+        """
+        try:
+            roles_list = self.keystone.roles.list()
+
+            roles = [{
+                "name": role.name,
+                "_id": role.id
+            } for role in roles_list if role.name != "service"]
+
+            return roles
+        except ClientException:
+            self.logger.exception("Error during user role listing using keystone")
+            raise AuthException("Error during user role listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
+
     def create_role(self, role):
         """
         Create a role.
     def create_role(self, role):
         """
         Create a role.
@@ -247,27 +310,52 @@ class AuthconnKeystone(Authconn):
         :raises AuthconnOperationException: if role creation failed.
         """
         try:
         :raises AuthconnOperationException: if role creation failed.
         """
         try:
-            self.keystone.roles.create(role)
+            result = self.keystone.roles.create(role)
+            return {"name": result.name, "_id": result.id}
         except Conflict as ex:
             self.logger.info("Duplicate entry: %s", str(ex))
         except ClientException:
             self.logger.exception("Error during role creation using keystone")
             raise AuthconnOperationException("Error during role creation using Keystone")
 
         except Conflict as ex:
             self.logger.info("Duplicate entry: %s", str(ex))
         except ClientException:
             self.logger.exception("Error during role creation using keystone")
             raise AuthconnOperationException("Error during role creation using Keystone")
 
-    def delete_role(self, role):
+    def delete_role(self, role_id):
         """
         Delete a role.
 
         """
         Delete a role.
 
-        :param role: role name.
+        :param role_id: role identifier.
         :raises AuthconnOperationException: if role deletion failed.
         """
         try:
         :raises AuthconnOperationException: if role deletion failed.
         """
         try:
-            role_obj = list(filter(lambda x: x.name == role, self.keystone.roles.list()))[0]
-            self.keystone.roles.delete(role_obj)
+            roles = self.keystone.roles.list()
+            role_obj = [role for role in roles if role.id == role_id][0]
+            result, _ = self.keystone.roles.delete(role_obj)
+
+            if result.status_code != 204:
+                raise ClientException("Role was not deleted")
+
+            return True
         except ClientException:
             self.logger.exception("Error during role deletion using keystone")
             raise AuthconnOperationException("Error during role deletion using Keystone")
 
         except ClientException:
             self.logger.exception("Error during role deletion using keystone")
             raise AuthconnOperationException("Error during role deletion using Keystone")
 
+    def get_project_list(self):
+        """
+        Get all the projects.
+
+        :return: list of projects
+        """
+        try:
+            projects = self.keystone.projects.list()
+            projects = [{
+                "name": project.name,
+                "_id": project.id
+            } for project in projects if project.name != self.admin_project]
+
+            return projects
+        except ClientException:
+            self.logger.exception("Error during user project listing using keystone")
+            raise AuthException("Error during user project listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
+
     def create_project(self, project):
         """
         Create a project.
     def create_project(self, project):
         """
         Create a project.
@@ -276,21 +364,28 @@ class AuthconnKeystone(Authconn):
         :raises AuthconnOperationException: if project creation failed.
         """
         try:
         :raises AuthconnOperationException: if project creation failed.
         """
         try:
-            self.keystone.project.create(project, self.project_domain_name)
+            result = self.keystone.projects.create(project, self.project_domain_name)
+            return {"name": result.name, "_id": result.id}
         except ClientException:
             self.logger.exception("Error during project creation using keystone")
             raise AuthconnOperationException("Error during project creation using Keystone")
 
         except ClientException:
             self.logger.exception("Error during project creation using keystone")
             raise AuthconnOperationException("Error during project creation using Keystone")
 
-    def delete_project(self, project):
+    def delete_project(self, project_id):
         """
         Delete a project.
 
         """
         Delete a project.
 
-        :param project: project name.
+        :param project_id: project identifier.
         :raises AuthconnOperationException: if project deletion failed.
         """
         try:
         :raises AuthconnOperationException: if project deletion failed.
         """
         try:
-            project_obj = list(filter(lambda x: x.name == project, self.keystone.projects.list()))[0]
-            self.keystone.project.delete(project_obj)
+            projects = self.keystone.projects.list()
+            project_obj = [project for project in projects if project.id == project_id][0]
+            result, _ = self.keystone.projects.delete(project_obj)
+
+            if result.status_code != 204:
+                raise ClientException("Project was not deleted")
+
+            return True
         except ClientException:
             self.logger.exception("Error during project deletion using keystone")
             raise AuthconnOperationException("Error during project deletion using Keystone")
         except ClientException:
             self.logger.exception("Error during project deletion using keystone")
             raise AuthconnOperationException("Error during project deletion using Keystone")
index b90f713..30af852 100644 (file)
 # limitations under the License.
 
 import logging
 # limitations under the License.
 
 import logging
+import yaml
 from osm_common import dbmongo, dbmemory, fslocal, msglocal, msgkafka, version as common_version
 from osm_common.dbbase import DbException
 from osm_common.fsbase import FsException
 from osm_common.msgbase import MsgException
 from http import HTTPStatus
 from osm_common import dbmongo, dbmemory, fslocal, msglocal, msgkafka, version as common_version
 from osm_common.dbbase import DbException
 from osm_common.fsbase import FsException
 from osm_common.msgbase import MsgException
 from http import HTTPStatus
+
+from authconn_keystone import AuthconnKeystone
 from base_topic import EngineException, versiontuple
 from admin_topics import UserTopic, ProjectTopic, VimAccountTopic, WimAccountTopic, SdnTopic
 from base_topic import EngineException, versiontuple
 from admin_topics import UserTopic, ProjectTopic, VimAccountTopic, WimAccountTopic, SdnTopic
+from admin_topics import UserTopicAuth, ProjectTopicAuth, RoleTopicAuth
 from descriptor_topics import VnfdTopic, NsdTopic, PduTopic, NstTopic
 from instance_topics import NsrTopic, VnfrTopic, NsLcmOpTopic, NsiTopic, NsiLcmOpTopic
 from base64 import b64encode
 from descriptor_topics import VnfdTopic, NsdTopic, PduTopic, NstTopic
 from instance_topics import NsrTopic, VnfrTopic, NsLcmOpTopic, NsiTopic, NsiLcmOpTopic
 from base64 import b64encode
-from os import urandom
+from os import urandom, path
 from threading import Lock
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 from threading import Lock
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
@@ -54,7 +58,9 @@ class Engine(object):
         self.db = None
         self.fs = None
         self.msg = None
         self.db = None
         self.fs = None
         self.msg = None
+        self.auth = None
         self.config = None
         self.config = None
+        self.operations = None
         self.logger = logging.getLogger("nbi.engine")
         self.map_topic = {}
         self.write_lock = None
         self.logger = logging.getLogger("nbi.engine")
         self.map_topic = {}
         self.write_lock = None
@@ -99,11 +105,48 @@ class Engine(object):
                 else:
                     raise EngineException("Invalid configuration param '{}' at '[message]':'driver'".format(
                         config["message"]["driver"]))
                 else:
                     raise EngineException("Invalid configuration param '{}' at '[message]':'driver'".format(
                         config["message"]["driver"]))
+            if not self.auth:
+                if config["authentication"]["backend"] == "keystone":
+                    self.auth = AuthconnKeystone(config["authentication"])
+            if not self.operations:
+                if "resources_to_operations" in config["rbac"]:
+                    resources_to_operations_file = config["rbac"]["resources_to_operations"]
+                else:
+                    possible_paths = (
+                        __file__[:__file__.rfind("engine.py")] + "resources_to_operations.yml",
+                        "./resources_to_operations.yml"
+                    )
+                    for config_file in possible_paths:
+                        if path.isfile(config_file):
+                            resources_to_operations_file = config_file
+                            break
+                    if not resources_to_operations_file:                        
+                        raise EngineException("Invalid permission configuration: resources_to_operations file missing")
+                
+                with open(resources_to_operations_file, 'r') as f:
+                    resources_to_operations = yaml.load(f)
+                
+                self.operations = []
+
+                for _, value in resources_to_operations["resources_to_operations"].items():
+                    if value not in self.operations:
+                        self.operations += value
+
+            if config["authentication"]["backend"] == "keystone":
+                self.map_from_topic_to_class["users"] = UserTopicAuth
+                self.map_from_topic_to_class["projects"] = ProjectTopicAuth
+                self.map_from_topic_to_class["roles"] = RoleTopicAuth
 
             self.write_lock = Lock()
             # create one class per topic
             for topic, topic_class in self.map_from_topic_to_class.items():
 
             self.write_lock = Lock()
             # create one class per topic
             for topic, topic_class in self.map_from_topic_to_class.items():
-                self.map_topic[topic] = topic_class(self.db, self.fs, self.msg)
+                if self.auth and topic_class in (UserTopicAuth, ProjectTopicAuth):
+                    self.map_topic[topic] = topic_class(self.db, self.fs, self.msg, self.auth)
+                elif self.auth and topic_class == RoleTopicAuth:
+                    self.map_topic[topic] = topic_class(self.db, self.fs, self.msg, self.auth,
+                                                        self.operations)
+                else:
+                    self.map_topic[topic] = topic_class(self.db, self.fs, self.msg)
         except (DbException, FsException, MsgException) as e:
             raise EngineException(str(e), http_code=e.http_code)
 
         except (DbException, FsException, MsgException) as e:
             raise EngineException(str(e), http_code=e.http_code)
 
index a9e2b24..63a6eca 100644 (file)
@@ -213,6 +213,9 @@ class Server(object):
                     "projects": {"METHODS": ("GET", "POST"),
                                  "<ID>": {"METHODS": ("GET", "DELETE")}
                                  },
                     "projects": {"METHODS": ("GET", "POST"),
                                  "<ID>": {"METHODS": ("GET", "DELETE")}
                                  },
+                    "roles": {"METHODS": ("GET", "POST"),
+                              "<ID>": {"METHODS": ("GET", "POST", "DELETE")}
+                              },
                     "vims": {"METHODS": ("GET", "POST"),
                              "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
                              },
                     "vims": {"METHODS": ("GET", "POST"),
                              "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
                              },
index c8c96cd..81b288d 100644 (file)
@@ -565,6 +565,17 @@ pdu_edit_schema = {
 }
 
 # USERS
 }
 
 # USERS
+project_role_mapping = {
+    "title": "",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "object",
+    "properties": {
+        "project": shortname_schema,
+        "role": shortname_schema
+    },
+    "required": ["project", "role"],
+    "additionalProperties": False
+}
 user_new_schema = {
     "$schema": "http://json-schema.org/draft-04/schema#",
     "title": "New user schema",
 user_new_schema = {
     "$schema": "http://json-schema.org/draft-04/schema#",
     "title": "New user schema",
@@ -573,8 +584,13 @@ user_new_schema = {
         "username": shortname_schema,
         "password": passwd_schema,
         "projects": nameshort_list_schema,
         "username": shortname_schema,
         "password": passwd_schema,
         "projects": nameshort_list_schema,
+        "project_role_mappings": {
+            "type": "array",
+            "items": project_role_mapping,
+            "minItems": 1
+        },
     },
     },
-    "required": ["username", "password", "projects"],
+    "required": ["username", "password"],
     "additionalProperties": False
 }
 user_edit_schema = {
     "additionalProperties": False
 }
 user_edit_schema = {
@@ -589,6 +605,11 @@ user_edit_schema = {
                 array_edition_schema
             ]
         },
                 array_edition_schema
             ]
         },
+        "project_role_mappings": {
+            "type": "array",
+            "items": project_role_mapping,
+            "minItems": 1
+        },
     },
     "minProperties": 1,
     "additionalProperties": False
     },
     "minProperties": 1,
     "additionalProperties": False
@@ -617,6 +638,32 @@ project_edit_schema = {
     "minProperties": 1
 }
 
     "minProperties": 1
 }
 
+# ROLES
+roles_new_schema = {
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "New role schema for administrators",
+    "type": "object",
+    "properties": {
+        "name": shortname_schema,
+        "definition": object_schema,
+    },
+    "required": ["name"],
+    "additionalProperties": False
+}
+roles_edit_schema = {
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "Roles edit schema for administrators",
+    "type": "object",
+    "properties": {
+        "_id": id_schema,
+        "name": shortname_schema,
+        "definition": object_schema,
+    },
+    "required": ["_id", "name", "definition"],
+    "additionalProperties": False,
+    "minProperties": 1
+}
+
 # GLOBAL SCHEMAS
 
 nbi_new_input_schemas = {
 # GLOBAL SCHEMAS
 
 nbi_new_input_schemas = {