def _internal_get_token(self, session, 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"] != session["username"] and not session["admin"]: raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) return token_value
def _internal_authorize(self, token_id): try: if not token_id: raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) # try to get from cache first now = time() session = self.tokens_cache.get(token_id) if session and session["expires"] < now: del self.tokens_cache[token_id] session = None if session: return session # get from database if not in cache session = self.db.get_one("tokens", {"_id": token_id}) if session["expires"] < now: raise AuthException("Expired Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) self.tokens_cache[token_id] = session return session except DbException as e: if e.http_code == HTTPStatus.NOT_FOUND: raise AuthException("Invalid Token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) else: raise except AuthException: 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"]} else: raise
def start(self, config): """ Method to configure the Authenticator object. This method should be called after object creation. It is responsible by initializing the selected backend, as well as the initialization of the database connection. :param config: dictionary containing the relevant parameters for this object. """ self.config = config try: if not self.db: if config["database"]["driver"] == "mongo": self.db = dbmongo.DbMongo() self.db.db_connect(config["database"]) elif config["database"]["driver"] == "memory": self.db = dbmemory.DbMemory() self.db.db_connect(config["database"]) else: raise AuthException( "Invalid configuration param '{}' at '[database]':'driver'" .format(config["database"]["driver"])) if not self.backend: if config["authentication"]["backend"] == "keystone": self.backend = AuthconnKeystone( self.config["authentication"]) elif config["authentication"]["backend"] == "internal": self._internal_tokens_prune() else: raise AuthException( "Unknown authentication backend: {}".format( config["authentication"]["backend"])) except Exception as e: raise AuthException(str(e))
def get_token(self, session, token): if self.config["authentication"]["backend"] == "internal": return self._internal_get_token(session, token) else: # TODO: check if this can be avoided. Backend may provide enough information 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"] != session["username"] and not session["admin"]: raise AuthException("needed admin privileges", http_code=HTTPStatus.UNAUTHORIZED) return token_value
def start(self, config): """ Method to configure the Authenticator object. This method should be called after object creation. It is responsible by initializing the selected backend, as well as the initialization of the database connection. :param config: dictionary containing the relevant parameters for this object. """ self.config = config try: if not self.db: if config["database"]["driver"] == "mongo": self.db = dbmongo.DbMongo() self.db.db_connect(config["database"]) elif config["database"]["driver"] == "memory": self.db = dbmemory.DbMemory() self.db.db_connect(config["database"]) else: raise AuthException("Invalid configuration param '{}' at '[database]':'driver'" .format(config["database"]["driver"])) if not self.backend: if config["authentication"]["backend"] == "keystone": self.backend = AuthconnKeystone(self.config["authentication"]) elif config["authentication"]["backend"] == "internal": self._internal_tokens_prune() else: raise AuthException("Unknown authentication backend: {}" .format(config["authentication"]["backend"])) if not self.resources_to_operations_file: 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"): if path.isfile(config_file): self.resources_to_operations_file = config_file break if not self.resources_to_operations_file: raise AuthException("Invalid permission configuration: resources_to_operations file missing") if not self.roles_to_operations_file: 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"): 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") except Exception as e: raise AuthException(str(e))
def _internal_new_token(self, session, indata, remote): now = time() user_content = None # Try using username/password if indata.get("username"): user_rows = self.db.get_list("users", {"username": indata.get("username")}) if user_rows: user_content = user_rows[0] salt = user_content["_admin"]["salt"] shadow_password = sha256(indata.get("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 session: user_rows = self.db.get_list("users", {"username": session["username"]}) if user_rows: user_content = user_rows[0] else: 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)) 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) else: project_id = user_content["projects"][0] if project_id == "admin": session_admin = True else: project = self.db.get_one("projects", {"_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"], "remote_port": remote.port, "admin": session_admin} if remote.name: new_session["remote_host"] = remote.name elif remote.ip: new_session["remote_host"] = remote.ip self.tokens_cache[token_id] = new_session self.db.create("tokens", new_session) # check if database must be prune self._internal_tokens_prune(now) return deepcopy(new_session)
def new_token(self, session, indata, remote): if self.config["authentication"]["backend"] == "internal": return self._internal_new_token(session, indata, remote) else: if indata.get("username"): token, projects = self.backend.authenticate_with_user_password( indata.get("username"), indata.get("password")) elif session: token, projects = self.backend.authenticate_with_token( session.get("id"), indata.get("project_id")) else: raise AuthException("Provide credentials: username/password or Authorization Bearer token", http_code=HTTPStatus.UNAUTHORIZED) if indata.get("project_id"): project_id = indata.get("project_id") if project_id not in projects: raise AuthException("Project {} not allowed for this user".format(project_id), http_code=HTTPStatus.UNAUTHORIZED) else: project_id = projects[0] if not session: token, projects = self.backend.authenticate_with_token(token, project_id) if project_id == "admin": session_admin = True else: session_admin = reduce(lambda x, y: x or (True if y == "admin" else False), projects, False) now = time() new_session = { "_id": token, "id": token, "issued_at": now, "expires": now + 3600, "project_id": project_id, "username": indata.get("username") if not session else session.get("username"), "remote_port": remote.port, "admin": session_admin } if remote.name: new_session["remote_host"] = remote.name elif remote.ip: new_session["remote_host"] = remote.ip # TODO: check if this can be avoided. Backend may provide enough information self.tokens_cache[token] = new_session return deepcopy(new_session)
def authorize(self): token = None user_passwd64 = None try: # 1. Get token Authorization bearer auth = cherrypy.request.headers.get("Authorization") if auth: auth_list = auth.split(" ") if auth_list[0].lower() == "bearer": token = auth_list[-1] elif auth_list[0].lower() == "basic": user_passwd64 = auth_list[-1] if not token: if cherrypy.session.get("Authorization"): # 2. Try using session before request a new token. If not, basic authentication will generate 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"): # 3. Get new token from user password user = None passwd = None try: user_passwd = standard_b64decode(user_passwd64).decode() user, _, passwd = user_passwd.partition(":") except Exception: pass outdata = self.new_token(None, {"username": user, "password": passwd}) token = outdata["id"] cherrypy.session['Authorization'] = token if self.config["authentication"]["backend"] == "internal": return self._internal_authorize(token) else: if not token: raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) try: self.backend.validate_token(token) self.check_permissions(self.tokens_cache[token], cherrypy.request.path_info, cherrypy.request.method) # TODO: check if this can be avoided. Backend may provide enough information return deepcopy(self.tokens_cache[token]) except AuthException: self.del_token(token) raise except AuthException as e: if cherrypy.session.get('Authorization'): del cherrypy.session['Authorization'] cherrypy.response.headers["WWW-Authenticate"] = 'Bearer realm="{}"'.format(e) raise AuthException(str(e))
def _normalize_url(self, url, method): # Removing query strings normalized_url = url if '?' not in url else url[:url.find("?")] 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]] for idx, path_part in enumerate(normalized_url_splitted): tmp_keys = [] for tmp_key in filtered_keys: splitted = tmp_key.split()[1].split("/") if "<" in splitted[idx] and ">" in splitted[idx]: if splitted[idx] == "<artifactPath>": tmp_keys.append(tmp_key) continue 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: 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 if len(filtered_keys) == 1 and \ filtered_keys[0].split("/")[-1] == "<artifactPath>": break if len(filtered_keys) == 0: raise AuthException("Cannot make an authorization decision. URL not found. URL: {0}".format(url)) elif len(filtered_keys) > 1: 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>": parameters[path_part[1:-1]] = "/".join(normalized_url_splitted[idx:]) else: parameters[path_part[1:-1]] = normalized_url_splitted[idx] return filtered_key, parameters
def decode_token_OIDC(self, token): if not token: raise AuthException("Needed a token or Authorization http header", http_code=HTTPStatus.UNAUTHORIZED) oidc_resource = self.config["authentication"]["oidc_resource"] oidc_well_known_url = self.config["authentication"][ "oidc_well_known_url"] response = urllib.request.urlopen(oidc_well_known_url) oidc_well_known_json = json.loads( urllib.request.urlopen(oidc_well_known_url).read().decode('utf-8')) oidc_jwks_uri = oidc_well_known_json["jwks_uri"] oidc_key_json = json.loads( urllib.request.urlopen(oidc_jwks_uri).read().decode('utf-8')) key_json = json.dumps(oidc_key_json["keys"][0]) public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key_json) decoded = jwt.decode(token, public_key, algorithms='RS256', audience=oidc_resource) localuser = decoded.get("resource_access").get(oidc_resource).get( "roles")[0] self.logger.info("localuser: " + localuser) return localuser
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] token = self.keystone.get_raw_token_from_identity_service( auth_url=self.auth_url, token=token, project_name=project, user_domain_name=self.user_domain_name, project_domain_name=self.project_domain_name) return token["auth_token"], project_names except ClientException: self.logger.exception( "Error during user authentication using keystone. Method: bearer" ) raise AuthException( "Error during user authentication using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
def authenticate_with_user_password(self, user, password): """ Authenticate a user using username and password. :param user: username :param password: password :return: an unscoped token that grants access to project list """ try: user_id = list( filter(lambda x: x.name == user, self.keystone.users.list()))[0].id project_names = [ project.name for project in self.keystone.projects.list(user=user_id) ] token = self.keystone.get_raw_token_from_identity_service( auth_url=self.auth_url, username=user, password=password, user_domain_name=self.user_domain_name, project_domain_name=self.project_domain_name) return token["auth_token"], project_names except ClientException: self.logger.exception( "Error during user authentication using keystone. Method: basic" ) raise AuthException( "Error during user authentication using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
def authorize_OIDC(self, access_token, remote): user = self.decode_token_OIDC(access_token) now = time() user_content = None if user: user_rows = self.db.get_list("users", {"username": user}) user_content = None if user_rows: user_content = user_rows[0] if access_token == None: user_content = None if not user_content: raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED) else: raise AuthException( "Provide credentials: username/password or Authorization Bearer token", http_code=HTTPStatus.UNAUTHORIZED) token_id = str(access_token) project_id = user_content["projects"][0] if project_id == "admin": session_admin = True else: project = self.db.get_one("projects", {"_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"], "remote_port": remote.port, "admin": session_admin } if remote.name: new_session["remote_host"] = remote.name elif remote.ip: new_session["remote_host"] = remote.ip self.tokens_cache[token_id] = new_session return deepcopy(new_session)
def del_token(self, token): if self.config["authentication"]["backend"] == "internal": return self._internal_del_token(token) else: try: self.backend.revoke_token(token) del self.tokens_cache[token] return "token '{}' deleted".format(token) except KeyError: raise AuthException("Token '{}' not found".format(token), http_code=HTTPStatus.NOT_FOUND)
def _internal_del_token(self, token_id): try: self.tokens_cache.pop(token_id, None) self.db.del_one("tokens", {"_id": token_id}) return "token '{}' deleted".format(token_id) except DbException as e: if e.http_code == HTTPStatus.NOT_FOUND: raise AuthException("Token '{}' not found".format(token_id), http_code=HTTPStatus.NOT_FOUND) else: raise
def revoke_token(self, token): """ Invalidate a token. :param token: token to be revoked """ try: self.keystone.tokens.revoke_token(token=token) return True except ClientException: self.logger.exception( "Error during token revocation using keystone") raise AuthException("Error during token revocation using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
def validate_token(self, token): """ Check if the token is valid. :param token: token to validate :return: dictionary with information associated with the token. If the token is not valid, returns None. """ if not token: return try: token_info = self.keystone.tokens.validate(token=token) return token_info except ClientException: self.logger.exception( "Error during token validation using keystone") raise AuthException("Error during token validation using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
def get_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: self.logger.exception( "Error during user project listing using keystone") raise AuthException( "Error during user project listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
def check_permissions(self, session, url, method): self.logger.info("Session: {}".format(session)) self.logger.info("URL: {}".format(url)) self.logger.info("Method: {}".format(method)) key, parameters = self._normalize_url(url, method) # TODO: Check if parameters might be useful for the decision operation = self.resources_to_operations_mapping[key] roles_required = self.operation_to_allowed_roles[operation] roles_allowed = self.backend.get_role_list(session["id"]) if "anonymous" in roles_required: return for role in roles_allowed: if role in roles_required: return raise AuthException("Access denied: lack of permissions.")
def get_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 = self.keystone.roles.list( user=token_info["user"]["id"], project=token_info["project"]["id"]) return roles except ClientException: self.logger.exception( "Error during user role listing using keystone") raise AuthException( "Error during user role listing using Keystone", http_code=HTTPStatus.UNAUTHORIZED)
def __init__(self, config): Authconn.__init__(self, config) self.logger = logging.getLogger("nbi.authenticator.keystone") self.auth_url = "http://{0}:{1}/v3".format( config.get("auth_url", "keystone"), config.get("auth_port", "5000")) self.user_domain_name = config.get("user_domain_name", "default") self.admin_project = config.get("service_project", "service") self.admin_username = config.get("service_username", "nbi") self.admin_password = config.get("service_password", "nbi") self.project_domain_name = config.get("project_domain_name", "default") # Waiting for Keystone to be up available = None counter = 300 while available is None: time.sleep(1) try: result = requests.get(self.auth_url) available = True if result.status_code == 200 else None except Exception: counter -= 1 if counter == 0: raise AuthException( "Keystone not available after 300s timeout") self.auth = v3.Password(user_domain_name=self.user_domain_name, username=self.admin_username, password=self.admin_password, project_domain_name=self.project_domain_name, project_name=self.admin_project, auth_url=self.auth_url) self.sess = session.Session(auth=self.auth) self.keystone = client.Client(session=self.sess)
def new_token_OIDC(self, indata, remote): oidc_resource = self.config["authentication"]["oidc_resource"] oidc_secret = self.config["authentication"]["oidc_secret"] oidc_callback_url = self.config["authentication"]["oidc_callback_url"] oidc_well_known_url = self.config["authentication"][ "oidc_well_known_url"] oidc_well_known_json = json.loads( urllib.request.urlopen(oidc_well_known_url).read().decode('utf-8')) oidc_token_endpoint = oidc_well_known_json["token_endpoint"] code = indata.get("code") url = oidc_token_endpoint payload = "code=" + code + "&redirect_uri=" + urllib.parse.quote( oidc_callback_url) + "&grant_type=authorization_code" basic = base64.b64encode( (oidc_resource + ':' + oidc_secret).encode()).decode("utf-8") headers = { 'Content-Type': "application/x-www-form-urlencoded", 'cache-control': "no-cache", 'Authorization': "Basic " + basic } login_response = requests.request("POST", url, data=payload, headers=headers) access_token = login_response.json()['access_token'] now = time() user_content = None user = self.decode_token_OIDC(access_token) if user: user_rows = self.db.get_list("users", {"username": user}) user_content = None if user_rows: user_content = user_rows[0] if access_token == None: user_content = None if not user_content: raise AuthException("Invalid username/password", http_code=HTTPStatus.UNAUTHORIZED) else: raise AuthException( "Provide credentials: username/password or Authorization Bearer token", http_code=HTTPStatus.UNAUTHORIZED) token_id = str(access_token) 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) else: project_id = user_content["projects"][0] if project_id == "admin": session_admin = True else: project = self.db.get_one("projects", {"_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"], "remote_port": remote.port, "admin": session_admin } if remote.name: new_session["remote_host"] = remote.name elif remote.ip: new_session["remote_host"] = remote.ip self.tokens_cache[token_id] = new_session return deepcopy(new_session)
def stop(self): try: if self.db: self.db.db_disconnect() except DbException as e: raise AuthException(str(e), http_code=e.http_code)