Reformat NBI to standardized format
[osm/NBI.git] / osm_nbi / auth.py
index 94eb1e9..7c29f84 100644 (file)
@@ -34,19 +34,20 @@ import logging
 import yaml
 from base64 import standard_b64decode
 from copy import deepcopy
 import yaml
 from base64 import standard_b64decode
 from copy import deepcopy
+
 # from functools import reduce
 from http import HTTPStatus
 from time import time
 from os import path
 
 # from functools import reduce
 from http import HTTPStatus
 from time import time
 from os import path
 
-from authconn import AuthException, AuthExceptionUnauthorized
-from authconn_keystone import AuthconnKeystone
-from authconn_internal import AuthconnInternal   # Comment out for testing&debugging, uncomment when ready
-from osm_common import dbmongo
-from osm_common import dbmemory
+from osm_nbi.authconn import AuthException, AuthconnException, AuthExceptionUnauthorized
+from osm_nbi.authconn_keystone import AuthconnKeystone
+from osm_nbi.authconn_internal import AuthconnInternal
+from osm_nbi.authconn_tacacs import AuthconnTacacs
+from osm_common import dbmemory, dbmongo, msglocal, msgkafka
 from osm_common.dbbase import DbException
 from osm_common.dbbase import DbException
+from osm_nbi.validation import is_valid_uuid
 from itertools import chain
 from itertools import chain
-
 from uuid import uuid4
 
 
 from uuid import uuid4
 
 
@@ -59,7 +60,10 @@ class Authenticator:
     This class must be threading safe
     """
 
     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
+    token_limit = 500  # when reached, the token cache will be cleared
 
     def __init__(self, valid_methods, valid_query_string):
         """
 
     def __init__(self, valid_methods, valid_query_string):
         """
@@ -69,8 +73,11 @@ class Authenticator:
         self.backend = None
         self.config = None
         self.db = None
         self.backend = None
         self.config = None
         self.db = None
+        self.msg = None
         self.tokens_cache = dict()
         self.tokens_cache = dict()
-        self.next_db_prune_time = 0  # time when next cleaning of expired tokens must be done
+        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.resources_to_operations_mapping = {}
         self.roles_to_operations_file = None
         # self.roles_to_operations_table = None
         self.resources_to_operations_mapping = {}
@@ -79,6 +86,8 @@ class Authenticator:
         self.role_permissions = []
         self.valid_methods = valid_methods
         self.valid_query_string = valid_query_string
         self.role_permissions = []
         self.valid_methods = valid_methods
         self.valid_query_string = valid_query_string
+        self.system_admin_role_id = None  # system_role id
+        self.test_project_id = None  # test_project_id
 
     def start(self, config):
         """
 
     def start(self, config):
         """
@@ -99,44 +108,79 @@ class Authenticator:
                     self.db = dbmemory.DbMemory()
                     self.db.db_connect(config["database"])
                 else:
                     self.db = dbmemory.DbMemory()
                     self.db.db_connect(config["database"])
                 else:
-                    raise AuthException("Invalid configuration param '{}' at '[database]':'driver'"
-                                        .format(config["database"]["driver"]))
+                    raise AuthException(
+                        "Invalid configuration param '{}' at '[database]':'driver'".format(
+                            config["database"]["driver"]
+                        )
+                    )
+            if not self.msg:
+                if config["message"]["driver"] == "local":
+                    self.msg = msglocal.MsgLocal()
+                    self.msg.connect(config["message"])
+                elif config["message"]["driver"] == "kafka":
+                    self.msg = msgkafka.MsgKafka()
+                    self.msg.connect(config["message"])
+                else:
+                    raise AuthException(
+                        "Invalid configuration param '{}' at '[message]':'driver'".format(
+                            config["message"]["driver"]
+                        )
+                    )
             if not self.backend:
                 if config["authentication"]["backend"] == "keystone":
             if not self.backend:
                 if config["authentication"]["backend"] == "keystone":
-                    self.backend = AuthconnKeystone(self.config["authentication"], self.db, self.tokens_cache)
+                    self.backend = AuthconnKeystone(
+                        self.config["authentication"], self.db, self.role_permissions
+                    )
                 elif config["authentication"]["backend"] == "internal":
                 elif config["authentication"]["backend"] == "internal":
-                    self.backend = AuthconnInternal(self.config["authentication"], self.db, self.tokens_cache)
-                    self._internal_tokens_prune()
+                    self.backend = AuthconnInternal(
+                        self.config["authentication"], self.db, self.role_permissions
+                    )
+                    self._internal_tokens_prune("tokens")
+                elif config["authentication"]["backend"] == "tacacs":
+                    self.backend = AuthconnTacacs(
+                        self.config["authentication"], self.db, self.role_permissions
+                    )
+                    self._internal_tokens_prune("tokens_tacacs")
                 else:
                 else:
-                    raise AuthException("Unknown authentication backend: {}"
-                                        .format(config["authentication"]["backend"]))
+                    raise AuthException(
+                        "Unknown authentication backend: {}".format(
+                            config["authentication"]["backend"]
+                        )
+                    )
 
             if not self.roles_to_operations_file:
                 if "roles_to_operations" in config["rbac"]:
 
             if not self.roles_to_operations_file:
                 if "roles_to_operations" in config["rbac"]:
-                    self.roles_to_operations_file = config["rbac"]["roles_to_operations"]
+                    self.roles_to_operations_file = config["rbac"][
+                        "roles_to_operations"
+                    ]
                 else:
                     possible_paths = (
                 else:
                     possible_paths = (
-                        __file__[:__file__.rfind("auth.py")] + "roles_to_operations.yml",
-                        "./roles_to_operations.yml"
+                        __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 not self.roles_to_operations_file:
                     )
                     for config_file in possible_paths:
                         if path.isfile(config_file):
                             self.roles_to_operations_file = config_file
                             break
                 if not self.roles_to_operations_file:
-                    raise AuthException("Invalid permission configuration: roles_to_operations file missing")
+                    raise AuthException(
+                        "Invalid permission configuration: roles_to_operations file missing"
+                    )
 
             # load role_permissions
             def load_role_permissions(method_dict):
                 for k in method_dict:
                     if k == "ROLE_PERMISSION":
 
             # load role_permissions
             def load_role_permissions(method_dict):
                 for k in method_dict:
                     if k == "ROLE_PERMISSION":
-                        for method in chain(method_dict.get("METHODS", ()), method_dict.get("TODO", ())):
+                        for method in chain(
+                            method_dict.get("METHODS", ()), method_dict.get("TODO", ())
+                        ):
                             permission = method_dict["ROLE_PERMISSION"] + method.lower()
                             if permission not in self.role_permissions:
                                 self.role_permissions.append(permission)
                     elif k in ("TODO", "METHODS"):
                         continue
                             permission = method_dict["ROLE_PERMISSION"] + method.lower()
                             if permission not in self.role_permissions:
                                 self.role_permissions.append(permission)
                     elif k in ("TODO", "METHODS"):
                         continue
-                    else:
+                    elif method_dict[k]:
                         load_role_permissions(method_dict[k])
 
             load_role_permissions(self.valid_methods)
                         load_role_permissions(method_dict[k])
 
             load_role_permissions(self.valid_methods)
@@ -146,6 +190,21 @@ class Authenticator:
                     if permission not in self.role_permissions:
                         self.role_permissions.append(permission)
 
                     if permission not in self.role_permissions:
                         self.role_permissions.append(permission)
 
+            # get ids of role system_admin and test project
+            role_system_admin = self.db.get_one(
+                "roles", {"name": "system_admin"}, fail_on_empty=False
+            )
+            if role_system_admin:
+                self.system_admin_role_id = role_system_admin["_id"]
+            test_project_name = self.config["authentication"].get(
+                "project_not_authorized", "admin"
+            )
+            test_project = self.db.get_one(
+                "projects", {"name": test_project_name}, fail_on_empty=False
+            )
+            if test_project:
+                self.test_project_id = test_project["_id"]
+
         except Exception as e:
             raise AuthException(str(e))
 
         except Exception as e:
             raise AuthException(str(e))
 
@@ -171,7 +230,9 @@ class Authenticator:
         project_desc["_id"] = str(uuid4())
         project_desc["_admin"] = {"created": now, "modified": now}
         pid = self.backend.create_project(project_desc)
         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"]))
+        self.logger.info(
+            "Project '{}' created at database".format(project_desc["name"])
+        )
         return pid
 
     def create_admin_user(self, project_id):
         return pid
 
     def create_admin_user(self, project_id):
@@ -185,7 +246,11 @@ class Authenticator:
             return None
         # user_desc = {"username": "admin", "password": "admin", "projects": [project_id]}
         now = time()
             return None
         # user_desc = {"username": "admin", "password": "admin", "projects": [project_id]}
         now = time()
-        user_desc = {"username": "admin", "password": "admin", "_admin": {"created": now, "modified": now}}
+        user_desc = {
+            "username": "admin",
+            "password": "admin",
+            "_admin": {"created": now, "modified": now},
+        }
         if project_id:
             pid = project_id
         else:
         if project_id:
             pid = project_id
         else:
@@ -195,12 +260,14 @@ class Authenticator:
         # 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:
         # 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"]}]
+            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
 
         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'):
+    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
         and insert the predefined mappings between roles and permissions.
         """
         Check if the database has been initialized, with at least one user. If not, create the required tables
         and insert the predefined mappings between roles and permissions.
@@ -211,10 +278,12 @@ class Authenticator:
 
         records = self.backend.get_role_list()
 
 
         records = self.backend.get_role_list()
 
-        # Loading permissions to MongoDB if there is not any permission.
-        if not records or (len(records) == 1 and records[0]["name"] == "admin"):
+        # Loading permissions to AUTH. At lease system_admin must be present.
+        if not records or not next(
+            (r for r in records if r["name"] == "system_admin"), None
+        ):
             with open(self.roles_to_operations_file, "r") as stream:
             with open(self.roles_to_operations_file, "r") as stream:
-                roles_to_operations_yaml = yaml.load(stream)
+                roles_to_operations_yaml = yaml.load(stream, Loader=yaml.Loader)
 
             role_names = []
             for role_with_operations in roles_to_operations_yaml["roles"]:
 
             role_names = []
             for role_with_operations in roles_to_operations_yaml["roles"]:
@@ -222,23 +291,36 @@ class Authenticator:
                 if role_with_operations["name"] not in role_names:
                     role_names.append(role_with_operations["name"])
                 else:
                 if role_with_operations["name"] not in role_names:
                     role_names.append(role_with_operations["name"])
                 else:
-                    raise AuthException("Duplicated role name '{}' at file '{}''"
-                                        .format(role_with_operations["name"], self.roles_to_operations_file))
+                    raise AuthException(
+                        "Duplicated role name '{}' at file '{}''".format(
+                            role_with_operations["name"], self.roles_to_operations_file
+                        )
+                    )
 
                 if not role_with_operations["permissions"]:
                     continue
 
 
                 if not role_with_operations["permissions"]:
                     continue
 
-                for permission, is_allowed in role_with_operations["permissions"].items():
+                for permission, is_allowed in role_with_operations[
+                    "permissions"
+                ].items():
                     if not isinstance(is_allowed, bool):
                     if not isinstance(is_allowed, bool):
-                        raise AuthException("Invalid value for permission '{}' at role '{}'; at file '{}'"
-                                            .format(permission, role_with_operations["name"],
-                                                    self.roles_to_operations_file))
-
-                    # TODO chek permission is ok
+                        raise AuthException(
+                            "Invalid value for permission '{}' at role '{}'; at file '{}'".format(
+                                permission,
+                                role_with_operations["name"],
+                                self.roles_to_operations_file,
+                            )
+                        )
+
+                    # TODO check permission is ok
                     if permission[-1] == ":":
                     if permission[-1] == ":":
-                        raise AuthException("Invalid permission '{}' terminated in ':' for role '{}'; at file {}"
-                                            .format(permission, role_with_operations["name"],
-                                                    self.roles_to_operations_file))
+                        raise AuthException(
+                            "Invalid permission '{}' terminated in ':' for role '{}'; at file {}".format(
+                                permission,
+                                role_with_operations["name"],
+                                self.roles_to_operations_file,
+                            )
+                        )
 
                 if "default" not in role_with_operations["permissions"]:
                     role_with_operations["permissions"]["default"] = False
 
                 if "default" not in role_with_operations["permissions"]:
                     role_with_operations["permissions"]["default"] = False
@@ -252,18 +334,63 @@ class Authenticator:
                 }
 
                 # 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"]))
+                try:
+                    self.backend.create_role(role_with_operations)
+                    self.logger.info(
+                        "Role '{}' created".format(role_with_operations["name"])
+                    )
+                except (AuthException, AuthconnException) as e:
+                    if role_with_operations["name"] == "system_admin":
+                        raise
+                    self.logger.error(
+                        "Role '{}' cannot be created: {}".format(
+                            role_with_operations["name"], e
+                        )
+                    )
 
         # Create admin project&user if required
         pid = self.create_admin_project()
 
         # Create admin project&user if required
         pid = self.create_admin_project()
-        self.create_admin_user(pid)
+        user_id = self.create_admin_user(pid)
 
 
-        if self.config["authentication"]["backend"] == "keystone":
+        # try to assign system_admin role to user admin if not any user has this role
+        if not user_id:
             try:
             try:
-                self.backend.assign_role_to_user("admin", "admin", "system_admin")
-            except Exception:
-                pass
+                users = self.backend.get_user_list()
+                roles = self.backend.get_role_list({"name": "system_admin"})
+                role_id = roles[0]["_id"]
+                user_with_system_admin = False
+                user_admin_id = None
+                for user in users:
+                    if not user_admin_id:
+                        user_admin_id = user["_id"]
+                    if user["username"] == "admin":
+                        user_admin_id = user["_id"]
+                    for prm in user.get("project_role_mappings", ()):
+                        if prm["role"] == role_id:
+                            user_with_system_admin = True
+                            break
+                    if user_with_system_admin:
+                        break
+                if not user_with_system_admin:
+                    self.backend.update_user(
+                        {
+                            "_id": user_admin_id,
+                            "add_project_role_mappings": [
+                                {"project": pid, "role": role_id}
+                            ],
+                        }
+                    )
+                    self.logger.info(
+                        "Added role system admin to user='{}' project=admin".format(
+                            user_admin_id
+                        )
+                    )
+            except Exception as e:
+                self.logger.error(
+                    "Error in Authorization DataBase initialization: {}: {}".format(
+                        type(e).__name__, e
+                    )
+                )
 
         self.load_operation_to_allowed_roles()
 
 
         self.load_operation_to_allowed_roles()
 
@@ -282,13 +409,21 @@ class Authenticator:
         for record in records:
             if not record.get("permissions"):
                 continue
         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
+            ]
             operations_joined.sort(key=lambda x: x[0].count(":"))
 
             for oper in operations_joined:
             operations_joined.sort(key=lambda x: x[0].count(":"))
 
             for oper in operations_joined:
-                match = list(filter(lambda x: x.find(oper[0]) == 0, record_permissions.keys()))
+                match = list(
+                    filter(lambda x: x.find(oper[0]) == 0, record_permissions.keys())
+                )
 
                 for m in match:
                     record_permissions[m] = oper[1]
 
                 for m in match:
                     record_permissions[m] = oper[1]
@@ -300,7 +435,9 @@ class Authenticator:
 
         self.operation_to_allowed_roles = permissions
 
 
         self.operation_to_allowed_roles = permissions
 
-    def authorize(self, role_permission=None, query_string_operations=None):
+    def authorize(
+        self, role_permission=None, query_string_operations=None, item_id=None
+    ):
         token = None
         user_passwd64 = None
         try:
         token = None
         user_passwd64 = None
         try:
@@ -318,7 +455,9 @@ class Authenticator:
                     token = cherrypy.session.get("Authorization")
                     if token == "logout":
                         token = None  # force Unauthorized response to insert user password again
                     token = cherrypy.session.get("Authorization")
                     if token == "logout":
                         token = None  # force Unauthorized response to insert user password again
-                elif user_passwd64 and cherrypy.request.config.get("auth.allow_basic_authentication"):
+                elif user_passwd64 and cherrypy.request.config.get(
+                    "auth.allow_basic_authentication"
+                ):
                     # 3. Get new token from user password
                     user = None
                     passwd = None
                     # 3. Get new token from user password
                     user = None
                     passwd = None
@@ -327,40 +466,78 @@ class Authenticator:
                         user, _, passwd = user_passwd.partition(":")
                     except Exception:
                         pass
                         user, _, passwd = user_passwd.partition(":")
                     except Exception:
                         pass
-                    outdata = self.new_token(None, {"username": user, "password": passwd})
+                    outdata = self.new_token(
+                        None, {"username": user, "password": passwd}
+                    )
                     token = outdata["_id"]
                     token = outdata["_id"]
-                    cherrypy.session['Authorization'] = token
+                    cherrypy.session["Authorization"] = token
 
             if not token:
 
             if not token:
-                raise AuthException("Needed a token or Authorization http header",
-                                    http_code=HTTPStatus.UNAUTHORIZED)
-            token_info = self.backend.validate_token(token)
+                raise AuthException(
+                    "Needed a token or Authorization http header",
+                    http_code=HTTPStatus.UNAUTHORIZED,
+                )
+
+            # try to get from cache first
+            now = time()
+            token_info = self.tokens_cache.get(token)
+            if token_info and token_info["expires"] < now:
+                # delete token. MUST be done with care, as another thread maybe already delete it. Do not use del
+                self.tokens_cache.pop(token, None)
+                token_info = None
+
+            # get from database if not in cache
+            if not token_info:
+                token_info = self.backend.validate_token(token)
+                # Clear cache if token limit reached
+                if len(self.tokens_cache) > self.token_limit:
+                    self.tokens_cache.clear()
+                self.tokens_cache[token] = token_info
             # TODO add to token info remote host, port
 
             if role_permission:
             # TODO add to token info remote host, port
 
             if role_permission:
-                self.check_permissions(token_info, cherrypy.request.method, role_permission,
-                                       query_string_operations)
+                RBAC_auth = self.check_permissions(
+                    token_info,
+                    cherrypy.request.method,
+                    role_permission,
+                    query_string_operations,
+                    item_id,
+                )
+                token_info["allow_show_user_project_role"] = RBAC_auth
+
             return token_info
         except AuthException as e:
             if not isinstance(e, AuthExceptionUnauthorized):
             return token_info
         except AuthException as e:
             if not isinstance(e, AuthExceptionUnauthorized):
-                if cherrypy.session.get('Authorization'):
-                    del cherrypy.session['Authorization']
-                cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e)
+                if cherrypy.session.get("Authorization"):
+                    del cherrypy.session["Authorization"]
+                cherrypy.response.headers[
+                    "WWW-Authenticate"
+                ] = 'Bearer realm="{}"'.format(e)
+            if self.config["authentication"].get("user_not_authorized"):
+                return {
+                    "id": "testing-token",
+                    "_id": "testing-token",
+                    "project_id": self.test_project_id,
+                    "username": self.config["authentication"]["user_not_authorized"],
+                    "roles": [self.system_admin_role_id],
+                    "admin": True,
+                    "allow_show_user_project_role": True,
+                }
             raise
 
     def new_token(self, token_info, indata, remote):
         new_token_info = self.backend.authenticate(
             raise
 
     def new_token(self, token_info, indata, remote):
         new_token_info = self.backend.authenticate(
-            user=indata.get("username"),
-            password=indata.get("password"),
+            credentials=indata,
             token_info=token_info,
             token_info=token_info,
-            project=indata.get("project_id")
         )
 
         new_token_info["remote_port"] = remote.port
         if not new_token_info.get("expires"):
             new_token_info["expires"] = time() + 3600
         if not new_token_info.get("admin"):
         )
 
         new_token_info["remote_port"] = remote.port
         if not new_token_info.get("expires"):
             new_token_info["expires"] = time() + 3600
         if not new_token_info.get("admin"):
-            new_token_info["admin"] = True if new_token_info.get("project_name") == "admin" else False
+            new_token_info["admin"] = (
+                True if new_token_info.get("project_name") == "admin" else False
+            )
             # TODO put admin in RBAC
 
         if remote.name:
             # TODO put admin in RBAC
 
         if remote.name:
@@ -368,8 +545,6 @@ class Authenticator:
         elif remote.ip:
             new_token_info["remote_host"] = remote.ip
 
         elif remote.ip:
             new_token_info["remote_host"] = remote.ip
 
-        self.tokens_cache[new_token_info["_id"]] = new_token_info
-
         # TODO call self._internal_tokens_prune(now) ?
         return deepcopy(new_token_info)
 
         # TODO call self._internal_tokens_prune(now) ?
         return deepcopy(new_token_info)
 
@@ -378,8 +553,11 @@ class Authenticator:
             return self._internal_get_token_list(token_info)
         else:
             # TODO: check if this can be avoided. Backend may provide enough information
             return self._internal_get_token_list(token_info)
         else:
             # TODO: check if this can be avoided. Backend may provide enough information
-            return [deepcopy(token) for token in self.tokens_cache.values()
-                    if token["username"] == token_info["username"]]
+            return [
+                deepcopy(token)
+                for token in self.tokens_cache.values()
+                if token["username"] == token_info["username"]
+            ]
 
     def get_token(self, token_info, token):
         if self.config["authentication"]["backend"] == "internal":
 
     def get_token(self, token_info, token):
         if self.config["authentication"]["backend"] == "internal":
@@ -389,19 +567,34 @@ class Authenticator:
             token_value = self.tokens_cache.get(token)
             if not token_value:
                 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
             token_value = self.tokens_cache.get(token)
             if not token_value:
                 raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
-            if token_value["username"] != token_info["username"] and not token_info["admin"]:
-                raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
+            if (
+                token_value["username"] != token_info["username"]
+                and not token_info["admin"]
+            ):
+                raise AuthException(
+                    "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED
+                )
             return token_value
 
     def del_token(self, token):
         try:
             self.backend.revoke_token(token)
             return token_value
 
     def del_token(self, token):
         try:
             self.backend.revoke_token(token)
-            self.tokens_cache.pop(token, None)
+            # self.tokens_cache.pop(token, None)
+            self.remove_token_from_cache(token)
             return "token '{}' deleted".format(token)
         except KeyError:
             return "token '{}' deleted".format(token)
         except KeyError:
-            raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
-
-    def check_permissions(self, token_info, method, role_permission=None, query_string_operations=None):
+            raise AuthException(
+                "Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND
+            )
+
+    def check_permissions(
+        self,
+        token_info,
+        method,
+        role_permission=None,
+        query_string_operations=None,
+        item_id=None,
+    ):
         """
         Checks that operation has permissions to be done, base on the assigned roles to this user project
         :param token_info: Dictionary that contains "roles" with a list of assigned roles.
         """
         Checks that operation has permissions to be done, base on the assigned roles to this user project
         :param token_info: Dictionary that contains "roles" with a list of assigned roles.
@@ -411,8 +604,11 @@ class Authenticator:
         :param role_permission: role permission name of the operation required
         :param query_string_operations: list of possible admin query strings provided by user. It is checked that the
             assigned role allows this query string for this method
         :param role_permission: role permission name of the operation required
         :param query_string_operations: list of possible admin query strings provided by user. It is checked that the
             assigned role allows this query string for this method
-        :return: None if granted, exception if not allowed
+        :param item_id: item identifier if included in the URL, None otherwise
+        :return: True if access granted by permission rules, False if access granted by default rules (Bug 853)
+        :raises: AuthExceptionUnauthorized if access denied
         """
         """
+        self.load_operation_to_allowed_roles()
 
         roles_required = self.operation_to_allowed_roles[role_permission]
         roles_allowed = [role["name"] for role in token_info["roles"]]
 
         roles_required = self.operation_to_allowed_roles[role_permission]
         roles_allowed = [role["name"] for role in token_info["roles"]]
@@ -425,24 +621,47 @@ class Authenticator:
                 break
 
         if "anonymous" in roles_required:
                 break
 
         if "anonymous" in roles_required:
-            return
+            return True
         operation_allowed = False
         for role in roles_allowed:
             if role in roles_required:
                 operation_allowed = True
                 # if query_string operations, check if this role allows it
                 if not query_string_operations:
         operation_allowed = False
         for role in roles_allowed:
             if role in roles_required:
                 operation_allowed = True
                 # if query_string operations, check if this role allows it
                 if not query_string_operations:
-                    return
+                    return True
                 for query_string_operation in query_string_operations:
                 for query_string_operation in query_string_operations:
-                    if role not in self.operation_to_allowed_roles[query_string_operation]:
+                    if (
+                        role
+                        not in self.operation_to_allowed_roles[query_string_operation]
+                    ):
                         break
                 else:
                         break
                 else:
-                    return
+                    return True
+
+        # Bug 853 - Final Solution
+        # User/Project/Role whole listings are filtered elsewhere
+        # uid, pid, rid = ("user_id", "project_id", "id") if is_valid_uuid(id) else ("username", "project_name", "name")
+        uid = "user_id" if is_valid_uuid(item_id) else "username"
+        if (
+            role_permission
+            in [
+                "projects:get",
+                "projects:id:get",
+                "roles:get",
+                "roles:id:get",
+                "users:get",
+            ]
+        ) or (role_permission == "users:id:get" and item_id == token_info[uid]):
+            # or (role_permission == "projects:id:get" and item_id == token_info[pid]) \
+            # or (role_permission == "roles:id:get" and item_id in [role[rid] for role in token_info["roles"]]):
+            return False
 
         if not operation_allowed:
             raise AuthExceptionUnauthorized("Access denied: lack of permissions.")
         else:
 
         if not operation_allowed:
             raise AuthExceptionUnauthorized("Access denied: lack of permissions.")
         else:
-            raise AuthExceptionUnauthorized("Access denied: You have not permissions to use these admin query string")
+            raise AuthExceptionUnauthorized(
+                "Access denied: You have not permissions to use these admin query string"
+            )
 
     def get_user_list(self):
         return self.backend.get_user_list()
 
     def get_user_list(self):
         return self.backend.get_user_list()
@@ -450,12 +669,15 @@ class Authenticator:
     def _normalize_url(self, url, method):
         # DEPRECATED !!!
         # Removing query strings
     def _normalize_url(self, url, method):
         # DEPRECATED !!!
         # Removing query strings
-        normalized_url = url if '?' not in url else url[:url.find("?")]
+        normalized_url = url if "?" not in url else url[: url.find("?")]
         normalized_url_splitted = normalized_url.split("/")
         parameters = {}
 
         normalized_url_splitted = normalized_url.split("/")
         parameters = {}
 
-        filtered_keys = [key for key in self.resources_to_operations_mapping.keys()
-                         if method in key.split()[0]]
+        filtered_keys = [
+            key
+            for key in self.resources_to_operations_mapping.keys()
+            if method in key.split()[0]
+        ]
 
         for idx, path_part in enumerate(normalized_url_splitted):
             tmp_keys = []
 
         for idx, path_part in enumerate(normalized_url_splitted):
             tmp_keys = []
@@ -467,33 +689,47 @@ class Authenticator:
                     if splitted[idx] == "<artifactPath>":
                         tmp_keys.append(tmp_key)
                         continue
                     if splitted[idx] == "<artifactPath>":
                         tmp_keys.append(tmp_key)
                         continue
-                    elif idx == len(normalized_url_splitted) - 1 and \
-                            len(normalized_url_splitted) != len(splitted):
+                    elif idx == len(normalized_url_splitted) - 1 and len(
+                        normalized_url_splitted
+                    ) != len(splitted):
                         continue
                     else:
                         tmp_keys.append(tmp_key)
                 elif splitted[idx] == path_part:
                         continue
                     else:
                         tmp_keys.append(tmp_key)
                 elif splitted[idx] == path_part:
-                    if idx == len(normalized_url_splitted) - 1 and \
-                            len(normalized_url_splitted) != len(splitted):
+                    if idx == len(normalized_url_splitted) - 1 and len(
+                        normalized_url_splitted
+                    ) != len(splitted):
                         continue
                     else:
                         tmp_keys.append(tmp_key)
             filtered_keys = tmp_keys
                         continue
                     else:
                         tmp_keys.append(tmp_key)
             filtered_keys = tmp_keys
-            if len(filtered_keys) == 1 and \
-                    filtered_keys[0].split("/")[-1] == "<artifactPath>":
+            if (
+                len(filtered_keys) == 1
+                and filtered_keys[0].split("/")[-1] == "<artifactPath>"
+            ):
                 break
 
         if len(filtered_keys) == 0:
                 break
 
         if len(filtered_keys) == 0:
-            raise AuthException("Cannot make an authorization decision. URL not found. URL: {0}".format(url))
+            raise AuthException(
+                "Cannot make an authorization decision. URL not found. URL: {0}".format(
+                    url
+                )
+            )
         elif len(filtered_keys) > 1:
         elif len(filtered_keys) > 1:
-            raise AuthException("Cannot make an authorization decision. Multiple URLs found. URL: {0}".format(url))
+            raise AuthException(
+                "Cannot make an authorization decision. Multiple URLs found. URL: {0}".format(
+                    url
+                )
+            )
 
         filtered_key = filtered_keys[0]
 
         for idx, path_part in enumerate(filtered_key.split()[1].split("/")):
             if "<" in path_part and ">" in path_part:
                 if path_part == "<artifactPath>":
 
         filtered_key = filtered_keys[0]
 
         for idx, path_part in enumerate(filtered_key.split()[1].split("/")):
             if "<" in path_part and ">" in path_part:
                 if path_part == "<artifactPath>":
-                    parameters[path_part[1:-1]] = "/".join(normalized_url_splitted[idx:])
+                    parameters[path_part[1:-1]] = "/".join(
+                        normalized_url_splitted[idx:]
+                    )
                 else:
                     parameters[path_part[1:-1]] = normalized_url_splitted[idx]
 
                 else:
                     parameters[path_part[1:-1]] = normalized_url_splitted[idx]
 
@@ -501,20 +737,34 @@ class Authenticator:
 
     def _internal_get_token_list(self, token_info):
         now = time()
 
     def _internal_get_token_list(self, token_info):
         now = time()
-        token_list = self.db.get_list("tokens", {"username": token_info["username"], "expires.gt": now})
+        token_list = self.db.get_list(
+            "tokens", {"username": token_info["username"], "expires.gt": now}
+        )
         return token_list
 
     def _internal_get_token(self, token_info, token_id):
         token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False)
         if not token_value:
             raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
         return token_list
 
     def _internal_get_token(self, token_info, token_id):
         token_value = self.db.get_one("tokens", {"_id": token_id}, fail_on_empty=False)
         if not token_value:
             raise AuthException("token not found", http_code=HTTPStatus.NOT_FOUND)
-        if token_value["username"] != token_info["username"] and not token_info["admin"]:
-            raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED)
+        if (
+            token_value["username"] != token_info["username"]
+            and not token_info["admin"]
+        ):
+            raise AuthException(
+                "needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED
+            )
         return token_value
 
         return token_value
 
-    def _internal_tokens_prune(self, now=None):
+    def _internal_tokens_prune(self, token_collection, now=None):
         now = now or time()
         if not self.next_db_prune_time or self.next_db_prune_time >= now:
         now = now or time()
         if not self.next_db_prune_time or self.next_db_prune_time >= now:
-            self.db.del_list("tokens", {"expires.lt": now})
+            self.db.del_list(token_collection, {"expires.lt": now})
             self.next_db_prune_time = self.periodin_db_pruning + now
             self.next_db_prune_time = self.periodin_db_pruning + now
-            self.tokens_cache.clear()  # force to reload tokens from database
+            # self.tokens_cache.clear()  # not required any more
+
+    def remove_token_from_cache(self, token=None):
+        if token:
+            self.tokens_cache.pop(token, None)
+        else:
+            self.tokens_cache.clear()
+        self.msg.write("admin", "revoke_token", {"_id": token} if token else None)