bug 710. Fix session used to delete ns, nsi when terminated
[osm/NBI.git] / osm_nbi / auth.py
index 7d586ad..dda1d22 100644 (file)
@@ -1,17 +1,23 @@
 # -*- coding: utf-8 -*-
 
 # -*- coding: utf-8 -*-
 
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
+# Copyright 2018 Whitestack, LLC
+# Copyright 2018 Telefonica S.A.
 #
 #
-#    http://www.apache.org/licenses/LICENSE-2.0
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 #
 # Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-# implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact: esousa@whitestack.com or alfonso.tiernosepulveda@telefonica.com
+##
 
 
 """
 
 
 """
@@ -19,7 +25,6 @@ Authenticator is responsible for authenticating the users,
 create the tokens unscoped and scoped, retrieve the role
 list inside the projects that they are inserted
 """
 create the tokens unscoped and scoped, retrieve the role
 list inside the projects that they are inserted
 """
-from os import path
 
 __author__ = "Eduardo Sousa <esousa@whitestack.com>; Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 __date__ = "$27-jul-2018 23:59:59$"
 
 __author__ = "Eduardo Sousa <esousa@whitestack.com>; Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 __date__ = "$27-jul-2018 23:59:59$"
@@ -34,7 +39,8 @@ from hashlib import sha256
 from http import HTTPStatus
 from random import choice as random_choice
 from time import time
 from http import HTTPStatus
 from random import choice as random_choice
 from time import time
-from uuid import uuid4
+from os import path
+from base_topic import BaseTopic    # To allow project names in project_id
 
 from authconn import AuthException
 from authconn_keystone import AuthconnKeystone
 
 from authconn import AuthException
 from authconn_keystone import AuthconnKeystone
@@ -49,6 +55,7 @@ class Authenticator:
     Authorization. Initially it should support Openstack Keystone as a
     backend through a plugin model where more backends can be added and a
     RBAC model to manage permissions on operations.
     Authorization. Initially it should support Openstack Keystone as a
     backend through a plugin model where more backends can be added and a
     RBAC model to manage permissions on operations.
+    This class must be threading safe
     """
 
     periodin_db_pruning = 60 * 30  # for the internal backend only. every 30 minutes expired tokens will be pruned
     """
 
     periodin_db_pruning = 60 * 30  # for the internal backend only. every 30 minutes expired tokens will be pruned
@@ -102,8 +109,11 @@ class Authenticator:
                 if "resources_to_operations" in config["rbac"]:
                     self.resources_to_operations_file = config["rbac"]["resources_to_operations"]
                 else:
                 if "resources_to_operations" in config["rbac"]:
                     self.resources_to_operations_file = config["rbac"]["resources_to_operations"]
                 else:
-                    for config_file in (__file__[:__file__.rfind("auth.py")] + "resources_to_operations.yml",
-                                        "./resources_to_operations.yml"):
+                    possible_paths = (
+                        __file__[:__file__.rfind("auth.py")] + "resources_to_operations.yml",
+                        "./resources_to_operations.yml"
+                    )
+                    for config_file in possible_paths:
                         if path.isfile(config_file):
                             self.resources_to_operations_file = config_file
                             break
                         if path.isfile(config_file):
                             self.resources_to_operations_file = config_file
                             break
@@ -113,8 +123,11 @@ class Authenticator:
                 if "roles_to_operations" in config["rbac"]:
                     self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
                 else:
                 if "roles_to_operations" in config["rbac"]:
                     self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
                 else:
-                    for config_file in (__file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml",
-                                        "./roles_to_operations.yml"):
+                    possible_paths = (
+                        __file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml",
+                        "./roles_to_operations.yml"
+                    )
+                    for config_file in possible_paths:
                         if path.isfile(config_file):
                             self.roles_to_operations_file = config_file
                             break
                         if path.isfile(config_file):
                             self.roles_to_operations_file = config_file
                             break
@@ -141,6 +154,9 @@ class Authenticator:
         # Always reads operation to resource mapping from file (this is static, no need to store it in MongoDB)
         # Operations encoding: "<METHOD> <URL>"
         # Note: it is faster to rewrite the value than to check if it is already there or not
         # Always reads operation to resource mapping from file (this is static, no need to store it in MongoDB)
         # Operations encoding: "<METHOD> <URL>"
         # Note: it is faster to rewrite the value than to check if it is already there or not
+        if self.config["authentication"]["backend"] == "internal":
+            return
+
         operations = []
         with open(self.resources_to_operations_file, "r") as stream:
             resources_to_operations_yaml = yaml.load(stream)
         operations = []
         with open(self.resources_to_operations_file, "r") as stream:
             resources_to_operations_yaml = yaml.load(stream)
@@ -168,7 +184,7 @@ class Authenticator:
                                         .format(role_with_operations["role"]))
                     continue
 
                                         .format(role_with_operations["role"]))
                     continue
 
-                operations = {}
+                role_ops = {}
                 root = None
 
                 if not role_with_operations["operations"]:
                 root = None
 
                 if not role_with_operations["operations"]:
@@ -189,8 +205,8 @@ class Authenticator:
                         continue
 
                     operation_key = operation.replace(".", ":")
                         continue
 
                     operation_key = operation.replace(".", ":")
-                    if operation_key not in operations.keys():
-                        operations[operation_key] = is_allowed
+                    if operation_key not in role_ops.keys():
+                        role_ops[operation_key] = is_allowed
                     else:
                         self.logger.info("In role {0}, the operation {1} with the value {2} was discarded due to "
                                          "repetition.".format(role_with_operations["role"], operation, is_allowed))
                     else:
                         self.logger.info("In role {0}, the operation {1} with the value {2} was discarded due to "
                                          "repetition.".format(role_with_operations["role"], operation, is_allowed))
@@ -202,28 +218,29 @@ class Authenticator:
 
                 now = time()
                 operation_to_roles_item = {
 
                 now = time()
                 operation_to_roles_item = {
-                    "_id": str(uuid4()),
                     "_admin": {
                         "created": now,
                         "modified": now,
                     },
                     "_admin": {
                         "created": now,
                         "modified": now,
                     },
-                    "role": role_with_operations["role"],
+                    "name": role_with_operations["role"],
                     "root": root
                 }
 
                     "root": root
                 }
 
-                for operation, value in operations.items():
+                for operation, value in role_ops.items():
                     operation_to_roles_item[operation] = value
 
                     operation_to_roles_item[operation] = value
 
+                if self.config["authentication"]["backend"] != "internal" and \
+                        role_with_operations["role"] != "anonymous":
+                    keystone_id = self.backend.create_role(role_with_operations["role"])
+                    operation_to_roles_item["_id"] = keystone_id["_id"]
+
                 self.db.create("roles_operations", operation_to_roles_item)
 
         permissions = {oper: [] for oper in operations}
         records = self.db.get_list("roles_operations")
 
                 self.db.create("roles_operations", operation_to_roles_item)
 
         permissions = {oper: [] for oper in operations}
         records = self.db.get_list("roles_operations")
 
-        ignore_fields = ["_id", "_admin", "role", "root"]
-        roles = []
+        ignore_fields = ["_id", "_admin", "name", "root"]
         for record in records:
         for record in records:
-
-            roles.append(record["role"])
             record_permissions = {oper: record["root"] for oper in operations}
             operations_joined = [(oper, value) for oper, value in record.items() if oper not in ignore_fields]
             operations_joined.sort(key=lambda x: x[0].count(":"))
             record_permissions = {oper: record["root"] for oper in operations}
             operations_joined = [(oper, value) for oper, value in record.items() if oper not in ignore_fields]
             operations_joined.sort(key=lambda x: x[0].count(":"))
@@ -237,17 +254,12 @@ class Authenticator:
             allowed_operations = [k for k, v in record_permissions.items() if v is True]
 
             for allowed_op in allowed_operations:
             allowed_operations = [k for k, v in record_permissions.items() if v is True]
 
             for allowed_op in allowed_operations:
-                permissions[allowed_op].append(record["role"])
+                permissions[allowed_op].append(record["name"])
 
         for oper, role_list in permissions.items():
             self.operation_to_allowed_roles[oper] = role_list
 
         if self.config["authentication"]["backend"] != "internal":
 
         for oper, role_list in permissions.items():
             self.operation_to_allowed_roles[oper] = role_list
 
         if self.config["authentication"]["backend"] != "internal":
-            for role in roles:
-                if role == "anonymous":
-                    continue
-                self.backend.create_role(role)
-
             self.backend.assign_role_to_user("admin", "admin", "system_admin")
 
     def authorize(self):
             self.backend.assign_role_to_user("admin", "admin", "system_admin")
 
     def authorize(self):
@@ -396,7 +408,7 @@ class Authenticator:
 
         operation = self.resources_to_operations_mapping[key]
         roles_required = self.operation_to_allowed_roles[operation]
 
         operation = self.resources_to_operations_mapping[key]
         roles_required = self.operation_to_allowed_roles[operation]
-        roles_allowed = self.backend.get_role_list(session["id"])
+        roles_allowed = self.backend.get_user_role_list(session["id"])
 
         if "anonymous" in roles_required:
             return
 
         if "anonymous" in roles_required:
             return
@@ -407,6 +419,9 @@ class Authenticator:
 
         raise AuthException("Access denied: lack of permissions.")
 
 
         raise AuthException("Access denied: lack of permissions.")
 
+    def get_user_list(self):
+        return self.backend.get_user_list()
+
     def _normalize_url(self, url, method):
         # Removing query strings
         normalized_url = url if '?' not in url else url[:url.find("?")]
     def _normalize_url(self, url, method):
         # Removing query strings
         normalized_url = url if '?' not in url else url[:url.find("?")]
@@ -420,7 +435,9 @@ class Authenticator:
             tmp_keys = []
             for tmp_key in filtered_keys:
                 splitted = tmp_key.split()[1].split("/")
             tmp_keys = []
             for tmp_key in filtered_keys:
                 splitted = tmp_key.split()[1].split("/")
-                if "<" in splitted[idx] and ">" in splitted[idx]:
+                if idx >= len(splitted):
+                    continue
+                elif "<" in splitted[idx] and ">" in splitted[idx]:
                     if splitted[idx] == "<artifactPath>":
                         tmp_keys.append(tmp_key)
                         continue
                     if splitted[idx] == "<artifactPath>":
                         tmp_keys.append(tmp_key)
                         continue
@@ -464,7 +481,8 @@ class Authenticator:
             now = time()
             session = self.tokens_cache.get(token_id)
             if session and session["expires"] < now:
             now = time()
             session = self.tokens_cache.get(token_id)
             if session and session["expires"] < now:
-                del self.tokens_cache[token_id]
+                # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del
+                self.tokens_cache.pop(token_id, None)
                 session = None
             if session:
                 return session
                 session = None
             if session:
                 return session
@@ -485,7 +503,7 @@ class Authenticator:
             if self.config["global"].get("test.user_not_authorized"):
                 return {"id": "fake-token-id-for-test",
                         "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
             if self.config["global"].get("test.user_not_authorized"):
                 return {"id": "fake-token-id-for-test",
                         "project_id": self.config["global"].get("test.project_not_authorized", "admin"),
-                        "username": self.config["global"]["test.user_not_authorized"]}
+                        "username": self.config["global"]["test.user_not_authorized"], "admin": True}
             else:
                 raise
 
             else:
                 raise
 
@@ -516,17 +534,21 @@ class Authenticator:
 
         token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
                            for _ in range(0, 32))
 
         token_id = ''.join(random_choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
                            for _ in range(0, 32))
-        if indata.get("project_id"):
-            project_id = indata.get("project_id")
-            if project_id not in user_content["projects"]:
-                raise AuthException("project {} not allowed for this user"
-                                    .format(project_id), http_code=HTTPStatus.UNAUTHORIZED)
+        project_id = indata.get("project_id")
+        if project_id:
+            if project_id != "admin":
+                # To allow project names in project_id
+                proj = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id})
+                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_id), http_code=HTTPStatus.UNAUTHORIZED)
         else:
             project_id = user_content["projects"][0]
         if project_id == "admin":
             session_admin = True
         else:
         else:
             project_id = user_content["projects"][0]
         if project_id == "admin":
             session_admin = True
         else:
-            project = self.db.get_one("projects", {"_id": project_id})
+            # To allow project names in project_id
+            project = self.db.get_one("projects", {BaseTopic.id_field("projects", project_id): project_id})
             session_admin = project.get("admin", False)
         new_session = {"issued_at": now, "expires": now + 3600,
                        "_id": token_id, "id": token_id, "project_id": project_id, "username": user_content["username"],
             session_admin = project.get("admin", False)
         new_session = {"issued_at": now, "expires": now + 3600,
                        "_id": token_id, "id": token_id, "project_id": project_id, "username": user_content["username"],