mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2025-01-19 09:45:28 +01:00
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:
commit
8ddd40012c
@ -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>
|
||||
|
@ -45,8 +45,7 @@ class UsersController:
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"maxLength": 20,
|
||||
"minLength": 6,
|
||||
"minLength": 8,
|
||||
"examples": ["crafty"],
|
||||
"title": "Password",
|
||||
},
|
||||
|
@ -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():
|
||||
|
@ -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"
|
||||
# )
|
||||
|
@ -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,
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
main.py
8
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user