RBAC with internal authentication backend - Phase 2 38/7738/22
authordelacruzramo <pedro.delacruzramos@altran.com>
Tue, 2 Jul 2019 12:37:47 +0000 (14:37 +0200)
committerdelacruzramo <pedro.delacruzramos@altran.com>
Wed, 7 Aug 2019 10:34:45 +0000 (12:34 +0200)
Change-Id: Iaca4f3022c4184e03f9346d492e55e902e5ca720
Signed-off-by: delacruzramo <pedro.delacruzramos@altran.com>
osm_nbi/admin_topics.py
osm_nbi/auth.py
osm_nbi/authconn.py
osm_nbi/authconn_internal.py
osm_nbi/authconn_keystone.py
osm_nbi/base_topic.py
osm_nbi/engine.py
osm_nbi/nbi.py

index 187ca82..efcb0f1 100644 (file)
@@ -25,7 +25,9 @@ from validation import validate_input
 from validation import ValidationError
 from validation import is_valid_uuid    # To check that User/Project Names don't look like UUIDs
 from base_topic import BaseTopic, EngineException
-from authconn_keystone import AuthconnKeystone
+from osm_common.dbbase import deep_update_rfc7396
+from authconn import AuthconnNotFoundException, AuthconnConflictException
+# from authconn_keystone import AuthconnKeystone
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 
@@ -86,7 +88,7 @@ class UserTopic(BaseTopic):
         if content.get("password"):
             content["password"] = sha256(content["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
         if content.get("project_role_mappings"):
-            projects = [mapping[0] for mapping in content["project_role_mappings"]]
+            projects = [mapping["project"] for mapping in content["project_role_mappings"]]
 
             if content.get("projects"):
                 content["projects"] += projects
@@ -413,10 +415,19 @@ class UserTopicAuth(UserTopic):
 
         if "projects" in indata.keys():
             # convert to new format project_role_mappings
+            role = self.auth.get_role_list({"name": "project_admin"})
+            if not role:
+                role = self.auth.get_role_list()
+            if not role:
+                raise AuthconnNotFoundException("Can't find default role for user '{}'".format(username))
+            rid = role[0]["_id"]
             if not indata.get("project_role_mappings"):
                 indata["project_role_mappings"] = []
             for project in indata["projects"]:
-                indata["project_role_mappings"].append({"project": project, "role": "project_user"})
+                pid = self.auth.get_project(project)["_id"]
+                prm = {"project": pid, "role": rid}
+                if prm not in indata["project_role_mappings"]:
+                    indata["project_role_mappings"].append(prm)
             # raise EngineException("Format invalid: the keyword 'projects' is not allowed for keystone authentication",
             #                       HTTPStatus.BAD_REQUEST)
 
@@ -458,40 +469,7 @@ class UserTopicAuth(UserTopic):
         """
         if db_content["username"] == session["username"]:
             raise EngineException("You cannot delete your own login 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"]
+        # TODO: Check that user is not logged in ? How? (Would require listing current tokens)
 
     @staticmethod
     def format_on_show(content):
@@ -501,14 +479,14 @@ class UserTopicAuth(UserTopic):
         """
         project_role_mappings = []
 
-        for project in content["projects"]:
-            for role in project["roles"]:
-                project_role_mappings.append({"project": project["_id"],
-                                              "project_name": project["name"],
-                                              "role": role["_id"],
-                                              "role_name": role["name"]})
-
-        del content["projects"]
+        if "projects" in content:
+            for project in content["projects"]:
+                for role in project["roles"]:
+                    project_role_mappings.append({"project": project["_id"],
+                                                  "project_name": project["name"],
+                                                  "role": role["_id"],
+                                                  "role_name": role["name"]})
+            del content["projects"]
         content["project_role_mappings"] = project_role_mappings
 
         return content
@@ -524,7 +502,7 @@ class UserTopicAuth(UserTopic):
         :param indata: data to be inserted
         :param kwargs: used to override the indata descriptor
         :param headers: http request headers
-        :return: _id: identity of the inserted data.
+        :return: _id: identity of the inserted data, operation _id (None)
         """
         try:
             content = BaseTopic._remove_envelop(indata)
@@ -534,16 +512,25 @@ class UserTopicAuth(UserTopic):
             content = self._validate_input_new(content, session["force"])
             self.check_conflict_on_new(session, content)
             # self.format_on_new(content, session["project_id"], make_public=session["public"])
-            _id = self.auth.create_user(content["username"], content["password"])["_id"]
-
-            if "project_role_mappings" in content.keys():
-                for mapping in content["project_role_mappings"]:
-                    self.auth.assign_role_to_user(_id, mapping["project"], mapping["role"])
+            now = time()
+            content["_admin"] = {"created": now, "modified": now}
+            prms = []
+            for prm in content.get("project_role_mappings", []):
+                proj = self.auth.get_project(prm["project"], not session["force"])
+                role = self.auth.get_role(prm["role"], not session["force"])
+                pid = proj["_id"] if proj else None
+                rid = role["_id"] if role else None
+                prl = {"project": pid, "role": rid}
+                if prl not in prms:
+                    prms.append(prl)
+            content["project_role_mappings"] = prms
+            # _id = self.auth.create_user(content["username"], content["password"])["_id"]
+            _id = self.auth.create_user(content)["_id"]
 
             rollback.append({"topic": self.topic, "_id": _id})
             # del content["password"]
             # self._send_msg("create", content)
-            return _id
+            return _id, None
         except ValidationError as e:
             raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
 
@@ -590,18 +577,30 @@ class UserTopicAuth(UserTopic):
             self.check_conflict_on_edit(session, content, indata, _id=_id)
             # self.format_on_edit(content, indata)
 
-            if "password" in indata or "username" in indata:
-                self.auth.update_user(_id, new_name=indata.get("username"), new_password=indata.get("password"))
-            if not indata.get("remove_project_role_mappings") and not indata.get("add_project_role_mappings") and \
-                    not indata.get("project_role_mappings"):
+            if not ("password" in indata or "username" in indata or indata.get("remove_project_role_mappings") or
+                    indata.get("add_project_role_mappings") or indata.get("project_role_mappings") or
+                    indata.get("projects") or indata.get("add_projects")):
                 return _id
-            if indata.get("project_role_mappings") and \
-                    (indata.get("remove_project_role_mappings") or indata.get("add_project_role_mappings")):
+            if indata.get("project_role_mappings") \
+                    and (indata.get("remove_project_role_mappings") or indata.get("add_project_role_mappings")):
                 raise EngineException("Option 'project_role_mappings' is incompatible with 'add_project_role_mappings"
                                       "' or 'remove_project_role_mappings'", http_code=HTTPStatus.BAD_REQUEST)
 
-            user = self.show(session, _id)
-            original_mapping = user["project_role_mappings"]
+            if indata.get("projects") or indata.get("add_projects"):
+                role = self.auth.get_role_list({"name": "project_admin"})
+                if not role:
+                    role = self.auth.get_role_list()
+                if not role:
+                    raise AuthconnNotFoundException("Can't find a default role for user '{}'"
+                                                    .format(content["username"]))
+                rid = role[0]["_id"]
+                if "add_project_role_mappings" not in indata:
+                    indata["add_project_role_mappings"] = []
+                for proj in indata.get("projects", []) + indata.get("add_projects", []):
+                    indata["add_project_role_mappings"].append({"project": proj, "role": rid})
+
+            # user = self.show(session, _id)   # Already in 'content'
+            original_mapping = content["project_role_mappings"]
 
             mappings_to_add = []
             mappings_to_remove = []
@@ -623,7 +622,9 @@ class UserTopicAuth(UserTopic):
                             mappings_to_remove.remove(mapping)
                         break  # do not add, it is already at user
                 else:
-                    mappings_to_add.append(to_add)
+                    pid = self.auth.get_project(to_add["project"])["_id"]
+                    rid = self.auth.get_role(to_add["role"])["_id"]
+                    mappings_to_add.append({"project": pid, "role": rid})
 
             # set
             if indata.get("project_role_mappings"):
@@ -631,12 +632,13 @@ class UserTopicAuth(UserTopic):
                     for mapping in original_mapping:
                         if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
                                 to_set["role"] in (mapping["role"], mapping["role_name"]):
-
                             if mapping in mappings_to_remove:   # do not remove
                                 mappings_to_remove.remove(mapping)
                             break  # do not add, it is already at user
                     else:
-                        mappings_to_add.append(to_set)
+                        pid = self.auth.get_project(to_set["project"])["_id"]
+                        rid = self.auth.get_role(to_set["role"])["_id"]
+                        mappings_to_add.append({"project": pid, "role": rid})
                 for mapping in original_mapping:
                     for to_set in indata["project_role_mappings"]:
                         if to_set["project"] in (mapping["project"], mapping["project_name"]) and \
@@ -647,21 +649,12 @@ class UserTopicAuth(UserTopic):
                         if mapping not in mappings_to_remove:   # do not remove
                             mappings_to_remove.append(mapping)
 
-            for mapping in mappings_to_remove:
-                self.auth.remove_role_from_user(
-                    _id,
-                    mapping["project"],
-                    mapping["role"]
-                )
-
-            for mapping in mappings_to_add:
-                self.auth.assign_role_to_user(
-                    _id,
-                    mapping["project"],
-                    mapping["role"]
-                )
-
-            return "_id"
+            self.auth.update_user({"_id": _id, "username": indata.get("username"), "password": indata.get("password"),
+                                   "add_project_role_mappings": mappings_to_add,
+                                   "remove_project_role_mappings": mappings_to_remove
+                                   })
+
+            # return _id
         except ValidationError as e:
             raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
 
@@ -687,14 +680,11 @@ class UserTopicAuth(UserTopic):
         :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
         """
         # Allow _id to be a name or uuid
-        filter_q = {self.id_field(self.topic, _id): _id}
-        user_list = self.auth.get_user_list(filter_q)
-        if not user_list:
-            raise EngineException("User '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
-        _id = user_list[0]["_id"]
-        self.check_conflict_on_del(session, _id, user_list[0])
+        user = self.auth.get_user(_id)
+        uid = user["_id"]
+        self.check_conflict_on_del(session, uid, user)
         if not dry_run:
-            v = self.auth.delete_user(_id)
+            v = self.auth.delete_user(uid)
             return v
         return None
 
@@ -739,11 +729,14 @@ class ProjectTopicAuth(ProjectTopic):
         """
 
         project_name = edit_content.get("name")
-        if project_name:
+        if project_name != final_content["name"]:  # It is a true renaming
             if is_valid_uuid(project_name):
-                raise EngineException("project name  '{}' cannot be an uuid format".format(project_name),
+                raise EngineException("project name  '{}' cannot have an uuid format".format(project_name),
                                       HTTPStatus.UNPROCESSABLE_ENTITY)
 
+            if final_content["name"] == "admin":
+                raise EngineException("You cannot rename project 'admin'", http_code=HTTPStatus.CONFLICT)
+
             # Check that project name is not used, regardless keystone already checks this
             if self.auth.get_project_list(filter_q={"name": project_name}):
                 raise EngineException("project '{}' is already used".format(project_name), HTTPStatus.CONFLICT)
@@ -757,13 +750,33 @@ class ProjectTopicAuth(ProjectTopic):
         :param db_content: The database content of this item _id
         :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"] in session["project_id"]][0]
-        # TODO check that any user is using this project, raise CONFLICT exception
-        if _id == session["project_id"]:
+
+        def check_rw_projects(topic, title, id_field):
+            for desc in self.db.get_list(topic):
+                if _id in desc["_admin"]["projects_read"] + desc["_admin"]["projects_write"]:
+                    raise EngineException("Project '{}' ({}) is being used by {} '{}'"
+                                          .format(db_content["name"], _id, title, desc[id_field]), HTTPStatus.CONFLICT)
+
+        if _id in session["project_id"]:
             raise EngineException("You cannot delete your own project", http_code=HTTPStatus.CONFLICT)
 
+        if db_content["name"] == "admin":
+            raise EngineException("You cannot delete project 'admin'", http_code=HTTPStatus.CONFLICT)
+
+        # If any user is using this project, raise CONFLICT exception
+        if not session["force"]:
+            for user in self.auth.get_user_list():
+                if _id in [proj["_id"] for proj in user.get("projects", [])]:
+                    raise EngineException("Project '{}' ({}) is being used by user '{}'"
+                                          .format(db_content["name"], _id, user["username"]), HTTPStatus.CONFLICT)
+
+        # If any VNFD, NSD, NST, PDU, etc. is using this project, raise CONFLICT exception
+        if not session["force"]:
+            check_rw_projects("vnfds", "VNF Descriptor", "id")
+            check_rw_projects("nsds", "NS Descriptor", "id")
+            check_rw_projects("nsts", "NS Template", "id")
+            check_rw_projects("pdus", "PDU Descriptor", "name")
+
     def new(self, rollback, session, indata=None, kwargs=None, headers=None):
         """
         Creates a new entry into the authentication backend.
@@ -775,7 +788,7 @@ class ProjectTopicAuth(ProjectTopic):
         :param indata: data to be inserted
         :param kwargs: used to override the indata descriptor
         :param headers: http request headers
-        :return: _id: identity of the inserted data.
+        :return: _id: identity of the inserted data, operation _id (None)
         """
         try:
             content = BaseTopic._remove_envelop(indata)
@@ -785,10 +798,10 @@ class ProjectTopicAuth(ProjectTopic):
             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"])
-            _id = self.auth.create_project(content["name"])
+            _id = self.auth.create_project(content)
             rollback.append({"topic": self.topic, "_id": _id})
             # self._send_msg("create", content)
-            return _id
+            return _id, None
         except ValidationError as e:
             raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
 
@@ -831,14 +844,11 @@ class ProjectTopicAuth(ProjectTopic):
         :return: dictionary with deleted item _id. It raises EngineException on error: not found, conflict, ...
         """
         # Allow _id to be a name or uuid
-        filter_q = {self.id_field(self.topic, _id): _id}
-        project_list = self.auth.get_project_list(filter_q)
-        if not project_list:
-            raise EngineException("Project '{}' not found".format(_id), http_code=HTTPStatus.NOT_FOUND)
-        _id = project_list[0]["_id"]
-        self.check_conflict_on_del(session, _id, project_list[0])
+        proj = self.auth.get_project(_id)
+        pid = proj["_id"]
+        self.check_conflict_on_del(session, pid, proj)
         if not dry_run:
-            v = self.auth.delete_project(_id)
+            v = self.auth.delete_project(pid)
             return v
         return None
 
@@ -864,10 +874,11 @@ class ProjectTopicAuth(ProjectTopic):
             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.format_on_edit(content, indata)
 
             if "name" in indata:
-                self.auth.update_project(content["_id"], indata["name"])
+                content["name"] = indata["name"]
+            self.auth.update_project(content["_id"], content)
         except ValidationError as e:
             raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
 
@@ -883,7 +894,7 @@ class RoleTopicAuth(BaseTopic):
         BaseTopic.__init__(self, db, fs, msg)
         self.auth = auth
         self.operations = ops
-        self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles"
+        self.topic = "roles_operations" if isinstance(auth, AuthconnKeystone) else "roles"
 
     @staticmethod
     def validate_role_definition(operations, role_definitions):
@@ -946,8 +957,10 @@ class RoleTopicAuth(BaseTopic):
         :return: None or raises EngineException
         """
         # 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)
+        name = indata["name"]
+        # if self.db.get_one(self.topic, {"name": indata.get("name")}, fail_on_empty=False, fail_on_more=False):
+        if self.auth.get_role_list({"name": name}):
+            raise EngineException("role name '{}' exists".format(name), HTTPStatus.CONFLICT)
 
     def check_conflict_on_edit(self, session, final_content, edit_content, _id):
         """
@@ -967,7 +980,9 @@ class RoleTopicAuth(BaseTopic):
         # 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):
+            # if self.db.get_one(self.topic, {"name":role_name,"_id.ne":_id}, fail_on_empty=False, fail_on_more=False):
+            roles = self.auth.get_role_list({"name": role_name})
+            if roles and roles[0][BaseTopic.id_field("roles", _id)] != _id:
                 raise EngineException("role name '{}' exists".format(role_name), HTTPStatus.CONFLICT)
 
     def check_conflict_on_del(self, session, _id, db_content):
@@ -979,14 +994,18 @@ class RoleTopicAuth(BaseTopic):
         :param db_content: The database content of this item _id
         :return: None if ok or raises EngineException with the conflict
         """
-        roles = self.auth.get_role_list()
-        system_admin_roles = [role for role in roles if role["name"] == "system_admin"]
+        role = self.auth.get_role(_id)
+        if role["name"] in ["system_admin", "project_admin"]:
+            raise EngineException("You cannot delete role '{}'".format(role["name"]), http_code=HTTPStatus.FORBIDDEN)
 
-        if system_admin_roles and _id == system_admin_roles[0]["_id"]:
-            raise EngineException("You cannot delete system_admin role", http_code=HTTPStatus.FORBIDDEN)
+        # If any user is using this role, raise CONFLICT exception
+        for user in self.auth.get_user_list():
+            if _id in [prl["_id"] for proj in user.get("projects", []) for prl in proj.get("roles", [])]:
+                raise EngineException("Role '{}' ({}) is being used by user '{}'"
+                                      .format(role["name"], _id, user["username"]), HTTPStatus.CONFLICT)
 
     @staticmethod
-    def format_on_new(content, project_id=None, make_public=False):
+    def format_on_new(content, project_id=None, make_public=False):   # TO BE REMOVED ?
         """
         Modifies content descriptor to include _admin
 
@@ -1019,7 +1038,8 @@ class RoleTopicAuth(BaseTopic):
         :param edit_content: alterations to be include
         :return: None, but final_content is modified
         """
-        final_content["_admin"]["modified"] = time()
+        if "_admin" in final_content:
+            final_content["_admin"]["modified"] = time()
 
         if "permissions" not in final_content:
             final_content["permissions"] = {}
@@ -1030,62 +1050,31 @@ class RoleTopicAuth(BaseTopic):
             final_content["permissions"]["admin"] = False
         return None
 
-    # @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}
-    #     filter_db = { BaseTopic.id_field(self.topic, _id): _id }   # To allow role addressing by name
-    #
-    #     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"]
-    #
-    #     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 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_q = {BaseTopic.id_field(self.topic, _id): _id}
+        roles = self.auth.get_role_list(filter_q)
+        if not roles:
+            raise AuthconnNotFoundException("Not found any role with filter {}".format(filter_q))
+        elif len(roles) > 1:
+            raise AuthconnConflictException("Found more than one role with filter {}".format(filter_q))
+        return roles[0]
+
+    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.
+        """
+        return self.auth.get_role_list(filter_q)
 
     def new(self, rollback, session, indata=None, kwargs=None, headers=None):
         """
@@ -1096,7 +1085,7 @@ class RoleTopicAuth(BaseTopic):
         :param indata: data to be inserted
         :param kwargs: used to override the indata descriptor
         :param headers: http request headers
-        :return: _id: identity of the inserted data.
+        :return: _id: identity of the inserted data, operation _id (None)
         """
         try:
             content = self._remove_envelop(indata)
@@ -1106,13 +1095,13 @@ class RoleTopicAuth(BaseTopic):
             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_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})
+            role_name = content["name"]
+            rid = self.auth.create_role(content)
+            content["_id"] = rid
+            _id = self.db.create(self.topic, content)
+            rollback.append({"topic": self.topic, "_id": rid})
             # self._send_msg("create", content)
-            return _id
+            return rid, None
         except ValidationError as e:
             raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
 
@@ -1125,12 +1114,19 @@ class RoleTopicAuth(BaseTopic):
         :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, None)
+        filter_q = {BaseTopic.id_field(self.topic, _id): _id}
+        roles = self.auth.get_role_list(filter_q)
+        if not roles:
+            raise AuthconnNotFoundException("Not found any role with filter {}".format(filter_q))
+        elif len(roles) > 1:
+            raise AuthconnConflictException("Found more than one role with filter {}".format(filter_q))
+        rid = roles[0]["_id"]
+        self.check_conflict_on_del(session, rid, None)
         # filter_q = {"_id": _id}
-        filter_q = {BaseTopic.id_field(self.topic, _id): _id}   # To allow role addressing by name
+        filter_q = {BaseTopic.id_field(self.topic, _id): _id}   # To allow role addressing by name
         if not dry_run:
-            self.auth.delete_role(_id)
-            v = self.db.del_one(self.topic, filter_q)
+            v = self.auth.delete_role(rid)
+            v = self.db.del_one(self.topic, filter_q)
             return v
         return None
 
@@ -1145,6 +1141,15 @@ class RoleTopicAuth(BaseTopic):
         :param content:
         :return: _id: identity of the inserted data.
         """
-        _id = super().edit(session, _id, indata, kwargs, content)
-        if indata.get("name"):
-            self.auth.update_role(_id, name=indata.get("name"))
+        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)
+            deep_update_rfc7396(content, indata)
+            self.check_conflict_on_edit(session, content, indata, _id=_id)
+            self.format_on_edit(content, indata)
+            self.auth.update_role(content)
+        except ValidationError as e:
+            raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
index 3d74d89..94eb1e9 100644 (file)
@@ -47,7 +47,7 @@ from osm_common import dbmemory
 from osm_common.dbbase import DbException
 from itertools import chain
 
-from uuid import uuid4   # For Role _id with internal authentication backend
+from uuid import uuid4
 
 
 class Authenticator:
@@ -72,7 +72,7 @@ class Authenticator:
         self.tokens_cache = dict()
         self.next_db_prune_time = 0  # time when next cleaning of expired tokens must be done
         self.roles_to_operations_file = None
-        self.roles_to_operations_table = None
+        self.roles_to_operations_table = None
         self.resources_to_operations_mapping = {}
         self.operation_to_allowed_roles = {}
         self.logger = logging.getLogger("nbi.authenticator")
@@ -103,7 +103,7 @@ class Authenticator:
                                         .format(config["database"]["driver"]))
             if not self.backend:
                 if config["authentication"]["backend"] == "keystone":
-                    self.backend = AuthconnKeystone(self.config["authentication"])
+                    self.backend = AuthconnKeystone(self.config["authentication"], self.db, self.tokens_cache)
                 elif config["authentication"]["backend"] == "internal":
                     self.backend = AuthconnInternal(self.config["authentication"], self.db, self.tokens_cache)
                     self._internal_tokens_prune()
@@ -126,11 +126,6 @@ class Authenticator:
                 if not self.roles_to_operations_file:
                     raise AuthException("Invalid permission configuration: roles_to_operations file missing")
 
-            if not self.roles_to_operations_table:  # PROVISIONAL ?
-                self.roles_to_operations_table = "roles_operations" \
-                    if config["authentication"]["backend"] == "keystone" \
-                    else "roles"
-
             # load role_permissions
             def load_role_permissions(method_dict):
                 for k in method_dict:
@@ -161,6 +156,50 @@ class Authenticator:
         except DbException as e:
             raise AuthException(str(e), http_code=e.http_code)
 
+    def create_admin_project(self):
+        """
+        Creates a new project 'admin' into database if it doesn't exist. Useful for initialization.
+        :return: _id identity of the 'admin' project
+        """
+
+        # projects = self.db.get_one("projects", fail_on_empty=False, fail_on_more=False)
+        project_desc = {"name": "admin"}
+        projects = self.backend.get_project_list(project_desc)
+        if projects:
+            return projects[0]["_id"]
+        now = time()
+        project_desc["_id"] = str(uuid4())
+        project_desc["_admin"] = {"created": now, "modified": now}
+        pid = self.backend.create_project(project_desc)
+        self.logger.info("Project '{}' created at database".format(project_desc["name"]))
+        return pid
+
+    def create_admin_user(self, project_id):
+        """
+        Creates a new user admin/admin into database if database is empty. Useful for initialization
+        :return: _id identity of the inserted data, or None
+        """
+        # users = self.db.get_one("users", fail_on_empty=False, fail_on_more=False)
+        users = self.backend.get_user_list()
+        if users:
+            return None
+        # user_desc = {"username": "admin", "password": "admin", "projects": [project_id]}
+        now = time()
+        user_desc = {"username": "admin", "password": "admin", "_admin": {"created": now, "modified": now}}
+        if project_id:
+            pid = project_id
+        else:
+            # proj = self.db.get_one("projects", {"name": "admin"}, fail_on_empty=False, fail_on_more=False)
+            proj = self.backend.get_project_list({"name": "admin"})
+            pid = proj[0]["_id"] if proj else None
+        # role = self.db.get_one("roles", {"name": "system_admin"}, fail_on_empty=False, fail_on_more=False)
+        roles = self.backend.get_role_list({"name": "system_admin"})
+        if pid and roles:
+            user_desc["project_role_mappings"] = [{"project": pid, "role": roles[0]["_id"]}]
+        uid = self.backend.create_user(user_desc)
+        self.logger.info("User '{}' created at database".format(user_desc["username"]))
+        return uid
+
     def init_db(self, target_version='1.0'):
         """
         Check if the database has been initialized, with at least one user. If not, create the required tables
@@ -170,14 +209,10 @@ class Authenticator:
         :return: None if OK, exception if error or version is different.
         """
 
-        # PCR 28/05/2019 Commented out to allow initialization for internal backend
-        # if self.config["authentication"]["backend"] == "internal":
-        #    return
-
-        records = self.db.get_list(self.roles_to_operations_table)
+        records = self.backend.get_role_list()
 
         # Loading permissions to MongoDB if there is not any permission.
-        if not records:
+        if not records or (len(records) == 1 and records[0]["name"] == "admin"):
             with open(self.roles_to_operations_file, "r") as stream:
                 roles_to_operations_yaml = yaml.load(stream)
 
@@ -216,22 +251,19 @@ class Authenticator:
                     "modified": now,
                 }
 
-                if self.config["authentication"]["backend"] == "keystone":
-                    if 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:
-                            backend_id = self.backend.create_role(role_with_operations["name"])
-                        role_with_operations["_id"] = backend_id
-                else:
-                    role_with_operations["_id"] = str(uuid4())
-
-                self.db.create(self.roles_to_operations_table, role_with_operations)
+                # self.db.create(self.roles_to_operations_table, role_with_operations)
+                self.backend.create_role(role_with_operations)
                 self.logger.info("Role '{}' created at database".format(role_with_operations["name"]))
 
-        if self.config["authentication"]["backend"] != "internal":
-            self.backend.assign_role_to_user("admin", "admin", "system_admin")
+        # Create admin project&user if required
+        pid = self.create_admin_project()
+        self.create_admin_user(pid)
+
+        if self.config["authentication"]["backend"] == "keystone":
+            try:
+                self.backend.assign_role_to_user("admin", "admin", "system_admin")
+            except Exception:
+                pass
 
         self.load_operation_to_allowed_roles()
 
@@ -243,10 +275,13 @@ class Authenticator:
         """
 
         permissions = {oper: [] for oper in self.role_permissions}
-        records = self.db.get_list(self.roles_to_operations_table)
+        # records = self.db.get_list(self.roles_to_operations_table)
+        records = self.backend.get_role_list()
 
         ignore_fields = ["_id", "_admin", "name", "default"]
         for record in records:
+            if not record.get("permissions"):
+                continue
             record_permissions = {oper: record["permissions"].get("default", False) for oper in self.role_permissions}
             operations_joined = [(oper, value) for oper, value in record["permissions"].items()
                                  if oper not in ignore_fields]
index 15d0d99..1727590 100644 (file)
@@ -27,6 +27,7 @@ __author__ = "Eduardo Sousa <esousa@whitestack.com>"
 __date__ = "$27-jul-2018 23:59:59$"
 
 from http import HTTPStatus
+from base_topic import BaseTopic
 
 
 class AuthException(Exception):
@@ -108,7 +109,7 @@ class Authconn:
     Each Auth backend connector plugin must be a subclass of
     Authconn class.
     """
-    def __init__(self, config):
+    def __init__(self, config, db, token_cache):
         """
         Constructor of the Authconn class.
 
@@ -136,18 +137,6 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    # def authenticate_with_token(self, token, project=None):
-    #     """
-    #     Authenticate a user using a token. Can be used to revalidate the token
-    #     or to get a scoped token.
-    #
-    #     :param token: a valid token.
-    #     :param project: (optional) project for a scoped token.
-    #     :return: return a revalidated token, scoped if a project was passed or
-    #     the previous token was already scoped.
-    #     """
-    #     raise AuthconnNotImplementedException("Should have implemented this")
-
     def validate_token(self, token):
         """
         Check if the token is valid.
@@ -166,43 +155,21 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def get_user_project_list(self, token):
-        """
-        Get all the projects associated with a user.
-
-        :param token: valid token
-        :return: list of projects
-        """
-        raise AuthconnNotImplementedException("Should have implemented this")
-
-    def get_user_role_list(self, token):
-        """
-        Get role list for a scoped project.
-
-        :param token: scoped token.
-        :return: returns the list of roles for the user in that project. If
-        the token is unscoped it returns None.
-        """
-        raise AuthconnNotImplementedException("Should have implemented this")
-
-    def create_user(self, user, password):
+    def create_user(self, user_info):
         """
         Create a user.
 
-        :param user: username.
-        :param password: password.
+        :param user_info: full user info.
         :raises AuthconnOperationException: if user creation failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def update_user(self, user, new_name=None, new_password=None):
+    def update_user(self, user_info):
         """
         Change the user name and/or password.
 
-        :param user: username or user_id
-        :param new_name: new name
-        :param new_password: new password.
-        :raises AuthconnOperationException: if change failed.
+        :param user_info:  user info modifications
+        :raises AuthconnNotImplementedException: if function not implemented
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
@@ -223,11 +190,21 @@ class Authconn:
         :return: returns a list of users.
         """
 
-    def create_role(self, role):
+    def get_user(self, id, fail=True):
+        filt = {BaseTopic.id_field("users", id): id}
+        users = self.get_user_list(filt)
+        if not users:
+            if fail:
+                raise AuthconnNotFoundException("User with {} not found".format(filt), http_code=HTTPStatus.NOT_FOUND)
+            else:
+                return None
+        return users[0]
+
+    def create_role(self, role_info):
         """
         Create a role.
 
-        :param role: role name.
+        :param role_info: full role info.
         :raises AuthconnOperationException: if role creation failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
@@ -250,20 +227,29 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def update_role(self, role, new_name):
+    def get_role(self, id, fail=True):
+        filt = {BaseTopic.id_field("roles", id): id}
+        roles = self.get_role_list(filt)
+        if not roles:
+            if fail:
+                raise AuthconnNotFoundException("Role with {} not found".format(filt))
+            else:
+                return None
+        return roles[0]
+
+    def update_role(self, role_info):
         """
-        Change the name of a role
-        :param role: role name or id to be changed
-        :param new_name: new name
+        Change the information of a role
+        :param role_info: full role info
         :return: None
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def create_project(self, project):
+    def create_project(self, project_info):
         """
         Create a project.
 
-        :param project: project name.
+        :param project_info: full project info.
         :return: the internal id of the created project
         :raises AuthconnOperationException: if project creation failed.
         """
@@ -287,33 +273,21 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def update_project(self, project_id, new_name):
+    def get_project(self, id, fail=True):
+        filt = {BaseTopic.id_field("projects", id): id}
+        projs = self.get_project_list(filt)
+        if not projs:
+            if fail:
+                raise AuthconnNotFoundException("project with {} not found".format(filt))
+            else:
+                return None
+        return projs[0]
+
+    def update_project(self, project_id, project_info):
         """
-        Change the name of a project
+        Change the information of a project
         :param project_id: project to be changed
-        :param new_name: new name
+        :param project_info: full project info
         :return: None
         """
         raise AuthconnNotImplementedException("Should have implemented this")
-
-    def assign_role_to_user(self, user, project, role):
-        """
-        Assigning a role to a user in a project.
-
-        :param user: username.
-        :param project: project name.
-        :param role: role name.
-        :raises AuthconnOperationException: if role assignment failed.
-        """
-        raise AuthconnNotImplementedException("Should have implemented this")
-
-    def remove_role_from_user(self, user, project, role):
-        """
-        Remove a role from a user in a project.
-
-        :param user: username.
-        :param project: project name.
-        :param role: role name.
-        :raises AuthconnOperationException: if role assignment revocation failed.
-        """
-        raise AuthconnNotImplementedException("Should have implemented this")
index d3258fe..02b5890 100644 (file)
@@ -27,11 +27,12 @@ OSM Internal Authentication Backend and leverages the RBAC model
 __author__ = "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>"
 __date__ = "$06-jun-2019 11:16:08$"
 
-from authconn import Authconn, AuthException
+from authconn import Authconn, AuthException   # , AuthconnOperationException
 from osm_common.dbbase import DbException
 from base_topic import BaseTopic
 
 import logging
+import re
 from time import time
 from http import HTTPStatus
 from uuid import uuid4
@@ -42,7 +43,7 @@ from random import choice as random_choice
 
 class AuthconnInternal(Authconn):
     def __init__(self, config, db, token_cache):
-        Authconn.__init__(self, config)
+        Authconn.__init__(self, config, db, token_cache)
 
         self.logger = logging.getLogger("nbi.authenticator.internal")
 
@@ -56,48 +57,6 @@ class AuthconnInternal(Authconn):
         self.auth = None
         self.sess = None
 
-    # def create_token (self, user, password, projects=[], project=None, remote=None):
-    # Not Required
-
-    # def authenticate_with_user_password(self, user, password, project=None, remote=None):
-    # Not Required
-
-    # def authenticate_with_token(self, token, project=None, remote=None):
-    # Not Required
-
-    # def get_user_project_list(self, token):
-    # Not Required
-
-    # def get_user_role_list(self, token):
-    # Not Required
-
-    # def create_user(self, user, password):
-    # Not Required
-
-    # def change_password(self, user, new_password):
-    # Not Required
-
-    # def delete_user(self, user_id):
-    # Not Required
-
-    # def get_user_list(self, filter_q={}):
-    # Not Required
-
-    # def get_project_list(self, filter_q={}):
-    # Not required
-
-    # def create_project(self, project):
-    # Not required
-
-    # def delete_project(self, project_id):
-    # Not required
-
-    # def assign_role_to_user(self, user, project, role):
-    # Not required in Phase 1
-
-    # def remove_role_from_user(self, user, project, role):
-    # Not required in Phase 1
-
     def validate_token(self, token):
         """
         Check if the token is valid.
@@ -190,100 +149,105 @@ class AuthconnInternal(Authconn):
         now = time()
         user_content = None
 
-        try:
-            # Try using username/password
-            if user:
-                user_rows = self.db.get_list("users", {BaseTopic.id_field("users", user): user})
-                if user_rows:
-                    user_content = user_rows[0]
-                    salt = user_content["_admin"]["salt"]
-                    shadow_password = sha256(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
-                    if shadow_password != user_content["password"]:
-                        user_content = None
-                if not user_content:
-                    raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
-            elif token_info:
-                user_rows = self.db.get_list("users", {"username": token_info["username"]})
-                if user_rows:
-                    user_content = user_rows[0]
-                else:
-                    raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
+        # Try using username/password
+        if user:
+            user_rows = self.db.get_list("users", {BaseTopic.id_field("users", user): user})
+            if user_rows:
+                user_content = user_rows[0]
+                salt = user_content["_admin"]["salt"]
+                shadow_password = sha256(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
+                if shadow_password != user_content["password"]:
+                    user_content = None
+            if not user_content:
+                raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED)
+        elif token_info:
+            user_rows = self.db.get_list("users", {"username": token_info["username"]})
+            if user_rows:
+                user_content = user_rows[0]
             else:
-                raise AuthException("Provide credentials: username/password or Authorization Bearer token",
-                                    http_code=HTTPStatus.UNAUTHORIZED)
-
-            token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
-                               for _ in range(0, 32))
-
-            # TODO when user contained project_role_mappings with project_id,project_ name this checking to
-            #  database will not be needed
-            if not project:
-                project = user_content["projects"][0]
-
-            # To allow project names in project_id
-            proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project): project})
-            if proj["_id"] not in user_content["projects"] and proj["name"] not in user_content["projects"]:
-                raise AuthException("project {} not allowed for this user".format(project),
-                                    http_code=HTTPStatus.UNAUTHORIZED)
-
-            # TODO remove admin, this vill be used by roles RBAC
-            if proj["name"] == "admin":
-                token_admin = True
-            else:
-                token_admin = proj.get("admin", False)
-
-            # TODO add token roles - PROVISIONAL. Get this list from user_content["project_role_mappings"]
-            role_id = self.db.get_one("roles", {"name": "system_admin"})["_id"]
-            roles_list = [{"name": "system_admin", "id": role_id}]
-
-            new_token = {"issued_at": now,
-                         "expires": now + 3600,
-                         "_id": token_id,
-                         "id": token_id,
-                         "project_id": proj["_id"],
-                         "project_name": proj["name"],
-                         "username": user_content["username"],
-                         "user_id": user_content["_id"],
-                         "admin": token_admin,
-                         "roles": roles_list,
-                         }
-
-            self.token_cache[token_id] = new_token
-            self.db.create("tokens", new_token)
-            return deepcopy(new_token)
-
-        except Exception as e:
-            msg = "Error during user authentication using internal backend: {}".format(e)
-            self.logger.exception(msg)
-            raise AuthException(msg, http_code=HTTPStatus.UNAUTHORIZED)
-
-    def get_role_list(self):
+                raise AuthException("Invalid token", http_code=HTTPStatus.UNAUTHORIZED)
+        else:
+            raise AuthException("Provide credentials: username/password or Authorization Bearer token",
+                                http_code=HTTPStatus.UNAUTHORIZED)
+
+        token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
+                           for _ in range(0, 32))
+
+        # projects = user_content.get("projects", [])
+        prm_list = user_content.get("project_role_mappings", [])
+
+        if not project:
+            project = prm_list[0]["project"] if prm_list else None
+        if not project:
+            raise AuthException("can't find a default project for this user", http_code=HTTPStatus.UNAUTHORIZED)
+
+        projects = [prm["project"] for prm in prm_list]
+
+        proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project): project})
+        project_name = proj["name"]
+        project_id = proj["_id"]
+        if project_name not in projects and project_id not in projects:
+            raise AuthException("project {} not allowed for this user".format(project),
+                                http_code=HTTPStatus.UNAUTHORIZED)
+
+        # TODO remove admin, this vill be used by roles RBAC
+        if project_name == "admin":
+            token_admin = True
+        else:
+            token_admin = proj.get("admin", False)
+
+        # add token roles
+        roles = []
+        roles_list = []
+        for prm in prm_list:
+            if prm["project"] in [project_id, project_name]:
+                role = self.db.get_one("roles", {BaseTopic.id_field("roles", prm["role"]): prm["role"]})
+                rid = role["_id"]
+                if rid not in roles:
+                    rnm = role["name"]
+                    roles.append(rid)
+                    roles_list.append({"name": rnm, "id": rid})
+        if not roles_list:
+            rid = self.db.get_one("roles", {"name": "project_admin"})["_id"]
+            roles_list = [{"name": "project_admin", "id": rid}]
+
+        new_token = {"issued_at": now,
+                     "expires": now + 3600,
+                     "_id": token_id,
+                     "id": token_id,
+                     "project_id": proj["_id"],
+                     "project_name": proj["name"],
+                     "username": user_content["username"],
+                     "user_id": user_content["_id"],
+                     "admin": token_admin,
+                     "roles": roles_list,
+                     }
+
+        self.token_cache[token_id] = new_token
+        self.db.create("tokens", new_token)
+        return deepcopy(new_token)
+
+    def get_role_list(self, filter_q={}):
         """
         Get role list.
 
         :return: returns the list of roles.
         """
-        try:
-            role_list = self.db.get_list("roles")
-            roles = [{"name": role["name"], "_id": role["_id"]} for role in role_list]   # if role.name != "service" ?
-            return roles
-        except Exception:
-            raise AuthException("Error during role listing using internal backend", http_code=HTTPStatus.UNAUTHORIZED)
+        return self.db.get_list("roles", filter_q)
 
-    def create_role(self, role):
+    def create_role(self, role_info):
         """
         Create a role.
 
-        :param role: role name.
+        :param role_info: full role info.
+        :return: returns the role id.
         :raises AuthconnOperationException: if role creation failed.
         """
-        # try:
         # TODO: Check that role name does not exist ?
-        return str(uuid4())
-        # except Exception:
-        #     raise AuthconnOperationException("Error during role creation using internal backend")
-        # except Conflict as ex:
-        #     self.logger.info("Duplicate entry: %s", str(ex))
+        rid = str(uuid4())
+        role_info["_id"] = rid
+        rid = self.db.create("roles", role_info)
+        return rid
 
     def delete_role(self, role_id):
         """
@@ -292,8 +256,162 @@ class AuthconnInternal(Authconn):
         :param role_id: role identifier.
         :raises AuthconnOperationException: if role deletion failed.
         """
-        # try:
-        # TODO: Check that role exists ?
+        return self.db.del_one("roles", {"_id": role_id})
+
+    def update_role(self, role_info):
+        """
+        Update a role.
+
+        :param role_info: full role info.
+        :return: returns the role name and id.
+        :raises AuthconnOperationException: if user creation failed.
+        """
+        rid = role_info["_id"]
+        self.db.set_one("roles", {"_id": rid}, role_info)   # CONFIRM
+        return {"_id": rid, "name": role_info["name"]}
+
+    def create_user(self, user_info):
+        """
+        Create a user.
+
+        :param user_info: full user info.
+        :return: returns the username and id of the user.
+        """
+        BaseTopic.format_on_new(user_info, make_public=False)
+        salt = uuid4().hex
+        user_info["_admin"]["salt"] = salt
+        if "password" in user_info:
+            user_info["password"] = sha256(user_info["password"].encode('utf-8') + salt.encode('utf-8')).hexdigest()
+        # "projects" are not stored any more
+        if "projects" in user_info:
+            del user_info["projects"]
+        self.db.create("users", user_info)
+        return {"username": user_info["username"], "_id": user_info["_id"]}
+
+    def update_user(self, user_info):
+        """
+        Change the user name and/or password.
+
+        :param user_info: user info modifications
+        """
+        uid = user_info["_id"]
+        user_data = self.db.get_one("users", {BaseTopic.id_field("users", uid): uid})
+        BaseTopic.format_on_edit(user_data, user_info)
+        # User Name
+        usnm = user_info.get("username")
+        if usnm:
+            user_data["username"] = usnm
+        # If password is given and is not already encripted
+        pswd = user_info.get("password")
+        if pswd and (len(pswd) != 64 or not re.match('[a-fA-F0-9]*', pswd)):   # TODO: Improve check?
+            salt = uuid4().hex
+            if "_admin" not in user_data:
+                user_data["_admin"] = {}
+            user_data["_admin"]["salt"] = salt
+            user_data["password"] = sha256(pswd.encode('utf-8') + salt.encode('utf-8')).hexdigest()
+        # Project-Role Mappings
+        # TODO: Check that user_info NEVER includes "project_role_mappings"
+        if "project_role_mappings" not in user_data:
+            user_data["project_role_mappings"] = []
+        for prm in user_info.get("add_project_role_mappings", []):
+            user_data["project_role_mappings"].append(prm)
+        for prm in user_info.get("remove_project_role_mappings", []):
+            for pidf in ["project", "project_name"]:
+                for ridf in ["role", "role_name"]:
+                    try:
+                        user_data["project_role_mappings"].remove({"role": prm[ridf], "project": prm[pidf]})
+                    except KeyError:
+                        pass
+                    except ValueError:
+                        pass
+        self.db.set_one("users", {BaseTopic.id_field("users", uid): uid}, user_data)   # CONFIRM
+
+    def delete_user(self, user_id):
+        """
+        Delete user.
+
+        :param user_id: user identifier.
+        :raises AuthconnOperationException: if user deletion failed.
+        """
+        self.db.del_one("users", {"_id": user_id})
         return True
-        # except Exception:
-        #     raise AuthconnOperationException("Error during role deletion using internal backend")
+
+    def get_user_list(self, filter_q=None):
+        """
+        Get user list.
+
+        :param filter_q: dictionary to filter user list by name (username is also admited) and/or _id
+        :return: returns a list of users.
+        """
+        filt = filter_q or {}
+        if "name" in filt:
+            filt["username"] = filt["name"]
+            del filt["name"]
+        users = self.db.get_list("users", filt)
+        for user in users:
+            projects = []
+            projs_with_roles = []
+            prms = user.get("project_role_mappings", [])
+            for prm in prms:
+                if prm["project"] not in projects:
+                    projects.append(prm["project"])
+            for project in projects:
+                roles = []
+                roles_for_proj = []
+                for prm in prms:
+                    if prm["project"] == project and prm["role"] not in roles:
+                        role = prm["role"]
+                        roles.append(role)
+                        rl = self.db.get_one("roles", {BaseTopic.id_field("roles", role): role})
+                        roles_for_proj.append({"name": rl["name"], "_id": rl["_id"], "id": rl["_id"]})
+                try:
+                    pr = self.db.get_one("projects", {BaseTopic.id_field("projects", project): project})
+                    projs_with_roles.append({"name": pr["name"], "_id": pr["_id"], "id": pr["_id"],
+                                             "roles": roles_for_proj})
+                except Exception as e:
+                    self.logger.exception("Error during user listing using internal backend: {}".format(e))
+            user["projects"] = projs_with_roles
+            if "project_role_mappings" in user:
+                del user["project_role_mappings"]
+        return users
+
+    def get_project_list(self, filter_q={}):
+        """
+        Get role list.
+
+        :return: returns the list of projects.
+        """
+        return self.db.get_list("projects", filter_q)
+
+    def create_project(self, project_info):
+        """
+        Create a project.
+
+        :param project: full project info.
+        :return: the internal id of the created project
+        :raises AuthconnOperationException: if project creation failed.
+        """
+        pid = self.db.create("projects", project_info)
+        return pid
+
+    def delete_project(self, project_id):
+        """
+        Delete a project.
+
+        :param project_id: project identifier.
+        :raises AuthconnOperationException: if project deletion failed.
+        """
+        filter_q = {BaseTopic.id_field("projects", project_id): project_id}
+        r = self.db.del_one("projects", filter_q)
+        return r
+
+    def update_project(self, project_id, project_info):
+        """
+        Change the name of a project
+
+        :param project_id: project to be changed
+        :param project_info: full project info
+        :return: None
+        :raises AuthconnOperationException: if project update failed.
+        """
+        self.db.set_one("projects", {BaseTopic.id_field("projects", project_id): project_id}, project_info)
index 9f5e02c..3aedfab 100644 (file)
@@ -44,8 +44,8 @@ from validation import is_valid_uuid
 
 
 class AuthconnKeystone(Authconn):
-    def __init__(self, config):
-        Authconn.__init__(self, config)
+    def __init__(self, config, db, token_cache):
+        Authconn.__init__(self, config, db, token_cache)
 
         self.logger = logging.getLogger("nbi.authenticator.keystone")
 
@@ -157,35 +157,6 @@ class AuthconnKeystone(Authconn):
             raise AuthException("Error during user authentication using Keystone: {}".format(e),
                                 http_code=HTTPStatus.UNAUTHORIZED)
 
-    # def authenticate_with_token(self, token, project=None):
-    #     """
-    #     Authenticate a user using a token. Can be used to revalidate the token
-    #     or to get a scoped token.
-    #
-    #     :param token: a valid token.
-    #     :param project: (optional) project for a scoped token.
-    #     :return: return a revalidated token, scoped if a project was passed or
-    #     the previous token was already scoped.
-    #     """
-    #     try:
-    #         token_info = self.keystone.tokens.validate(token=token)
-    #         projects = self.keystone.projects.list(user=token_info["user"]["id"])
-    #         project_names = [project.name for project in projects]
-    #
-    #         new_token = self.keystone.get_raw_token_from_identity_service(
-    #             auth_url=self.auth_url,
-    #             token=token,
-    #             project_name=project,
-    #             project_id=None,
-    #             user_domain_name=self.user_domain_name,
-    #             project_domain_name=self.project_domain_name)
-    #
-    #         return new_token["auth_token"], project_names
-    #     except ClientException as e:
-    #         # self.logger.exception("Error during user authentication using keystone. Method: bearer: {}".format(e))
-    #         raise AuthException("Error during user authentication using Keystone: {}".format(e),
-    #                             http_code=HTTPStatus.UNAUTHORIZED)
-
     def validate_token(self, token):
         """
         Check if the token is valid.
@@ -238,55 +209,20 @@ class AuthconnKeystone(Authconn):
             raise AuthException("Error during token revocation using Keystone: {}".format(e),
                                 http_code=HTTPStatus.UNAUTHORIZED)
 
-    def get_user_project_list(self, token):
-        """
-        Get all the projects associated with a user.
-
-        :param token: valid token
-        :return: list of projects
-        """
-        try:
-            token_info = self.keystone.tokens.validate(token=token)
-            projects = self.keystone.projects.list(user=token_info["user"]["id"])
-            project_names = [project.name for project in projects]
-
-            return project_names
-        except ClientException as e:
-            # self.logger.exception("Error during user project listing using keystone: {}".format(e))
-            raise AuthException("Error during user project listing using Keystone: {}".format(e),
-                                http_code=HTTPStatus.UNAUTHORIZED)
-
-    def get_user_role_list(self, token):
-        """
-        Get role list for a scoped project.
-
-        :param token: scoped token.
-        :return: returns the list of roles for the user in that project. If
-        the token is unscoped it returns None.
-        """
-        try:
-            token_info = self.keystone.tokens.validate(token=token)
-            roles_info = self.keystone.roles.list(user=token_info["user"]["id"], project=token_info["project"]["id"])
-
-            roles = [role.name for role in roles_info]
-
-            return roles
-        except ClientException as e:
-            # self.logger.exception("Error during user role listing using keystone: {}".format(e))
-            raise AuthException("Error during user role listing using Keystone: {}".format(e),
-                                http_code=HTTPStatus.UNAUTHORIZED)
-
-    def create_user(self, user, password):
+    def create_user(self, user_info):
         """
         Create a user.
 
-        :param user: username.
-        :param password: password.
+        :param user_info: full user info.
         :raises AuthconnOperationException: if user creation failed.
         :return: returns the id of the user in keystone.
         """
         try:
-            new_user = self.keystone.users.create(user, password=password, domain=self.user_domain_name)
+            new_user = self.keystone.users.create(user_info["username"], password=user_info["password"],
+                                                  domain=self.user_domain_name, _admin=user_info["_admin"])
+            if "project_role_mappings" in user_info.keys():
+                for mapping in user_info["project_role_mappings"]:
+                    self.assign_role_to_user(new_user.id, mapping["project"], mapping["role"])
             return {"username": new_user.name, "_id": new_user.id}
         except Conflict as e:
             # self.logger.exception("Error during user creation using keystone: {}".format(e))
@@ -295,28 +231,34 @@ class AuthconnKeystone(Authconn):
             # self.logger.exception("Error during user creation using keystone: {}".format(e))
             raise AuthconnOperationException("Error during user creation using Keystone: {}".format(e))
 
-    def update_user(self, user, new_name=None, new_password=None):
+    def update_user(self, user_info):
         """
         Change the user name and/or password.
 
-        :param user: username or user_id
-        :param new_name: new name
-        :param new_password: new password.
+        :param user_info: user info modifications
         :raises AuthconnOperationException: if change failed.
         """
         try:
+            user = user_info.get("_id") or user_info.get("username")
             if is_valid_uuid(user):
-                user_id = user
+                user_obj_list = [self.keystone.users.get(user)]
             else:
                 user_obj_list = self.keystone.users.list(name=user)
-                if not user_obj_list:
-                    raise AuthconnNotFoundException("User '{}' not found".format(user))
-                user_id = user_obj_list[0].id
-
-            self.keystone.users.update(user_id, password=new_password, name=new_name)
+            if not user_obj_list:
+                raise AuthconnNotFoundException("User '{}' not found".format(user))
+            user_obj = user_obj_list[0]
+            user_id = user_obj.id
+            if user_info.get("password") or user_info.get("username") \
+                    or user_info.get("add_project_role_mappings") or user_info.get("remove_project_role_mappings"):
+                self.keystone.users.update(user_id, password=user_info.get("password"), name=user_info.get("username"),
+                                           _admin={"created": user_obj._admin["created"], "modified": time.time()})
+            for mapping in user_info.get("remove_project_role_mappings", []):
+                self.remove_role_from_user(user_id, mapping["project"], mapping["role"])
+            for mapping in user_info.get("add_project_role_mappings", []):
+                self.assign_role_to_user(user_id, mapping["project"], mapping["role"])
         except ClientException as e:
             # self.logger.exception("Error during user password/name update using keystone: {}".format(e))
-            raise AuthconnOperationException("Error during user password/name update using Keystone: {}".format(e))
+            raise AuthconnOperationException("Error during user update using Keystone: {}".format(e))
 
     def delete_user(self, user_id):
         """
@@ -326,14 +268,9 @@ class AuthconnKeystone(Authconn):
         :raises AuthconnOperationException: if user deletion failed.
         """
         try:
-            # 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)
-
             result, detail = self.keystone.users.delete(user_id)
             if result.status_code != 204:
                 raise ClientException("error {} {}".format(result.status_code, detail))
-
             return True
         except ClientException as e:
             # self.logger.exception("Error during user deletion using keystone: {}".format(e))
@@ -354,7 +291,8 @@ class AuthconnKeystone(Authconn):
             users = [{
                 "username": user.name,
                 "_id": user.id,
-                "id": user.id
+                "id": user.id,
+                "_admin": user.to_dict().get("_admin", {})   # TODO: REVISE
             } for user in users if user.name != self.admin_username]
 
             if filter_q and filter_q.get("_id"):
@@ -399,7 +337,9 @@ class AuthconnKeystone(Authconn):
 
             roles = [{
                 "name": role.name,
-                "_id": role.id
+                "_id": role.id,
+                "_admin": role.to_dict().get("_admin", {}),
+                "permissions": role.to_dict().get("permissions", {})
             } for role in roles_list if role.name != "service"]
 
             if filter_q and filter_q.get("_id"):
@@ -411,15 +351,16 @@ class AuthconnKeystone(Authconn):
             raise AuthException("Error during user role listing using Keystone: {}".format(e),
                                 http_code=HTTPStatus.UNAUTHORIZED)
 
-    def create_role(self, role):
+    def create_role(self, role_info):
         """
         Create a role.
 
-        :param role: role name.
+        :param role_info: full role info.
         :raises AuthconnOperationException: if role creation failed.
         """
         try:
-            result = self.keystone.roles.create(role)
+            result = self.keystone.roles.create(role_info["name"], permissions=role_info.get("permissions"),
+                                                _admin=role_info.get("_admin"))
             return result.id
         except Conflict as ex:
             raise AuthconnConflictException(str(ex))
@@ -445,22 +386,21 @@ 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):
+    def update_role(self, role_info):
         """
         Change the name of a role
-        :param role: role  name or id to be changed
-        :param new_name: new name
+        :param role_info: full role info
         :return: None
         """
         try:
-            if is_valid_uuid(role):
-                role_id = role
-            else:
-                role_obj_list = self.keystone.roles.list(name=role)
+            rid = role_info["_id"]
+            if not is_valid_uuid(rid):   # Is this required?
+                role_obj_list = self.keystone.roles.list(name=rid)
                 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)
+                    raise AuthconnNotFoundException("Role '{}' not found".format(rid))
+                rid = role_obj_list[0].id
+            self.keystone.roles.update(rid, name=role_info["name"], permissions=role_info.get("permissions"),
+                                       _admin=role_info.get("_admin"))
         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))
@@ -480,7 +420,8 @@ class AuthconnKeystone(Authconn):
 
             projects = [{
                 "name": project.name,
-                "_id": project.id
+                "_id": project.id,
+                "_admin": project.to_dict().get("_admin", {})  # TODO: REVISE
             } for project in projects]
 
             if filter_q and filter_q.get("_id"):
@@ -493,16 +434,17 @@ class AuthconnKeystone(Authconn):
             raise AuthException("Error during user project listing using Keystone: {}".format(e),
                                 http_code=HTTPStatus.UNAUTHORIZED)
 
-    def create_project(self, project):
+    def create_project(self, project_info):
         """
         Create a project.
 
-        :param project: project name.
+        :param project_info: full project info.
         :return: the internal id of the created project
         :raises AuthconnOperationException: if project creation failed.
         """
         try:
-            result = self.keystone.projects.create(project, self.project_domain_name)
+            result = self.keystone.projects.create(project_info["name"], self.project_domain_name,
+                                                   _admin=project_info["_admin"])
             return result.id
         except ClientException as e:
             # self.logger.exception("Error during project creation using keystone: {}".format(e))
@@ -529,18 +471,18 @@ class AuthconnKeystone(Authconn):
             # self.logger.exception("Error during project deletion using keystone: {}".format(e))
             raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
 
-    def update_project(self, project_id, new_name):
+    def update_project(self, project_id, project_info):
         """
         Change the name of a project
         :param project_id: project to be changed
-        :param new_name: new name
+        :param project_info: full project info
         :return: None
         """
         try:
-            self.keystone.projects.update(project_id, name=new_name)
+            self.keystone.projects.update(project_id, name=project_info["name"], _admin=project_info["_admin"])
         except ClientException as e:
             # self.logger.exception("Error during project update using keystone: {}".format(e))
-            raise AuthconnOperationException("Error during project deletion using Keystone: {}".format(e))
+            raise AuthconnOperationException("Error during project update using Keystone: {}".format(e))
 
     def assign_role_to_user(self, user, project, role):
         """
index 20d54bb..4fb84a5 100644 (file)
@@ -61,8 +61,7 @@ class BaseTopic:
     alt_id_field = {
         "projects": "name",
         "users": "username",
-        "roles": "name",
-        "roles_operations": "name"
+        "roles": "name"
     }
 
     def __init__(self, db, fs, msg):
index 1f7f0f0..ccfd7d3 100644 (file)
@@ -24,7 +24,7 @@ from http import HTTPStatus
 from authconn_keystone import AuthconnKeystone
 from authconn_internal import AuthconnInternal
 from base_topic import EngineException, versiontuple
-from admin_topics import UserTopic, ProjectTopic, VimAccountTopic, WimAccountTopic, SdnTopic
+from admin_topics import 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
@@ -49,8 +49,8 @@ class Engine(object):
         "vim_accounts": VimAccountTopic,
         "wim_accounts": WimAccountTopic,
         "sdns": SdnTopic,
-        "users": UserTopic,
-        "projects": ProjectTopic,
+        "users": UserTopicAuth,   # Valid for both internal and keystone authentication backends
+        "projects": ProjectTopicAuth,   # Valid for both internal and keystone authentication backends
         "roles": RoleTopicAuth,   # Valid for both internal and keystone authentication backends
         "nsis": NsiTopic,
         "nsilcmops": NsiLcmOpTopic
@@ -118,9 +118,9 @@ class Engine(object):
                         config["message"]["driver"]))
             if not self.auth:
                 if config["authentication"]["backend"] == "keystone":
-                    self.auth = AuthconnKeystone(config["authentication"])
+                    self.auth = AuthconnKeystone(config["authentication"], self.db, None)
                 else:
-                    self.auth = AuthconnInternal(config["authentication"], self.db, dict())   # TO BE CONFIRMED
+                    self.auth = AuthconnInternal(config["authentication"], self.db, dict())
             if not self.operations:
                 if "resources_to_operations" in config["rbac"]:
                     resources_to_operations_file = config["rbac"]["resources_to_operations"]
@@ -145,11 +145,6 @@ class Engine(object):
                     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():
@@ -267,7 +262,7 @@ class Engine(object):
         :param session: contains the used login username and working project
         :param topic: it can be: users, projects, vnfds, nsds, ...
         :param _id: server id of the item
-        :return: operation id (None if there is not operation), raise exception if error or not found.
+        :return: dictionary with deleted item _id. It raises exception if not found.
         """
         if topic not in self.map_topic:
             raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR)
@@ -282,54 +277,13 @@ class Engine(object):
         :param _id: identifier to be updated
         :param indata: data to be inserted
         :param kwargs: used to override the indata descriptor
-        :return: operation id (None if there is not operation), raise exception if error or not found.
+        :return: dictionary with edited item _id, raise exception if not found.
         """
         if topic not in self.map_topic:
             raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR)
         with self.write_lock:
             return self.map_topic[topic].edit(session, _id, indata, kwargs)
 
-    def create_admin_project(self):
-        """
-        Creates a new project 'admin' into database if database is empty. Useful for initialization.
-        :return: _id identity of the inserted data, or None
-        """
-
-        projects = self.db.get_one("projects", fail_on_empty=False, fail_on_more=False)
-        if projects:
-            return None
-        project_desc = {"name": "admin"}
-        fake_session = {"project_id": "admin", "username": "admin", "admin": True, "force": True, "public": None}
-        rollback_list = []
-        _id = self.map_topic["projects"].new(rollback_list, fake_session, project_desc)
-        return _id
-
-    def create_admin_user(self):
-        """
-        Creates a new user admin/admin into database if database is empty. Useful for initialization
-        :return: _id identity of the inserted data, or None
-        """
-        users = self.db.get_one("users", fail_on_empty=False, fail_on_more=False)
-        if users:
-            return None
-        user_desc = {"username": "admin", "password": "admin", "projects": ["admin"]}
-        fake_session = {"project_id": "admin", "username": "admin", "admin": True, "force": True, "public": None}
-        rollback_list = []
-        _id = self.map_topic["users"].new(rollback_list, fake_session, user_desc)
-        return _id
-
-    def create_admin(self):
-        """
-        Creates new 'admin' user and project into database if database is empty. Useful for initialization.
-        :return: _id identity of the inserted data, or None
-        """
-        project_id = self.create_admin_project()
-        user_id = self.create_admin_user()
-        if project_id or user_id:
-            return {'project_id': project_id, 'user_id': user_id}
-        else:
-            return None
-
     def upgrade_db(self, current_version, target_version):
         if target_version not in self.map_target_version_to_int.keys():
             raise EngineException("Cannot upgrade to version '{}' with this version of code".format(target_version),
@@ -357,9 +311,9 @@ class Engine(object):
             current_version = "1.0"
             
         if current_version in ("1.0", "1.1") and target_version_int >= self.map_target_version_to_int["1.2"]:
-            table = "roles_operations" if self.config['authentication']['backend'] == "keystone" else "roles"
-            self.db.del_list(table)
-            
+            if self.config['authentication']['backend'] == "internal":
+                self.db.del_list("roles")
+
             version_data = {
                 "_id": "version",
                 "version_int": 1002,
@@ -391,8 +345,4 @@ class Engine(object):
         if db_version != target_version:
             self.upgrade_db(db_version, target_version)
 
-        # create admin project&user if they don't exist
-        if self.config['authentication']['backend'] == 'internal' or not self.auth:
-            self.create_admin()
-        
         return
index 1b3a6ce..33147b7 100644 (file)
@@ -24,7 +24,7 @@ import logging.handlers
 import getopt
 import sys
 
-from authconn import AuthException
+from authconn import AuthException, AuthconnException
 from auth import Authenticator
 from engine import Engine, EngineException
 from subscriptions import SubscriptionThread
@@ -598,7 +598,7 @@ class Server(object):
         try:
             if cherrypy.request.method == "GET":
                 token_info = self.authenticator.authorize()
-                outdata = "Index page"
+                outdata = token_info   # Home page
             else:
                 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
                                          "Method {} not allowed for tokens".format(cherrypy.request.method))
@@ -1110,7 +1110,7 @@ class Server(object):
             return self._format_out(outdata, token_info, _format)
         except Exception as e:
             if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
-                              ValidationError)):
+                              ValidationError, AuthconnException)):
                 http_code_value = cherrypy.response.status = e.http_code.value
                 http_code_name = e.http_code.name
                 cherrypy.log("Exception {}".format(e))
@@ -1255,7 +1255,10 @@ def _start_service():
     try:
         with open("{}/version".format(engine_config["/static"]['tools.staticdir.dir'])) as version_file:
             version_data = version_file.read()
-            cherrypy.log.error("Starting OSM NBI Version: {}".format(version_data.replace("\n", " ")))
+            version = version_data.replace("\n", " ")
+            backend = engine_config["authentication"]["backend"]
+            cherrypy.log.error("Starting OSM NBI Version {} with {} authentication backend"
+                               .format(version, backend))
     except Exception:
         pass