Fix bug 724
[osm/NBI.git] / osm_nbi / auth.py
index 7d586ad..bb65738 100644 (file)
@@ -1,17 +1,23 @@
 # -*- 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
-# 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
 """
-from os import path
 
 __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 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
@@ -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.
+    This class must be threading safe
     """
 
     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:
-                    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
@@ -113,8 +123,11 @@ class Authenticator:
                 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
@@ -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
+        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)
@@ -168,7 +184,7 @@ class Authenticator:
                                         .format(role_with_operations["role"]))
                     continue
 
-                operations = {}
+                role_ops = {}
                 root = None
 
                 if not role_with_operations["operations"]:
@@ -189,8 +205,8 @@ class Authenticator:
                         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))
@@ -202,28 +218,34 @@ class Authenticator:
 
                 now = time()
                 operation_to_roles_item = {
-                    "_id": str(uuid4()),
                     "_admin": {
                         "created": now,
                         "modified": now,
                     },
-                    "role": role_with_operations["role"],
+                    "name": role_with_operations["role"],
                     "root": root
                 }
 
-                for operation, value in operations.items():
+                for operation, value in role_ops.items():
                     operation_to_roles_item[operation] = value
 
+                if self.config["authentication"]["backend"] != "internal" and \
+                        role_with_operations["role"] != "anonymous":
+                    keystone_id = [role for role in self.backend.get_role_list() 
+                                   if role["name"] == role_with_operations["role"]]
+                    if keystone_id:
+                        keystone_id = keystone_id[0]
+                    else:
+                        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")
 
-        ignore_fields = ["_id", "_admin", "role", "root"]
-        roles = []
+        ignore_fields = ["_id", "_admin", "name", "root"]
         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(":"))
@@ -237,17 +259,12 @@ class Authenticator:
             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 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):
@@ -396,7 +413,7 @@ class Authenticator:
 
         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
@@ -407,6 +424,9 @@ class Authenticator:
 
         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("?")]
@@ -420,7 +440,9 @@ class Authenticator:
             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
@@ -464,7 +486,8 @@ class Authenticator:
             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
@@ -485,7 +508,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"),
-                        "username": self.config["global"]["test.user_not_authorized"]}
+                        "username": self.config["global"]["test.user_not_authorized"], "admin": True}
             else:
                 raise
 
@@ -516,17 +539,21 @@ class Authenticator:
 
         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:
-            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"],