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