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 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>"
 
 
 __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"):
         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
 
             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
 
         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"]:
             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)
 
             # 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)
         """
         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):
 
     @staticmethod
     def format_on_show(content):
@@ -501,14 +479,14 @@ class UserTopicAuth(UserTopic):
         """
         project_role_mappings = []
 
         """
         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
         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
         :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)
         """
         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"])
             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)
 
             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)
 
         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)
 
             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
                 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)
 
                 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 = []
 
             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_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"):
 
             # 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"]):
                     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:
                             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 \
                 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)
 
                         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)
 
         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
         :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:
         if not dry_run:
-            v = self.auth.delete_user(_id)
+            v = self.auth.delete_user(uid)
             return v
         return None
 
             return v
         return None
 
@@ -739,11 +729,14 @@ class ProjectTopicAuth(ProjectTopic):
         """
 
         project_name = edit_content.get("name")
         """
 
         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):
             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)
 
                                       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)
             # 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
         """
         :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)
 
             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.
     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
         :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)
         """
         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"])
             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)
             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)
 
         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
         :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:
         if not dry_run:
-            v = self.auth.delete_project(_id)
+            v = self.auth.delete_project(pid)
             return v
         return None
 
             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)
             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:
 
             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)
 
         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
         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):
 
     @staticmethod
     def validate_role_definition(operations, role_definitions):
@@ -946,8 +957,10 @@ class RoleTopicAuth(BaseTopic):
         :return: None or raises EngineException
         """
         # check name not exists
         :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):
         """
 
     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"]
         # 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):
                 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
         """
         :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
 
     @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
 
         """
         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
         """
         :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"] = {}
 
         if "permissions" not in final_content:
             final_content["permissions"] = {}
@@ -1030,62 +1050,31 @@ class RoleTopicAuth(BaseTopic):
             final_content["permissions"]["admin"] = False
         return None
 
             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):
         """
 
     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
         :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)
         """
         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"])
             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)
             # self._send_msg("create", content)
-            return _id
+            return rid, None
         except ValidationError as e:
             raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
 
         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, ...
         """
         :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 = {"_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:
         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
 
             return v
         return None
 
@@ -1145,6 +1141,15 @@ class RoleTopicAuth(BaseTopic):
         :param content:
         :return: _id: identity of the inserted data.
         """
         :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 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:
 
 
 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.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")
         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":
                                         .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()
                 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_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:
             # 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)
 
         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
     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.
         """
 
         :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.
 
         # 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)
 
             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,
                 }
 
                     "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"]))
 
                 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()
 
 
         self.load_operation_to_allowed_roles()
 
@@ -243,10 +275,13 @@ class Authenticator:
         """
 
         permissions = {oper: [] for oper in self.role_permissions}
         """
 
         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:
 
         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]
             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
 __date__ = "$27-jul-2018 23:59:59$"
 
 from http import HTTPStatus
+from base_topic import BaseTopic
 
 
 class AuthException(Exception):
 
 
 class AuthException(Exception):
@@ -108,7 +109,7 @@ class Authconn:
     Each Auth backend connector plugin must be a subclass of
     Authconn class.
     """
     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.
 
         """
         Constructor of the Authconn class.
 
@@ -136,18 +137,6 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         """
         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.
     def validate_token(self, token):
         """
         Check if the token is valid.
@@ -166,43 +155,21 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         """
         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.
 
         """
         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")
 
         :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.
 
         """
         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")
 
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
@@ -223,11 +190,21 @@ class Authconn:
         :return: returns a list of users.
         """
 
         :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.
 
         """
         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")
         :raises AuthconnOperationException: if role creation failed.
         """
         raise AuthconnNotImplementedException("Should have implemented this")
@@ -250,20 +227,29 @@ class Authconn:
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
         """
         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")
 
         :return: None
         """
         raise AuthconnNotImplementedException("Should have implemented this")
 
-    def create_project(self, project):
+    def create_project(self, project_info):
         """
         Create a project.
 
         """
         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.
         """
         :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")
 
         """
         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 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")
         :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$"
 
 __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
 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
 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):
 
 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")
 
 
         self.logger = logging.getLogger("nbi.authenticator.internal")
 
@@ -56,48 +57,6 @@ class AuthconnInternal(Authconn):
         self.auth = None
         self.sess = None
 
         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.
     def validate_token(self, token):
         """
         Check if the token is valid.
@@ -190,100 +149,105 @@ class AuthconnInternal(Authconn):
         now = time()
         user_content = None
 
         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:
             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.
         """
         """
         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.
 
         """
         Create a role.
 
-        :param role: role name.
+        :param role_info: full role info.
+        :return: returns the role id.
         :raises AuthconnOperationException: if role creation failed.
         """
         :raises AuthconnOperationException: if role creation failed.
         """
-        # try:
         # TODO: Check that role name does not exist ?
         # 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):
         """
 
     def delete_role(self, role_id):
         """
@@ -292,8 +256,162 @@ class AuthconnInternal(Authconn):
         :param role_id: role identifier.
         :raises AuthconnOperationException: if role deletion failed.
         """
         :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
         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):
 
 
 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")
 
 
         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)
 
             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.
     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)
 
             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.
 
         """
         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:
         :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))
             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))
 
             # 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.
 
         """
         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:
         :raises AuthconnOperationException: if change failed.
         """
         try:
+            user = user_info.get("_id") or user_info.get("username")
             if is_valid_uuid(user):
             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)
             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))
         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):
         """
 
     def delete_user(self, user_id):
         """
@@ -326,14 +268,9 @@ class AuthconnKeystone(Authconn):
         :raises AuthconnOperationException: if user deletion failed.
         """
         try:
         :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))
             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))
             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,
             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"):
             } 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,
 
             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"):
             } 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)
 
             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.
 
         """
         Create a role.
 
-        :param role: role name.
+        :param role_info: full role info.
         :raises AuthconnOperationException: if role creation failed.
         """
         try:
         :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))
             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))
 
             # 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
         """
         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:
         :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:
                 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))
         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,
 
             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"):
             } 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)
 
             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.
 
         """
         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:
         :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))
             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))
 
             # 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
         """
         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:
         :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))
         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):
         """
 
     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",
     alt_id_field = {
         "projects": "name",
         "users": "username",
-        "roles": "name",
-        "roles_operations": "name"
+        "roles": "name"
     }
 
     def __init__(self, db, fs, msg):
     }
 
     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 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
 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,
         "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
         "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":
                         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:
                 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"]
             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 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():
@@ -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
         :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)
         """
         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
         :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)
 
         """
         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),
     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"]:
             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,
             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)
 
         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
         return
index 1b3a6ce..33147b7 100644 (file)
@@ -24,7 +24,7 @@ import logging.handlers
 import getopt
 import sys
 
 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
 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()
         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))
             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,
             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))
                 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()
     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
 
     except Exception:
         pass