diff --git a/CHANGELOG.md b/CHANGELOG.md index 917cc3a3..2a862277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ TBD - Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666)) - Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667)) - Add server name tag in panel header (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667)) +- Setup logging for panel authentication attempts ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669)) +- Update minimum password length from 6 to 8, and unrestrict maximum password length ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/669)) ### Lang TBD

diff --git a/app/classes/controllers/users_controller.py b/app/classes/controllers/users_controller.py index ed53ad61..87cc513c 100644 --- a/app/classes/controllers/users_controller.py +++ b/app/classes/controllers/users_controller.py @@ -45,8 +45,7 @@ class UsersController: }, "password": { "type": "string", - "maxLength": 20, - "minLength": 6, + "minLength": 8, "examples": ["crafty"], "title": "Password", }, diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py index 23586696..27104b62 100644 --- a/app/classes/shared/main_controller.py +++ b/app/classes/shared/main_controller.py @@ -78,6 +78,37 @@ class Controller: self.first_login = False self.cached_login = self.management.get_login_image() self.support_scheduler.start() + try: + with open( + os.path.join(os.path.curdir, "logs", "auth_tracker.log"), + "r", + encoding="utf-8", + ) as f: + self.auth_tracker = json.load(f) + except: + self.auth_tracker = {} + + def log_attempt(self, remote_ip, username): + remote = self.auth_tracker.get(str(remote_ip), None) + if remote: + remote["names"].append(username) + remote["attempts"] += 1 + remote["times"].append(datetime.now().strftime("%d/%m/%Y %H:%M:%S")) + self.auth_tracker[str(remote_ip)] = remote + else: + self.auth_tracker[str(remote_ip)] = { + "names": [username], + "attempts": 1, + "times": [datetime.now().strftime("%d/%m/%Y %H:%M:%S")], + } + + def write_auth_tracker(self): + with open( + os.path.join(os.path.curdir, "logs", "auth_tracker.log"), + "w", + encoding="utf-8", + ) as f: + json.dump(self.auth_tracker, f, indent=4) @staticmethod def check_system_user(): diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index 0402c587..ff20e7ec 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -201,6 +201,13 @@ class TasksManager: id="update_watcher", start_date=datetime.datetime.now(), ) + self.scheduler.add_job( + self.controller.write_auth_tracker, + "interval", + minutes=5, + id="auth_tracker_write", + start_date=datetime.datetime.now(), + ) # self.scheduler.add_job( # self.scheduler.print_jobs, "interval", seconds=10, id="-1" # ) diff --git a/app/classes/web/base_handler.py b/app/classes/web/base_handler.py index 2504bc13..d8181b94 100644 --- a/app/classes/web/base_handler.py +++ b/app/classes/web/base_handler.py @@ -14,6 +14,7 @@ from app.classes.shared.translation import Translation from app.classes.shared.main_models import DatabaseShortcuts logger = logging.getLogger(__name__) +auth_log = logging.getLogger("auth") bearer_pattern = re.compile(r"^Bearer ", flags=re.IGNORECASE) @@ -231,9 +232,16 @@ class BaseHandler(tornado.web.RequestHandler): user, ) logging.debug("Auth unsuccessful") + auth_log.error( + f"Authentication attempted from {self.get_remote_ip()}. Invalid token" + ) self.access_denied(None, "the user provided an invalid token") return None except Exception as auth_exception: + auth_log.error( + f"Authentication attempted from {self.get_remote_ip()}." + f" Error: {auth_exception}" + ) logger.debug( "An error occured while authenticating an API user:", exc_info=auth_exception, diff --git a/app/classes/web/public_handler.py b/app/classes/web/public_handler.py index b7d1be9b..017ea554 100644 --- a/app/classes/web/public_handler.py +++ b/app/classes/web/public_handler.py @@ -6,6 +6,7 @@ from app.classes.models.users import HelperUsers from app.classes.web.base_handler import BaseHandler logger = logging.getLogger(__name__) +auth_log = logging.getLogger("auth") class PublicHandler(BaseHandler): @@ -96,6 +97,9 @@ class PublicHandler(BaseHandler): page_data["query"] = self.request.query if page == "login": + auth_log.info( + f"User attempting to authenticate from {self.get_remote_ip()}" + ) next_page = "/login" if self.request.query: next_page = "/login?" + self.request.query @@ -108,6 +112,12 @@ class PublicHandler(BaseHandler): user_id = HelperUsers.get_user_id_by_name(entered_username.lower()) user_data = HelperUsers.get_user_model(user_id) except: + self.controller.log_attempt(self.get_remote_ip(), entered_username) + auth_log.error( + f"User attempted to log into {entered_username}." + f" Authentication failed from remote IP {self.get_remote_ip()}" + " Users does not exist." + ) error_msg = "Incorrect username or password. Please try again." # self.clear_cookie("user") # self.clear_cookie("user_data") @@ -120,6 +130,12 @@ class PublicHandler(BaseHandler): # if we don't have a user if not user_data: + auth_log.error( + f"User attempted to log into {entered_username}. Authentication" + f" failed from remote IP {self.get_remote_ip()}" + " User does not exist." + ) + self.controller.log_attempt(self.get_remote_ip(), entered_username) error_msg = "Incorrect username or password. Please try again." # self.clear_cookie("user") # self.clear_cookie("user_data") @@ -132,6 +148,12 @@ class PublicHandler(BaseHandler): # if they are disabled if not user_data.enabled: + auth_log.error( + f"User attempted to log into {entered_username}. " + f"Authentication failed from remote IP {self.get_remote_ip()}." + " User account disabled" + ) + self.controller.log_attempt(self.get_remote_ip(), entered_username) error_msg = ( "User account disabled. Please contact " "your system administrator for more info." @@ -159,7 +181,11 @@ class PublicHandler(BaseHandler): user_data.last_ip = self.get_remote_ip() user_data.last_login = Helpers.get_time_as_string() user_data.save() - + auth_log.info( + f"{entered_username} successfully" + " authenticated and logged" + f" into panel from remote IP {self.get_remote_ip()}" + ) # log this login self.controller.management.add_to_audit_log( user_data.user_id, "Logged in", 0, self.get_remote_ip() @@ -172,6 +198,11 @@ class PublicHandler(BaseHandler): self.redirect(next_page) else: + auth_log.error( + f"User attempted to log into {entered_username}." + f" Authentication failed from remote IP {self.get_remote_ip()}" + ) + self.controller.log_attempt(self.get_remote_ip(), entered_username) # self.clear_cookie("user") # self.clear_cookie("user_data") self.clear_cookie("token") diff --git a/app/classes/web/routes/api/auth/login.py b/app/classes/web/routes/api/auth/login.py index 84ae2815..b91b295d 100644 --- a/app/classes/web/routes/api/auth/login.py +++ b/app/classes/web/routes/api/auth/login.py @@ -7,7 +7,7 @@ from app.classes.shared.helpers import Helpers from app.classes.web.base_api_handler import BaseApiHandler logger = logging.getLogger(__name__) - +auth_log = logging.getLogger("auth") login_schema = { "type": "object", "properties": { @@ -29,6 +29,10 @@ class ApiAuthLoginHandler(BaseApiHandler): try: data = json.loads(self.request.body) except json.decoder.JSONDecodeError as e: + logger.error( + "Invalid JSON schema for API" + f" login attempt from {self.get_remote_ip()}" + ) return self.finish_json( 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} ) @@ -36,6 +40,10 @@ class ApiAuthLoginHandler(BaseApiHandler): try: validate(data, login_schema) except ValidationError as e: + logger.error( + "Invalid JSON schema for API" + f" login attempt from {self.get_remote_ip()}" + ) return self.finish_json( 400, { @@ -52,12 +60,23 @@ class ApiAuthLoginHandler(BaseApiHandler): user_data = Users.get_or_none(Users.username == username) if user_data is None: + self.controller.log_attempt(self.get_remote_ip(), username) + auth_log.error( + f"User attempted to log into {username}." + " Authentication failed from remote IP" + f" {self.get_remote_ip()}. User not found" + ) return self.finish_json( 401, {"status": "error", "error": "INCORRECT_CREDENTIALS", "token": None}, ) if not user_data.enabled: + auth_log.error( + f"User attempted to log into {username}." + " Authentication failed from remote" + f" IP {self.get_remote_ip()} account disabled" + ) self.finish_json( 403, {"status": "error", "error": "ACCOUNT_DISABLED", "token": None} ) @@ -67,6 +86,11 @@ class ApiAuthLoginHandler(BaseApiHandler): # Valid Login if login_result: + auth_log.info( + f"{username} successfully" + " authenticated and logged" + f" into panel from remote IP {self.get_remote_ip()}" + ) logger.info(f"User: {user_data} Logged in from IP: {self.get_remote_ip()}") # record this login diff --git a/app/config/logging.json b/app/config/logging.json index 99de60e4..9dd09c81 100644 --- a/app/config/logging.json +++ b/app/config/logging.json @@ -10,16 +10,17 @@ }, "schedule": { "format": "%(asctime)s - [Schedules] - %(levelname)s - %(message)s" + }, + "auth": { + "format": "%(asctime)s - [AUTH] - %(levelname)s - %(message)s" } }, - "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "commander", "stream": "ext://sys.stdout" }, - "main_file_handler": { "class": "logging.handlers.RotatingFileHandler", "formatter": "commander", @@ -50,24 +51,45 @@ "maxBytes": 10485760, "backupCount": 20, "encoding": "utf8" + }, + "auth_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "auth", + "filename": "logs/auth.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" } }, - "loggers": { "": { "level": "INFO", - "handlers": ["main_file_handler", "session_file_handler"], + "handlers": [ + "main_file_handler", + "session_file_handler" + ], "propagate": false }, "tornado.access": { "level": "INFO", - "handlers": ["tornado_access_file_handler"], + "handlers": [ + "tornado_access_file_handler" + ], "propagate": false }, "apscheduler": { "level": "INFO", - "handlers": ["schedule_file_handler"], + "handlers": [ + "schedule_file_handler" + ], + "propagate": false + }, + "auth": { + "level": "INFO", + "handlers": [ + "auth_file_handler" + ], "propagate": false } } -} +} \ No newline at end of file diff --git a/main.py b/main.py index 143dfb4f..4cd78814 100644 --- a/main.py +++ b/main.py @@ -73,6 +73,14 @@ def do_intro(): def setup_logging(debug=True): logging_config_file = os.path.join(os.path.curdir, "app", "config", "logging.json") + if not helper.check_file_exists( + os.path.join(os.path.curdir, "logs", "auth_tracker.log") + ): + open( + os.path.join(os.path.curdir, "logs", "auth_tracker.log"), + "a", + encoding="utf-8", + ).close() if os.path.exists(logging_config_file): # open our logging config file