From f575da97a6806d0dd88e389d9bca0c972eeac130 Mon Sep 17 00:00:00 2001 From: multiflexi Date: Sun, 3 Mar 2024 20:55:44 +0100 Subject: [PATCH 1/8] upgrade core to use current Alpine and Python --- docker/Dockerfile.core | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile.core b/docker/Dockerfile.core index 2747b3351..c9afe20a5 100644 --- a/docker/Dockerfile.core +++ b/docker/Dockerfile.core @@ -1,4 +1,4 @@ -FROM python:3.7-alpine3.14 AS build_shared +FROM python:3.12-alpine3.19 AS build_shared WORKDIR /build_shared/ @@ -8,7 +8,7 @@ RUN python -m build -FROM python:3.7-alpine3.14 AS production +FROM python:3.12-alpine3.19 AS production WORKDIR /app/ From 23272befad79ab6ca6d72a83bb61332d9588c995 Mon Sep 17 00:00:00 2001 From: multiflexi Date: Sun, 3 Mar 2024 20:57:17 +0100 Subject: [PATCH 2/8] upgrade requirements.txt, remove unnecessary items --- src/core/requirements.txt | 58 ++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/src/core/requirements.txt b/src/core/requirements.txt index 5784a289b..bb43dbdd7 100644 --- a/src/core/requirements.txt +++ b/src/core/requirements.txt @@ -1,38 +1,28 @@ -alembic==1.10.2 -certifi==2019.11.28 -Flask==1.1.4 -Flask-Cors==3.0.10 -Flask-JWT-Extended==3.24.1 -Flask-Migrate==2.5.2 +alembic==1.11.0 +Flask==3.0.2 +Flask-Cors==4.0.0 +Flask-JWT-Extended==4.6.0 +Flask-Migrate==4.0.5 flask-oidc==1.4.0 -Flask-RESTful==0.3.7 -Flask-Script==2.0.6 -Flask-SSE==0.2.1 -Flask-SQLAlchemy==2.5.1 -gevent==21.8.0 -greenlet==1.1.1 -gunicorn==20.0.4 -idna==2.9 -Jinja2==2.11.3 +Flask-RESTful==0.3.10 +#Flask-Script==2.0.6 +Flask-SSE==1.0.0 +Flask-SQLAlchemy==3.0.5 +gevent==24.2.1 +gunicorn==21.2.0 +Jinja2==3.1.3 ldap3==2.9.1 -Mako==1.1.0 -MarkupSafe==1.1.0 -marshmallow==3.18.0 +# markupsafe==2.0.1 #remove after Jinja2 upgraded +marshmallow==3.19.0 marshmallow-enum==1.5.1 psycogreen==1.0.2 -psycopg2-binary==2.9.6 -PyJWT==1.7.1 -python-dateutil==2.8.1 -python-dotenv==0.10.3 -python-editor==1.0.4 -python-keycloak==0.23.0 -pytz==2019.3 -requests==2.26.0 -schedule==0.6.0 -six==1.13.0 -sseclient-py==1.7 -soupsieve==1.9.5 -SQLAlchemy==1.4.47 -urllib3==1.26.7 -Werkzeug==0.16.0 -pycryptodomex==3.17 +psycopg2==2.9.9 +PyJWT==2.8.0 +python-dotenv==1.0.1 +python-keycloak==3.9.1 +requests==2.31.0 +schedule==1.2.1 +sseclient-py==1.8.0 +SQLAlchemy==1.4.51 #upgrade +Werkzeug==3.0.1 #update +pycryptodomex==3.20 From 7ae6548387dec924887168ae16c9d9568ef05b4a Mon Sep 17 00:00:00 2001 From: multiflexi Date: Sun, 3 Mar 2024 21:17:57 +0100 Subject: [PATCH 3/8] update code for newer version of flask_jwt_extended, f-strings --- src/core/managers/auth_manager.py | 305 ++++++++++++++++++++++++------ 1 file changed, 245 insertions(+), 60 deletions(-) diff --git a/src/core/managers/auth_manager.py b/src/core/managers/auth_manager.py index 12758b9e4..1616b1251 100644 --- a/src/core/managers/auth_manager.py +++ b/src/core/managers/auth_manager.py @@ -1,10 +1,34 @@ +"""This module contains the authentication manager for the Taranis-NG application. + +The authentication manager handles user authentication and authorization using different authenticators. +It provides functions for initializing the manager, authenticating users, refreshing authentication tokens, +performing access control checks, and retrieving user permissions. + +Classes: + ACLCheck: Enumeration for ACL checks. + +Functions: + cleanup_token_blacklist: Clean up the token blacklist by deleting tokens older than one day. + initialize: Initialize the authentication manager. + get_required_credentials: Get the required credentials for authentication. + authenticate: Authenticate the user with the provided credentials. + refresh: Refresh the authentication token for the specified user. + logout: Log out a user. + check_acl: Check the access control list (ACL) for the given item. + no_auth: Decorator that allows access to the decorated function without authentication. + get_id_name_by_acl: Get the corresponding ID name based on the given ACL. + get_user_from_api_key: Try to authenticate the user by API key. + get_perm_from_user: Get user permissions. + get_user_from_jwt_token: Try to authenticate the user by JWT token. + get_perm_from_jwt_token: Get user permissions from JWT token. +""" import os from datetime import datetime, timedelta from enum import Enum, auto from functools import wraps import jwt from flask import request -from flask_jwt_extended import JWTManager, get_jwt_claims, get_jwt_identity, verify_jwt_in_request, get_raw_jwt +from flask_jwt_extended import JWTManager, get_jwt_identity, verify_jwt_in_request, get_jwt from flask_jwt_extended.exceptions import JWTExtendedException from managers import log_manager, time_manager @@ -25,29 +49,44 @@ current_authenticator = None -api_key = os.getenv('API_KEY') +api_key = os.getenv("API_KEY") def cleanup_token_blacklist(app): + """Clean up the token blacklist by deleting tokens older than one day. + + Arguments: + app -- The Flask application object. + """ with app.app_context(): TokenBlacklist.delete_older(datetime.today() - timedelta(days=1)) def initialize(app): + """Initialize the authentication manager. + + This function sets up the authentication manager based on the configured authenticator. + + Arguments: + app: The Flask application object. + + Returns: + None + """ global current_authenticator JWTManager(app) - which = os.getenv('TARANIS_NG_AUTHENTICATOR') + which = os.getenv("TARANIS_NG_AUTHENTICATOR") if which is not None: which = which.lower() - if which == 'openid': + if which == "openid": current_authenticator = OpenIDAuthenticator() - elif which == 'keycloak': + elif which == "keycloak": current_authenticator = KeycloakAuthenticator() - elif which == 'password': + elif which == "password": current_authenticator = PasswordAuthenticator() - elif which == 'ldap': + elif which == "ldap": current_authenticator = LDAPAuthenticator() else: current_authenticator = PasswordAuthenticator() @@ -58,22 +97,67 @@ def initialize(app): def get_required_credentials(): + """Get the required credentials for authentication. + + This function returns the required credentials for authentication. + + Returns: + The required credentials for authentication. + """ return current_authenticator.get_required_credentials() def authenticate(credentials): + """Authenticate the user with the provided credentials. + + Arguments: + credentials -- The user's credentials. + + Returns: + The result of the authentication process. + """ return current_authenticator.authenticate(credentials) def refresh(user): + """Refresh the authentication token for the specified user. + + Arguments: + user -- The user object for which the authentication token needs to be refreshed. + + Returns: + The refreshed authentication token. + """ return current_authenticator.refresh(user) def logout(token): + """Log out a user. + + Arguments: + token (str): The authentication token of the user. + + Returns: + None + """ return current_authenticator.logout(token) class ACLCheck(Enum): + """Enumeration for ACL checks. + + This enumeration defines the different types of access control checks that can be performed. + + Attributes: + OSINT_SOURCE_GROUP_ACCESS: Access check for OSINT source group. + NEWS_ITEM_ACCESS: Access check for news item. + NEWS_ITEM_MODIFY: Modify check for news item. + REPORT_ITEM_ACCESS: Access check for report item. + REPORT_ITEM_MODIFY: Modify check for report item. + PRODUCT_TYPE_ACCESS: Access check for product type. + PRODUCT_TYPE_MODIFY: Modify check for product type. + """ + OSINT_SOURCE_GROUP_ACCESS = auto() NEWS_ITEM_ACCESS = auto() NEWS_ITEM_MODIFY = auto() @@ -84,11 +168,23 @@ class ACLCheck(Enum): def check_acl(item_id, acl_check, user): - check_see = 'SEE' in str(acl_check) - check_access = 'ACCESS' in str(acl_check) - check_modify = 'MODIFY' in str(acl_check) + """Check the access control list (ACL) for the given item. + + This function determines whether the user has the necessary permissions to perform the specified ACL check on the item. + + Arguments: + item_id (int): The ID of the item. + acl_check (ACLCheck): The type of ACL check to perform. + user (User): The user performing the ACL check. + + Returns: + bool: True if the user is allowed to perform the ACL check, False otherwise. + """ + check_see = "SEE" in str(acl_check) + check_access = "ACCESS" in str(acl_check) + check_modify = "MODIFY" in str(acl_check) allowed = False - item_type = 'UNKNOWN' + item_type = "UNKNOWN" if acl_check == ACLCheck.OSINT_SOURCE_GROUP_ACCESS: item_type = "OSINT Source Group" @@ -108,14 +204,23 @@ def check_acl(item_id, acl_check, user): if not allowed: if check_access: - log_manager.store_user_auth_error_activity(user, "Unauthorized access attempt to {}: {}".format(item_type, item_id)) + log_manager.store_user_auth_error_activity(user, f"Unauthorized access attempt to {item_type}: {item_id}") else: - log_manager.store_user_auth_error_activity(user, "Unauthorized modification attempt to {}: {}".format(item_type, item_id)) + log_manager.store_user_auth_error_activity(user, f"Unauthorized modification attempt to {item_type}: {item_id}") return allowed def no_auth(fn): + """Allow access to the decorated function without authentication. + + Arguments: + fn (function): The function to be decorated. + + Returns: + function: The decorated function. + """ + @wraps(fn) def wrapper(*args, **kwargs): log_manager.store_activity("API_ACCESS", None) @@ -125,6 +230,14 @@ def wrapper(*args, **kwargs): def get_id_name_by_acl(acl): + """Return the corresponding ID name based on the given ACL. + + Arguments: + acl -- The ACL object. + + Returns: + The ID name associated with the ACL. + """ if "NEWS_ITEM" in acl.name: return "item_id" elif "REPORT_ITEM" in acl.name: @@ -136,30 +249,28 @@ def get_id_name_by_acl(acl): def get_user_from_api_key(): - """ - Try to authenticate the user by API key + """Try to authenticate the user by API key. Returns: (user) user: User object or None """ try: - if 'Authorization' not in request.headers or not request.headers['Authorization'].__contains__('Bearer '): + if "Authorization" not in request.headers or not request.headers["Authorization"].__contains__("Bearer "): return None - key_string = request.headers['Authorization'].replace('Bearer ', '') + key_string = request.headers["Authorization"].replace("Bearer ", "") api_key = ApiKey.find_by_key(key_string) if not api_key: return None user = User.find_by_id(api_key.user_id) return user except Exception as ex: - log_manager.store_auth_error_activity("Apikey check presence error: " + str(ex)) + log_manager.store_auth_error_activity(f"ApiKey check presence error: {ex}") return None def get_perm_from_user(user): - """ - Get user permmisions + """Get user permissions. Returns: (all_user_perms) @@ -174,13 +285,12 @@ def get_perm_from_user(user): all_users_perms = all_users_perms.union(role_perms) return all_users_perms except Exception as ex: - log_manager.store_auth_error_activity("Get permmision from user error: " + str(ex)) + log_manager.store_auth_error_activity(f"Get permission from user error: {ex}") return None def get_user_from_jwt_token(): - """ - Try to authenticate the user by API key + """Try to authenticate the user by API key. Returns: (user) @@ -195,43 +305,55 @@ def get_user_from_jwt_token(): # does it encode an identity? identity = get_jwt_identity() if not identity: - log_manager.store_auth_error_activity("Missing identity in JWT: " + get_raw_jwt()) + log_manager.store_auth_error_activity(f"Missing identity in JWT: {get_jwt()}") return None user = User.find(identity) if not user: - log_manager.store_auth_error_activity("Unknown identity in JWT: {}".format(identity)) + log_manager.store_auth_error_activity(f"Unknown identity in JWT: {identity}") return None return user def get_perm_from_jwt_token(user): - """ - Get user permmisions + """Get user permissions from JWT token. + + Args: + user: The user object. Returns: - (all_user_perms) - all_users_perms: set of user's Permissions or None + A set of user's permissions if available, otherwise None. """ try: # does it include permissions? - claims = get_jwt_claims() - if not claims or 'permissions' not in claims: + jwt_data = get_jwt() + claims = jwt_data["user_claims"] + if not claims or "permissions" not in claims: log_manager.store_user_auth_error_activity(user, "Missing permissions in JWT") return None - all_users_perms = set(claims['permissions']) + all_users_perms = set(claims["permissions"]) return all_users_perms except Exception as ex: - log_manager.store_auth_error_activity("Get permmision from JWT error: " + str(ex)) + log_manager.store_auth_error_activity(f"Get permission from JWT error: {ex}") return None def auth_required(required_permissions, *acl_args): + """Check if the user has the required permissions and ACL access. + + Arguments: + required_permissions (str or list): The required permissions for the user. + *acl_args: Additional arguments for ACL access. + + Returns: + The decorated function. + """ + def auth_required_wrap(fn): @wraps(fn) def wrapper(*args, **kwargs): - error = ({'error': 'not authorized'}, 401) + error = ({"error": "not authorized"}, 401) if isinstance(required_permissions, list): required_permissions_set = set(required_permissions) @@ -251,12 +373,12 @@ def wrapper(*args, **kwargs): # is there at least one match with the permissions required by the call? if not required_permissions_set.intersection(active_permissions_set): - log_manager.store_user_auth_error_activity(user, "Insufficient permissions for user: {}".format(user.username)) + log_manager.store_user_auth_error_activity(user, f"Insufficient permissions for user: {user.username}") return error # if the object does have an ACL, do we match it? if len(acl_args) > 0 and not check_acl(kwargs[get_id_name_by_acl(acl_args[0])], acl_args[0], user): - log_manager.store_user_auth_error_activity(user, "Access denied by ACL for user: {}".format(user.username)) + log_manager.store_user_auth_error_activity(user, f"Access denied by ACL for user: {user.username}") return error # allow @@ -264,30 +386,40 @@ def wrapper(*args, **kwargs): return fn(*args, **kwargs) return wrapper + return auth_required_wrap def api_key_required(fn): + """Check for the presence of an API key in the Authorization header. + + Arguments: + fn: The function to be decorated. + + Returns: + The decorated function. + """ + @wraps(fn) def wrapper(*args, **kwargs): - error = ({'error': 'not authorized'}, 401) + error = ({"error": "not authorized"}, 401) # do we have the authorization header? - if 'Authorization' not in request.headers: + if "Authorization" not in request.headers: log_manager.store_auth_error_activity("Missing Authorization header for external access") return error # is it properly encoded? - auth_header = request.headers['Authorization'] - if not auth_header.startswith('Bearer'): + auth_header = request.headers["Authorization"] + if not auth_header.startswith("Bearer"): log_manager.store_auth_error_activity("Missing Authorization Bearer for external access") return error # does it match some of our collector's keys? - api_key = auth_header.replace('Bearer ', '') + api_key = auth_header.replace("Bearer ", "") if not CollectorsNode.exists_by_api_key(api_key): api_key = log_manager.sensitive_value(api_key) - log_manager.store_auth_error_activity("Incorrect api key: " + api_key + " for external access") + log_manager.store_auth_error_activity(f"Incorrect api key: {api_key} for external access") return error # allow @@ -297,26 +429,36 @@ def wrapper(*args, **kwargs): def access_key_required(fn): + """Check for access key authorization. + + This decorator can be used to protect routes or functions that require access key authorization. + It checks if the request has a valid access key in the Authorization header. + + Arguments: + fn (function): The function to be decorated. + + Returns: + function: The decorated function. + """ + @wraps(fn) def wrapper(*args, **kwargs): - error = ({'error': 'not authorized'}, 401) + error = ({"error": "not authorized"}, 401) # do we have the authorization header? - if 'Authorization' not in request.headers: + if "Authorization" not in request.headers: log_manager.store_auth_error_activity("Missing Authorization header for remote access") return error # is it properly encoded? - auth_header = request.headers['Authorization'] - if not auth_header.startswith('Bearer'): + auth_header = request.headers["Authorization"] + if not auth_header.startswith("Bearer"): log_manager.store_auth_error_activity("Missing Authorization Bearer for remote access") return error # does it match some of our remote peer's access keys? - if not RemoteAccess.exists_by_access_key(auth_header.replace('Bearer ', '')): - log_manager.store_auth_error_activity("Incorrect access key: " - + auth_header.replace('Bearer ', - '') + " for remote access") + if not RemoteAccess.exists_by_access_key(auth_header.replace("Bearer ", "")): + log_manager.store_auth_error_activity(f"Incorrect access key: {auth_header.replace('Bearer ', '')} for remote access") return error # allow @@ -326,24 +468,32 @@ def wrapper(*args, **kwargs): def jwt_required(fn): + """Check if a valid JWT is present in the request headers. + + Arguments: + fn -- The function to be decorated. + + Returns: + The decorated function. + """ + @wraps(fn) def wrapper(*args, **kwargs): - try: verify_jwt_in_request() except JWTExtendedException: log_manager.store_auth_error_activity("Missing JWT") - return {'error': 'authorization required'}, 401 + return {"error": "authorization required"}, 401 identity = get_jwt_identity() if not identity: - log_manager.store_auth_error_activity("Missing identity in JWT: {}".format(get_raw_jwt())) - return {'error': 'authorization failed'}, 401 + log_manager.store_auth_error_activity(f"Missing identity in JWT: {get_jwt()}") + return {"error": "authorization failed"}, 401 user = User.find(identity) if user is None: - log_manager.store_auth_error_activity("Unknown identity: ".format(identity)) - return {'error': 'authorization failed'}, 401 + log_manager.store_auth_error_activity(f"Unknown identity: {identity}") + return {"error": "authorization failed"}, 401 log_manager.store_user_activity(user, "API_ACCESS", "Access permitted") return fn(*args, **kwargs) @@ -352,11 +502,26 @@ def wrapper(*args, **kwargs): def get_access_key(): - return request.headers['Authorization'].replace('Bearer ', '') + """Get the access key from the request headers. + + This function retrieves the access key from the "Authorization" header of the request. + The access key is expected to be in the format "Bearer ". + + Returns: + The access key extracted from the request headers. + """ + return request.headers["Authorization"].replace("Bearer ", "") def get_user_from_jwt(): - # obtain the identity and current permissions + """Obtain the identity and current permissions. + + This function first tries to obtain the user from the API key. + If the user is not found, it then tries to obtain the user from the JWT token. + + Returns: + The user object if found, None otherwise. + """ user = get_user_from_api_key() if user is None: user = get_user_from_jwt_token() @@ -364,21 +529,41 @@ def get_user_from_jwt(): def decode_user_from_jwt(jwt_token): + """Decode the user from a JWT token. + + Arguments: + jwt_token (str): The JWT token to decode. + + Returns: + User: The user object decoded from the JWT token. + """ decoded = None try: - decoded = jwt.decode(jwt_token, os.getenv('JWT_SECRET_KEY')) + decoded = jwt.decode(jwt_token, os.getenv("JWT_SECRET_KEY")) except Exception as ex: # e.g. "Signature has expired" log_manager.store_auth_error_activity("Invalid JWT: " + str(ex)) if decoded is None: return None - return User.find(decoded['sub']) + return User.find(decoded["sub"]) def get_external_permissions_ids(): + """Return a list of external permissions IDs. + + This function returns a list of permission IDs that are related to accessing, creating, and configuring assets. + + Returns: + list: A list of external permission IDs. + """ return ["MY_ASSETS_ACCESS", "MY_ASSETS_CREATE", "MY_ASSETS_CONFIG"] def get_external_permissions(): + """Get the external permissions. + + Returns: + A list of external permissions. + """ permissions = [] for permission_id in get_external_permissions_ids(): permissions.append(Permission.find(permission_id)) From 2e607d4825bf86bc64f486f3fb12b646cbf5611f Mon Sep 17 00:00:00 2001 From: multiflexi Date: Sun, 3 Mar 2024 21:26:22 +0100 Subject: [PATCH 4/8] remove flask_script, use click --- src/core/manage.py | 758 ++++++++++++++++++++++++--------------------- 1 file changed, 412 insertions(+), 346 deletions(-) diff --git a/src/core/manage.py b/src/core/manage.py index 2f0474ea0..5d315321e 100755 --- a/src/core/manage.py +++ b/src/core/manage.py @@ -1,4 +1,5 @@ #! /usr/bin/env python +"""This script is used to manage user accounts, roles, and collectors in the Taranis-NG application.""" from os import abort, getenv, read import random @@ -6,20 +7,18 @@ import string import time import logging +import click from flask import Flask -from flask_script import Manager,Command -from flask_script.commands import Option import traceback from managers import db_manager -from model import * +from model import user, role, permission, collectors_node, collector from model import apikey from remote.collectors_api import CollectorsApi app = Flask(__name__) -app.config.from_object('config.Config') -manager = Manager(app=app) -app.logger = logging.getLogger('gunicorn.error') +app.config.from_object("config.Config") +app.logger = logging.getLogger("gunicorn.error") app.logger.level = logging.INFO db_manager.initialize(app) @@ -28,48 +27,90 @@ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) while True: try: - s.connect((app.config.get('DB_URL'), 5432)) + s.connect((app.config.get("DB_URL"), 5432)) s.close() break - except socket.error as ex: + except socket.error: time.sleep(0.1) -# user account management -class AccountManagement(Command): - - option_list = ( - Option('--list', '-l', dest='opt_list', action='store_true'), - Option('--create', '-c', dest='opt_create', action='store_true'), - Option('--edit', '-e', dest='opt_edit', action='store_true'), - Option('--delete', '-d', dest='opt_delete', action='store_true'), - Option('--username', dest='opt_username'), - Option('--name', dest='opt_name', default=""), - Option('--password', dest='opt_password'), - Option('--roles', dest='opt_roles'), - ) - - def run(self, opt_list, opt_create, opt_edit, opt_delete, opt_username, opt_name, opt_password, opt_roles): - - if (opt_list): - users = user.User.get_all() - for us in users: - roles = [] - for r in us.roles: - roles.append(r.id) - print('Id: {}\n\tUsername: {}\n\tName: {}\n\tRoles: {}'.format(us.id, us.username, us.name, roles)) - exit() - - if (opt_create): - if (not opt_username or not opt_password or not opt_roles): - app.logger.critical("Username, password or role not specified!") - abort() - if user.User.find(opt_username): - app.logger.critical("User already exists!") +@app.cli.command("account") +@click.option("--list", "-l", "opt_list", is_flag=True) +@click.option("--create", "-c", "opt_create", is_flag=True) +@click.option("--edit", "-e", "opt_edit", is_flag=True) +@click.option("--delete", "-d", "opt_delete", is_flag=True) +@click.option("--username", "opt_username") +@click.option("--name", "opt_name", default="") +@click.option("--password", "opt_password") +@click.option("--roles", "opt_roles") +def account_management(opt_list, opt_create, opt_edit, opt_delete, opt_username, opt_name, opt_password, opt_roles): + """Manage user accounts. + + Args: + opt_list (bool): List all user accounts. + opt_create (bool): Create a new user account. + opt_edit (bool): Edit an existing user account. + opt_delete (bool): Delete an existing user account. + opt_username (str): Username of the user account. + opt_name (str): Name of the user. + opt_password (str): Password of the user account. + opt_roles (str): Roles assigned to the user account. + """ + if opt_list: + users = user.User.get_all() + for us in users: + roles = [] + for r in us.roles: + roles.append(r.id) + print("Id: {}\n\tUsername: {}\n\tName: {}\n\tRoles: {}".format(us.id, us.username, us.name, roles)) + exit() + + if opt_create: + if not opt_username or not opt_password or not opt_roles: + app.logger.critical("Username, password or role not specified!") + abort() + + if user.User.find(opt_username): + app.logger.critical("User already exists!") + abort() + + opt_roles = opt_roles.split(",") + roles = [] + for ro in opt_roles: + r = None + try: + r = role.Role.find(int(ro)) + except Exception: + r = role.Role.find_by_name(ro) + + if not r: + app.logger.critical("The specified role '{}' does not exist!".format(ro)) abort() - opt_roles = opt_roles.split(',') + roles.append(r) + + new_user = user.User(-1, opt_username, opt_name, opt_password, None, roles, None) + db_manager.db.session.add(new_user) + db_manager.db.session.commit() + + print("User '{}' with id {} created.".format(opt_name, new_user.id)) + + if opt_edit: + if not opt_username: + app.logger.critical("Username not specified!") + abort() + if not opt_password or not opt_roles: + app.logger.critical("Please specify a new password or role id!") + abort() + + if not user.User.find(opt_username): + app.logger.critical("User does not exist!") + abort() + + if opt_roles: + opt_roles = opt_roles.split(",") roles = [] + for ro in opt_roles: r = None try: @@ -83,362 +124,387 @@ def run(self, opt_list, opt_create, opt_edit, opt_delete, opt_username, opt_name roles.append(r) - new_user = user.User(-1, opt_username, opt_name, opt_password, None, roles, None) - db_manager.db.session.add(new_user) - db_manager.db.session.commit() - - print('User \'{}\' with id {} created.'.format(opt_name, new_user.id)) - - if (opt_edit): - if (not opt_username): - app.logger.critical("Username not specified!") - abort() - if (not opt_password or not opt_roles): - app.logger.critical("Please specify a new password or role id!") - abort() + if opt_delete: + if not opt_username: + app.logger.critical("Username not specified!") + abort() - if not user.User.find(opt_username): - app.logger.critical("User does not exist!") - abort() + u = user.User.find(opt_username) + if not u: + app.logger.critical("User does not exist!") + abort() - if (opt_roles): - opt_roles = opt_roles.split(',') - roles = [] + user.User.delete(u.id) + print("The user '{}' has been deleted.".format(opt_username)) + + +@app.cli.command("role") +@click.option("--list", "-l", "opt_list", is_flag=True) +@click.option("--create", "-c", "opt_create", is_flag=True) +@click.option("--edit", "-e", "opt_edit", is_flag=True) +@click.option("--delete", "-d", "opt_delete", is_flag=True) +@click.option("--filter", "-f", "opt_filter") +@click.option("--id", "opt_id") +@click.option("--name", "opt_name") +@click.option("--description", "opt_description", default="") +@click.option("--permissions", "opt_permissions") +def role_management(opt_list, opt_create, opt_edit, opt_delete, opt_filter, opt_id, opt_name, opt_description, opt_permissions): + """Manage roles. + + Args: + opt_list (bool): List all roles. + opt_create (bool): Create a new role. + opt_edit (bool): Edit an existing role. + opt_delete (bool): Delete an existing role. + opt_filter (str): Filter roles by name. + opt_id (str): ID of the role. + opt_name (str): Name of the role. + opt_description (str): Description of the role. + opt_permissions (str): Permissions assigned to the role. + """ + if opt_list: + roles = None + if opt_filter: + roles = role.Role.get(opt_filter)[0] + else: + roles = role.Role.get_all() + + for ro in roles: + perms = [] + for p in ro.permissions: + perms.append(p.id) + print("Id: {}\n\tName: {}\n\tDescription: {}\n\tPermissions: {}".format(ro.id, ro.name, ro.description, perms)) + exit() - for ro in opt_roles: - r = None - try: - r = role.Role.find(int(ro)) - except Exception: - r = role.Role.find_by_name(ro) + if opt_create: + if not opt_name or not opt_permissions: + app.logger.critical("Role name or permissions not specified!") + abort() - if not r: - app.logger.critical("The specified role '{}' does not exist!".format(ro)) - abort() + opt_permissions = opt_permissions.split(",") + perms = [] - roles.append(r) + for pe in opt_permissions: + p = permission.Permission.find(pe) - if (opt_delete): - if (not opt_username): - app.logger.critical("Username not specified!") + if not p: + app.logger.critical("The specified permission '{}' does not exist!".format(pe)) abort() - u = user.User.find(opt_username) - if not u: - app.logger.critical("User does not exist!") - abort() + perms.append(p) - user.User.delete(u.id) - print('The user \'{}\' has been deleted.'.format(opt_username)) - -# role management -class RoleManagement(Command): - - option_list = ( - Option('--list', '-l', dest='opt_list', action='store_true'), - Option('--create', '-c', dest='opt_create', action='store_true'), - Option('--edit', '-e', dest='opt_edit', action='store_true'), - Option('--delete', '-d', dest='opt_delete', action='store_true'), - Option('--filter', '-f', dest='opt_filter'), - Option('--id', dest='opt_id'), - Option('--name', dest='opt_name'), - Option('--description', dest='opt_description', default=""), - Option('--permissions', dest='opt_permissions'), - ) - - def run(self, opt_list, opt_create, opt_edit, opt_delete, opt_filter, opt_id, opt_name, opt_description, opt_permissions): - - if (opt_list): - roles = None - if (opt_filter): - roles = role.Role.get(opt_filter)[0] - else: - roles = role.Role.get_all() - - for ro in roles: - perms = [] - for p in ro.permissions: - perms.append(p.id) - print('Id: {}\n\tName: {}\n\tDescription: {}\n\tPermissions: {}'.format(ro.id, ro.name, ro.description, perms)) - exit() - - if (opt_create): - if (not opt_name or not opt_permissions): - app.logger.critical("Role name or permissions not specified!") - abort() + new_role = role.Role(-1, opt_name, opt_description, perms) + db_manager.db.session.add(new_role) + db_manager.db.session.commit() - opt_permissions = opt_permissions.split(',') - perms = [] + print("Role '{}' with id {} created.".format(opt_name, new_role.id)) - for pe in opt_permissions: - p = permission.Permission.find(pe) + if opt_edit: + if not opt_id or not opt_name: + app.logger.critical("Role id or name not specified!") + abort() + if not opt_name or not opt_description or not opt_permissions: + app.logger.critical("Please specify a new name, description or permissions!") + abort() - if not p: - app.logger.critical("The specified permission '{}' does not exist!".format(pe)) - abort() + if opt_delete: + if not opt_id or not opt_name: + app.logger.critical("Role id or name not specified!") + abort() - perms.append(p) - new_role = role.Role(-1, opt_name, opt_description, perms) - db_manager.db.session.add(new_role) - db_manager.db.session.commit() +@app.cli.command("collector") +@click.option("--list", "-l", "opt_list", is_flag=True) +@click.option("--create", "-c", "opt_create", is_flag=True) +@click.option("--edit", "-e", "opt_edit", is_flag=True) +@click.option("--delete", "-d", "opt_delete", is_flag=True) +@click.option("--update", "-u", "opt_update", is_flag=True) +@click.option("--all", "-a", "opt_all", is_flag=True) +@click.option("--show-api-key", "opt_show_api_key", is_flag=True) +@click.option("--id", "opt_id") +@click.option("--name", "opt_name") +@click.option("--description", "opt_description", default="") +@click.option("--api-url", "opt_api_url") +@click.option("--api-key", "opt_api_key") +def collector_management( + opt_list, + opt_create, + opt_edit, + opt_delete, + opt_update, + opt_all, + opt_show_api_key, + opt_id, + opt_name, + opt_description, + opt_api_url, + opt_api_key, +): + """Manage collectors. + + Args: + opt_list (bool): List all collectors. + opt_create (bool): Create a new collector. + opt_edit (bool): Edit an existing collector. + opt_delete (bool): Delete an existing collector. + opt_update (bool): Update collectors. + opt_all (bool): Update all collectors. + opt_show_api_key (bool): Show API key in the output. + opt_id (str): ID of the collector. + opt_name (str): Name of the collector. + opt_description (str): Description of the collector. + opt_api_url (str): API URL of the collector. + opt_api_key (str): API key of the collector. + """ + if opt_list: + collector_nodes = collectors_node.CollectorsNode.get_all() + + for node in collector_nodes: + capabilities = [] + sources = [] + for c in node.collectors: + capabilities.append(c.type) + for s in c.sources: + sources.append("{} ({})".format(s.name, s.id)) + print( + "Id: {}\n\tName: {}\n\tURL: {}\n\t{}Created: {}\n\tLast seen: {}\n\tCapabilities: {}\n\tSources: {}".format( + node.id, + node.name, + node.api_url, + "API key: {}\n\t".format(node.api_key) if opt_show_api_key else "", + node.created, + node.last_seen, + capabilities, + sources, + ) + ) + exit() - print('Role \'{}\' with id {} created.'.format(opt_name, new_role.id)) + if opt_create: + if not opt_name or not opt_api_url or not opt_api_key: + app.logger.critical("Please specify the collector node name, API url and key!") + abort() - if (opt_edit): - if (not opt_id or not opt_name): - app.logger.critical("Role id or name not specified!") - abort() - if (not opt_name or not opt_description or not opt_permissions): - app.logger.critical("Please specify a new name, description or permissions!") - abort() + data = { + "id": "", + "name": opt_name, + "description": opt_description if opt_description else "", + "api_url": opt_api_url, + "api_key": opt_api_key, + "collectors": [], + "status": "0", + } + + print("Trying to contact a new collector node...") + retries, max_retries = 0, 30 + while retries < max_retries: + try: + collectors_info, status_code = CollectorsApi(opt_api_url, opt_api_key).get_collectors_info("") + break + except: # noqa: E722 + collectors_info = "Collector unavailable" + status_code = 0 + time.sleep(1) + retries += 1 + print("Retrying [{}/{}]...".format(retries, max_retries)) + + if status_code != 200: + print("Cannot create a new collector node!") + print("Response from collector: {}".format(collectors_info)) + abort() - if (opt_delete): - if (not opt_id or not opt_name): - app.logger.critical("Role id or name not specified!") - abort() + collectors = collector.Collector.create_all(collectors_info) + node = collectors_node.CollectorsNode.add_new(data, collectors) + collectors_info, status_code = CollectorsApi(opt_api_url, opt_api_key).get_collectors_info(node.id) -# collector management -class CollectorManagement(Command): - - option_list = ( - Option('--list', '-l', dest='opt_list', action='store_true'), - Option('--create', '-c', dest='opt_create', action='store_true'), - Option('--edit', '-e', dest='opt_edit', action='store_true'), - Option('--delete', '-d', dest='opt_delete', action='store_true'), - Option('--update', '-u', dest='opt_update', action='store_true'), - Option('--all', '-a', dest='opt_all', action='store_true'), - Option('--show-api-key', dest='opt_show_api_key', action='store_true'), - Option('--id', dest='opt_id'), - Option('--name', dest='opt_name'), - Option('--description', dest='opt_description', default=""), - Option('--api-url', dest='opt_api_url'), - Option('--api-key', dest='opt_api_key'), - ) - - def run(self, opt_list, opt_create, opt_edit, opt_delete, opt_update, opt_all, opt_show_api_key, opt_id, opt_name, opt_description, opt_api_url, opt_api_key): - if (opt_list): - collector_nodes = collectors_node.CollectorsNode.get_all() - - for node in collector_nodes: - capabilities = [] - sources = [] - for c in node.collectors: - capabilities.append(c.type) - for s in c.sources: - sources.append('{} ({})'.format(s.name, s.id)) - print('Id: {}\n\tName: {}\n\tURL: {}\n\t{}Created: {}\n\tLast seen: {}\n\tCapabilities: {}\n\tSources: {}'.format(node.id, node.name, node.api_url, 'API key: {}\n\t'.format(node.api_key) if opt_show_api_key else '', node.created, node.last_seen, capabilities, sources)) - exit() - - if (opt_create): - if (not opt_name or not opt_api_url or not opt_api_key): - app.logger.critical("Please specify the collector node name, API url and key!") - abort() + print("Collector node '{}' with id {} created.".format(opt_name, node.id)) - data = { - 'id': '', - 'name': opt_name, - 'description': opt_description if opt_description else '', - 'api_url': opt_api_url, - 'api_key': opt_api_key, - 'collectors': [], - 'status': '0' - } - - print('Trying to contact a new collector node...') - retries, max_retries = 0, 30 - while retries < max_retries: - try: - collectors_info, status_code = CollectorsApi(opt_api_url, opt_api_key).get_collectors_info("") - break; - except: - collectors_info = 'Collector unavailable' - status_code = 0 - time.sleep(1) - retries += 1 - print('Retrying [{}/{}]...'.format(retries, max_retries)) - - - if status_code != 200: - print('Cannot create a new collector node!') - print('Response from collector: {}'.format(collectors_info)) - abort() + if opt_edit: + if not opt_id or not opt_name: + app.logger.critical("Collector node id or name not specified!") + abort() + if not opt_name or not opt_description or not opt_api_url or not opt_api_key: + app.logger.critical("Please specify a new name, description, API url or key!") + abort() - collectors = collector.Collector.create_all(collectors_info) - node = collectors_node.CollectorsNode.add_new(data, collectors) - collectors_info, status_code = CollectorsApi(opt_api_url, opt_api_key).get_collectors_info(node.id) + if opt_delete: + if not opt_id or not opt_name: + app.logger.critical("Collector node id or name not specified!") + abort() - print('Collector node \'{}\' with id {} created.'.format(opt_name, node.id)) + if opt_update: + if not opt_all and not opt_id and not opt_name: + app.logger.critical("Collector node id or name not specified!") + app.logger.critical("If you want to update all collectors, pass the --all parameter.") + abort() - if (opt_edit): - if (not opt_id or not opt_name): - app.logger.critical("Collector node id or name not specified!") + nodes = None + if opt_id: + nodes = [collectors_node.CollectorsNode.get_by_id(opt_id)] + if not nodes: + app.logger.critical("Collector node does not exit!") abort() - if (not opt_name or not opt_description or not opt_api_url or not opt_api_key): - app.logger.critical("Please specify a new name, description, API url or key!") + elif opt_name: + nodes, count = collectors_node.CollectorsNode.get(opt_name) + if not count: + app.logger.critical("Collector node does not exit!") abort() - - if (opt_delete): - if (not opt_id or not opt_name): - app.logger.critical("Collector node id or name not specified!") + else: + nodes, count = collectors_node.CollectorsNode.get(None) + if not count: + app.logger.critical("No collector nodes exist!") abort() - if (opt_update): - if (not opt_all and not opt_id and not opt_name): - app.logger.critical("Collector node id or name not specified!") - app.logger.critical("If you want to update all collectors, pass the --all parameter.") - abort() - - nodes = None - if opt_id: - nodes = [ collectors_node.CollectorsNode.get_by_id(opt_id) ] - if not nodes: - app.logger.critical("Collector node does not exit!") - abort() - elif opt_name: - nodes, count = collectors_node.CollectorsNode.get(opt_name) - if not count: - app.logger.critical("Collector node does not exit!") - abort() + for node in nodes: + # refresh collector node id + collectors_info, status_code = CollectorsApi(node.api_url, node.api_key).get_collectors_info(node.id) + if status_code == 200: + print("Collector node {} updated.".format(node.id)) else: - nodes, count = collectors_node.CollectorsNode.get(None) - if not count: - app.logger.critical("No collector nodes exist!") - abort() + print("Unable to update collector node {}.\n\tResponse: [{}] {}.".format(node.id, status_code, collectors_info)) - for node in nodes: - # refresh collector node id - collectors_info, status_code = CollectorsApi(node.api_url, node.api_key).get_collectors_info(node.id) - if status_code == 200: - print('Collector node {} updated.'.format(node.id)) - else: - print('Unable to update collector node {}.\n\tResponse: [{}] {}.'.format(node.id, status_code, collectors_info)) - -# dictionary management -class DictionaryManagement(Command): - - option_list = ( - Option('--upload-cve', dest='opt_cve', action='store_true'), - Option('--upload-cpe', dest='opt_cpe', action='store_true'), - ) - - def run(self, opt_cve, opt_cpe): - from model import attribute - - if (opt_cve): - cve_update_file = getenv('CVE_UPDATE_FILE') - if cve_update_file is None: - app.logger.critical("CVE_UPDATE_FILE is undefined") - abort() - self.upload_to(cve_update_file) - try: - attribute.Attribute.load_dictionaries('cve') - except Exception: - app.logger.debug(traceback.format_exc()) - app.logger.critical("File structure was not recognized!") - abort() +@app.cli.command("dictionary") +@click.option("--upload-cve", is_flag=True) +@click.option("--upload-cpe", is_flag=True) +def dictionary_management(upload_cve, upload_cpe): + """Manage the dictionaries by uploading and loading CVE and CPE files. - if (opt_cpe): - cpe_update_file = getenv('CPE_UPDATE_FILE') - if cpe_update_file is None: - app.logger.critical("CPE_UPDATE_FILE is undefined") - abort() + This function uploads the CVE and CPE files and loads the dictionaries accordingly. + If `upload_cve` is True, it uploads the CVE file and loads the CVE dictionary. + If `upload_cpe` is True, it uploads the CPE file and loads the CPE dictionary. - self.upload_to(cpe_update_file) - try: - attribute.Attribute.load_dictionaries('cpe') - except Exception: - app.logger.debug(traceback.format_exc()) - app.logger.critical("File structure was not recognized!") - abort() + Arguments: + upload_cve (bool): Indicates whether to upload the CVE file and load the CVE dictionary. + upload_cpe (bool): Indicates whether to upload the CPE file and load the CPE dictionary. + """ + from model import attribute - app.logger.error("Dictionary was uploaded.") - exit() + if upload_cve: + cve_update_file = getenv("CVE_UPDATE_FILE") + if cve_update_file is None: + app.logger.critical("CVE_UPDATE_FILE is undefined") + abort() - def upload_to(self, filename): + upload_to(cve_update_file) try: - with open(filename, 'wb') as out_file: - while True: - chunk = read(0, 131072) - if not chunk: - break - out_file.write(chunk) + attribute.Attribute.load_dictionaries("cve") except Exception: app.logger.debug(traceback.format_exc()) - app.logger.critical("Upload failed!") + app.logger.critical("File structure was not recognized!") abort() -# ApiKeys management -class ApiKeysManagement(Command): + if upload_cpe: + cpe_update_file = getenv("CPE_UPDATE_FILE") + if cpe_update_file is None: + app.logger.critical("CPE_UPDATE_FILE is undefined") + abort() - option_list = ( - Option('--list', '-l', dest='opt_list', action='store_true'), - Option('--create', '-c', dest='opt_create', action='store_true'), - Option('--delete', '-d', dest='opt_delete', action='store_true'), - Option('--name', '-n', dest='opt_name'), - Option('--user', '-u', dest='opt_user'), - Option('--expires', '-e', dest='opt_expires') - ) + upload_to(cpe_update_file) + try: + attribute.Attribute.load_dictionaries("cpe") + except Exception: + app.logger.debug(traceback.format_exc()) + app.logger.critical("File structure was not recognized!") + abort() + app.logger.error("Dictionary was uploaded.") + exit() - def run(self, opt_list, opt_create, opt_delete, opt_name, opt_user, opt_expires): - if (opt_list): - apikeys = apikey.ApiKey.get_all() - for k in apikeys: - print('Id: {}\n\tName: {}\n\tKey: {}\n\tCreated: {}\n\tUser id: {}\n\tExpires: {}'.format(k.id, k.name, k.key, k.created_at, k.user_id, k.expires_at)) - exit() +def upload_to(filename): + """Upload a file to the specified filename. - if (opt_create): - if (not opt_name): - app.logger.critical("Name not specified!") - abort() + Arguments: + filename (str): The name of the file to upload. + """ + try: + with open(filename, "wb") as out_file: + while True: + chunk = read(0, 131072) + if not chunk: + break + out_file.write(chunk) + except Exception: + app.logger.debug(traceback.format_exc()) + app.logger.critical("Upload failed!") + abort() + + +@app.cli.command("apikey") +@click.option("--list", "-l", "opt_list", is_flag=True) +@click.option("--create", "-c", "opt_create", is_flag=True) +@click.option("--delete", "-d", "opt_delete", is_flag=True) +@click.option("--name", "-n", "opt_name") +@click.option("--user", "-u", "opt_user") +@click.option("--expires", "-e", "opt_expires") +def api_keys_management(opt_list, opt_create, opt_delete, opt_name, opt_user, opt_expires): + """Manage API keys. + + This function provides functionality to list, create, and delete API keys. + + Arguments: + opt_list (bool): If True, list all existing API keys. + opt_create (bool): If True, create a new API key. + opt_delete (bool): If True, delete an existing API key. + opt_name (str): The name of the API key. + opt_user (str): The user associated with the API key. + opt_expires (str): The expiration date of the API key. + """ + if opt_list: + apikeys = apikey.ApiKey.get_all() + for k in apikeys: + print( + "Id: {}\n\tName: {}\n\tKey: {}\n\tCreated: {}\n\tUser id: {}\n\tExpires: {}".format( + k.id, k.name, k.key, k.created_at, k.user_id, k.expires_at + ) + ) + exit() - if apikey.ApiKey.find_by_name(opt_name): - app.logger.critical("Name already exists!") - abort() + if opt_create: + if not opt_name: + app.logger.critical("Name not specified!") + abort() - if (not opt_user): - app.logger.critical("User not specified!") - abort() + if apikey.ApiKey.find_by_name(opt_name): + app.logger.critical("Name already exists!") + abort() - u = None - if opt_user: - u = user.User.find(opt_user) - if not u: - app.logger.critical("The specified user '{}' does not exist!".format(opt_user)) - abort() + if not opt_user: + app.logger.critical("User not specified!") + abort() - data = { - #'id': None, - 'name': opt_name, - 'key': ''.join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=40)), - 'user_id': u.id, - 'expires_at': opt_expires if opt_expires else None - } + u = None + if opt_user: + u = user.User.find(opt_user) + if not u: + app.logger.critical("The specified user '{}' does not exist!".format(opt_user)) + abort() - k = apikey.ApiKey.add_new(data) - print('ApiKey \'{}\' with id {} created.'.format(opt_name, k.id)) + data = { + # 'id': None, + "name": opt_name, + "key": "".join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=40)), + "user_id": u.id, + "expires_at": opt_expires if opt_expires else None, + } - if (opt_delete): - if (not opt_name): - app.logger.critical("Name not specified!") - abort() + k = apikey.ApiKey.add_new(data) + print("ApiKey '{}' with id {} created.".format(opt_name, k.id)) - k = apikey.ApiKey.find_by_name(opt_name) - if not k: - app.logger.critical("Name not found!") - abort() + if opt_delete: + if not opt_name: + app.logger.critical("Name not specified!") + abort() + + k = apikey.ApiKey.find_by_name(opt_name) + if not k: + app.logger.critical("Name not found!") + abort() - apikey.ApiKey.delete(k.id) - print('ApiKey \'{}\' has been deleted.'.format(opt_name)) + apikey.ApiKey.delete(k.id) + print("ApiKey '{}' has been deleted.".format(opt_name)) -manager.add_command('account', AccountManagement) -manager.add_command('role', RoleManagement) -manager.add_command('collector', CollectorManagement) -manager.add_command('dictionary', DictionaryManagement) -manager.add_command('apikey', ApiKeysManagement) -if __name__ == '__main__': - manager.run() +if __name__ == "__main__": + app.run() From 68eb7edc7c4bb83cd40e1156e591bcf6766d98a0 Mon Sep 17 00:00:00 2001 From: multiflexi Date: Sun, 3 Mar 2024 21:32:14 +0100 Subject: [PATCH 5/8] remove flask_script --- src/core/db_migration.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/core/db_migration.py b/src/core/db_migration.py index 2cf3a9af7..dc74bc7c4 100755 --- a/src/core/db_migration.py +++ b/src/core/db_migration.py @@ -1,16 +1,19 @@ #! /usr/bin/env python +"""This script is responsible for performing database migrations for the Taranis-NG application. +It initializes the Flask application, configures the database manager, and waits for the database to be ready. +Once the database is ready, it performs the necessary migrations using Flask-Migrate. +""" import socket import time from flask import Flask -from flask_script import Manager -from flask_migrate import Migrate, MigrateCommand +from flask_migrate import Migrate from managers import db_manager -from model import * +from model import * # noqa: F401, F403 app = Flask(__name__) -app.config.from_object('config.Config') +app.config.from_object("config.Config") db_manager.initialize(app) @@ -18,17 +21,13 @@ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) while True: try: - s.connect((app.config.get('DB_URL'), 5432)) + s.connect((app.config.get("DB_URL"), 5432)) s.close() break - except socket.error as ex: + except socket.error: time.sleep(0.1) migrate = Migrate(app=app, db=db_manager.db) -manager = Manager(app=app) - -manager.add_command('db', MigrateCommand) - -if __name__ == '__main__': - manager.run() +if __name__ == "__main__": + app.run() From 9ae19281ba3fb736ff6243396ad6f8b6b011cec5 Mon Sep 17 00:00:00 2001 From: multiflexi Date: Tue, 5 Mar 2024 08:16:30 +0100 Subject: [PATCH 6/8] JWT fixes --- src/core/auth/base_authenticator.py | 82 ++++++++++++++++++++++++++--- src/core/managers/auth_manager.py | 2 +- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/core/auth/base_authenticator.py b/src/core/auth/base_authenticator.py index 704638471..aa996606a 100644 --- a/src/core/auth/base_authenticator.py +++ b/src/core/auth/base_authenticator.py @@ -1,3 +1,8 @@ +"""This module contains the `BaseAuthenticator` class which provides the base functionality for authentication. + +The `BaseAuthenticator` class defines methods for authentication, token generation, token refresh, and user logout. +""" + from flask_jwt_extended import create_access_token from managers import log_manager @@ -6,42 +11,103 @@ class BaseAuthenticator: + """Base class for authenticators. + + This class provides the basic structure and methods for implementing an authenticator. + Subclasses should override the methods as needed for specific authentication mechanisms. + + Methods: + get_required_credentials: Return the required credentials for authentication. + authenticate: Authenticate the user based on the provided credentials. + refresh: Refresh the authentication token for the given user. + logout: Logout the user by adding the token to the blacklist. + initialize: Initialize the authenticator. + generate_error: Generate an error response for authentication failure. + generate_jwt: Generate a JSON Web Token (JWT) for the given username. + """ def get_required_credentials(self): + """Return the required credentials for authentication. + + Returns: + A list of required credentials. + """ return [] def authenticate(self, credentials): + """Authenticate the user based on the provided credentials. + + Arguments: + credentials -- The user's credentials. + + Returns: + The result of the authentication process. + """ return BaseAuthenticator.generate_error() def refresh(self, user): + """Refresh the authentication token for the given user. + + Arguments: + user -- The user object. + + Returns: + The refreshed authentication token. + """ return BaseAuthenticator.generate_jwt(user.username) @staticmethod def logout(token): + """Logout the user by adding the token to the blacklist. + + Arguments: + token -- The token to be blacklisted. + """ if token is not None: TokenBlacklist.add(token) @staticmethod def initialize(app): + """Initialize the authenticator. + + Arguments: + app -- The application object. + """ pass @staticmethod def generate_error(): - return {'error': 'Authentication failed'}, 401 + """Generate an error response for authentication failure. + + Returns: + A tuple containing the error message and the HTTP status code. + """ + return {"error": "Authentication failed"}, 401 @staticmethod def generate_jwt(username): + """Generate a JSON Web Token (JWT) for the given username. + + Arguments: + username (str): The username for which to generate the JWT. + Returns: + tuple: A tuple containing the generated access token and the HTTP status code. + """ user = User.find(username) if not user: - log_manager.store_auth_error_activity("User does not exist after authentication: " + username) + log_manager.store_auth_error_activity(f"User does not exist after authentication: {username}") return BaseAuthenticator.generate_error() else: log_manager.store_user_activity(user, "LOGIN", "Successful") - access_token = create_access_token(identity=user.username, - user_claims={'id': user.id, - 'name': user.name, - 'organization_name': user.get_current_organization_name(), - 'permissions': user.get_permissions()}) + access_token = create_access_token( + identity=user.username, + additional_claims={ + "id": user.id, + "name": user.name, + "organization_name": user.get_current_organization_name(), + "permissions": user.get_permissions(), + }, + ) - return {'access_token': access_token}, 200 + return {"access_token": access_token}, 200 diff --git a/src/core/managers/auth_manager.py b/src/core/managers/auth_manager.py index 1616b1251..6d7d1a5a7 100644 --- a/src/core/managers/auth_manager.py +++ b/src/core/managers/auth_manager.py @@ -539,7 +539,7 @@ def decode_user_from_jwt(jwt_token): """ decoded = None try: - decoded = jwt.decode(jwt_token, os.getenv("JWT_SECRET_KEY")) + decoded = jwt.decode(jwt_token, os.getenv("JWT_SECRET_KEY"), algorithms=["HS256"]) except Exception as ex: # e.g. "Signature has expired" log_manager.store_auth_error_activity("Invalid JWT: " + str(ex)) if decoded is None: From 79ed0a2d34207be6754941c0864013b0aa5ae158 Mon Sep 17 00:00:00 2001 From: multiflexi Date: Tue, 5 Mar 2024 08:17:40 +0100 Subject: [PATCH 7/8] add git to build dependencies in dockerfile --- docker/Dockerfile.core | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile.core b/docker/Dockerfile.core index c9afe20a5..62ab44129 100644 --- a/docker/Dockerfile.core +++ b/docker/Dockerfile.core @@ -36,6 +36,7 @@ RUN \ apk add --no-cache --virtual .build-deps \ gcc \ g++ \ + git \ make \ glib-dev \ musl-dev \ From 200475e86455eb22c501d813a94fd491e13b2c30 Mon Sep 17 00:00:00 2001 From: multiflexi Date: Tue, 5 Mar 2024 08:17:57 +0100 Subject: [PATCH 8/8] update requirements --- src/core/requirements.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/core/requirements.txt b/src/core/requirements.txt index bb43dbdd7..60ff3c119 100644 --- a/src/core/requirements.txt +++ b/src/core/requirements.txt @@ -3,16 +3,15 @@ Flask==3.0.2 Flask-Cors==4.0.0 Flask-JWT-Extended==4.6.0 Flask-Migrate==4.0.5 -flask-oidc==1.4.0 +#https://github.com/puiterwijk/flask-oidc/issues/147 +git+https://github.com/puiterwijk/flask-oidc.git@b10e6bf881a3fe0c3972e4093648f2b77f32a97c Flask-RESTful==0.3.10 -#Flask-Script==2.0.6 Flask-SSE==1.0.0 Flask-SQLAlchemy==3.0.5 gevent==24.2.1 gunicorn==21.2.0 Jinja2==3.1.3 ldap3==2.9.1 -# markupsafe==2.0.1 #remove after Jinja2 upgraded marshmallow==3.19.0 marshmallow-enum==1.5.1 psycogreen==1.0.2 @@ -24,5 +23,5 @@ requests==2.31.0 schedule==1.2.1 sseclient-py==1.8.0 SQLAlchemy==1.4.51 #upgrade -Werkzeug==3.0.1 #update +Werkzeug==3.0.1 pycryptodomex==3.20