X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fauth.py;h=6cbfe685732d3fba9ac46f3eff42854dc231bac7;hp=023d2866b26a6754ba5a3a969499f22e0cdd7f90;hb=7ddb0732d05743a56ee3376446f76be8fa73d3ad;hpb=15ec70643bdaa3ba84eccefbed73ab525bfd31fc diff --git a/osm_nbi/auth.py b/osm_nbi/auth.py index 023d286..6cbfe68 100644 --- a/osm_nbi/auth.py +++ b/osm_nbi/auth.py @@ -39,11 +39,11 @@ from http import HTTPStatus from time import time from os import path -from osm_nbi.authconn import AuthException, AuthExceptionUnauthorized +from osm_nbi.authconn import AuthException, AuthconnException, AuthExceptionUnauthorized from osm_nbi.authconn_keystone import AuthconnKeystone -from osm_nbi.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_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_nbi.validation import is_valid_uuid from itertools import chain @@ -60,6 +60,7 @@ class Authenticator: """ 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): """ @@ -69,6 +70,7 @@ class Authenticator: self.backend = None self.config = None self.db = None + self.msg = 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 @@ -79,6 +81,8 @@ class Authenticator: 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): """ @@ -101,12 +105,25 @@ class Authenticator: else: 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": - 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": - 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: raise AuthException("Unknown authentication backend: {}" .format(config["authentication"]["backend"])) @@ -136,7 +153,7 @@ class Authenticator: 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) @@ -146,6 +163,15 @@ class Authenticator: 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)) @@ -211,8 +237,8 @@ class Authenticator: 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: roles_to_operations_yaml = yaml.load(stream, Loader=yaml.Loader) @@ -234,7 +260,7 @@ class Authenticator: .format(permission, role_with_operations["name"], self.roles_to_operations_file)) - # TODO chek permission is ok + # TODO check permission is ok if permission[-1] == ":": raise AuthException("Invalid permission '{}' terminated in ':' for role '{}'; at file {}" .format(permission, role_with_operations["name"], @@ -252,8 +278,13 @@ class Authenticator: } # 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() @@ -354,7 +385,22 @@ class Authenticator: if not token: raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) - token_info = self.backend.validate_token(token) + + # 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: @@ -368,20 +414,18 @@ class Authenticator: if cherrypy.session.get('Authorization'): del cherrypy.session['Authorization'] cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e) - elif self.config.get("user_not_authorized"): - # TODO provide user_id, roles id (not name), project_id - return {"id": "fake-token-id-for-test", - "project_id": self.config.get("project_not_authorized", "admin"), - "username": self.config["user_not_authorized"], - "roles": ["system_admin"]} + 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( - user=indata.get("username"), - password=indata.get("password"), + credentials=indata, token_info=token_info, - project=indata.get("project_id") ) new_token_info["remote_port"] = remote.port @@ -396,8 +440,6 @@ class Authenticator: 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) @@ -424,7 +466,8 @@ class Authenticator: 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: raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND) @@ -552,9 +595,16 @@ class Authenticator: raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) 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: - 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.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)