Merge branch 'critical/security' into 'dev'

Setup logging for panel authentication attempts, password requirement changes

See merge request crafty-controller/crafty-4!669
This commit is contained in:
Iain Powrie 2023-11-25 19:43:15 +00:00
commit 8ddd40012c
9 changed files with 143 additions and 11 deletions

View File

@ -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
<br><br>

View File

@ -45,8 +45,7 @@ class UsersController:
},
"password": {
"type": "string",
"maxLength": 20,
"minLength": 6,
"minLength": 8,
"examples": ["crafty"],
"title": "Password",
},

View File

@ -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():

View File

@ -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"
# )

View File

@ -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,

View File

@ -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")

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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