forked from Github-Mirrors/canaille
feat: sign in/out events are logged in #177
This commit is contained in:
parent
053156ec18
commit
920395c27f
5 changed files with 56 additions and 4 deletions
|
@ -1,3 +1,8 @@
|
||||||
|
Added
|
||||||
|
^^^^^
|
||||||
|
|
||||||
|
- Sign in/out events are logged in :issuer:`177`
|
||||||
|
|
||||||
[0.0.49] - 2024-04-08
|
[0.0.49] - 2024-04-08
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|
|
@ -259,8 +259,9 @@ class CoreSettings(BaseModel):
|
||||||
LOGGING: Optional[Union[str, Dict]] = None
|
LOGGING: Optional[Union[str, Dict]] = None
|
||||||
"""Configures the logging output using the python logging configuration format:
|
"""Configures the logging output using the python logging configuration format:
|
||||||
|
|
||||||
- if :py:data:`None`, everything is logged in the standard output
|
- if :py:data:`None`, everything is logged in the standard error output
|
||||||
the log level is :py:data:`~logging.DEBUG` if the :attr:`~canaille.app.configuration.RootSettings.DEBUG` setting is :py:data:`True`, else this is :py:data:`~logging.INFO`
|
the log level is :py:data:`~logging.DEBUG` if the :attr:`~canaille.app.configuration.RootSettings.DEBUG`
|
||||||
|
setting is :py:data:`True`, else this is :py:data:`~logging.INFO`
|
||||||
- if this is a :class:`dict`, it is passed to :func:`logging.config.dictConfig`:
|
- if this is a :class:`dict`, it is passed to :func:`logging.config.dictConfig`:
|
||||||
- if this is a :class:`str`, it is expected to be a file path that will be passed
|
- if this is a :class:`str`, it is expected to be a file path that will be passed
|
||||||
to :func:`logging.config.fileConfig`
|
to :func:`logging.config.fileConfig`
|
||||||
|
|
|
@ -85,13 +85,20 @@ def password():
|
||||||
)
|
)
|
||||||
|
|
||||||
success, message = BaseBackend.get().check_user_password(user, form.password.data)
|
success, message = BaseBackend.get().check_user_password(user, form.password.data)
|
||||||
|
request_ip = request.remote_addr or "unknown IP"
|
||||||
if not success:
|
if not success:
|
||||||
logout_user()
|
logout_user()
|
||||||
|
current_app.logger.info(
|
||||||
|
f'Failed login attempt for {session["attempt_login"]} from {request_ip}'
|
||||||
|
)
|
||||||
flash(message or _("Login failed, please check your information"), "error")
|
flash(message or _("Login failed, please check your information"), "error")
|
||||||
return render_template(
|
return render_template(
|
||||||
"password.html", form=form, username=session["attempt_login"]
|
"password.html", form=form, username=session["attempt_login"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_app.logger.info(
|
||||||
|
f'Succeed login attempt for {session["attempt_login"]} from {request_ip}'
|
||||||
|
)
|
||||||
del session["attempt_login"]
|
del session["attempt_login"]
|
||||||
login_user(user)
|
login_user(user)
|
||||||
flash(
|
flash(
|
||||||
|
@ -106,6 +113,9 @@ def logout():
|
||||||
user = current_user()
|
user = current_user()
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
|
request_ip = request.remote_addr or "unknown IP"
|
||||||
|
current_app.logger.info(f"Logout {user.identifier} from {request_ip}")
|
||||||
|
|
||||||
flash(
|
flash(
|
||||||
_(
|
_(
|
||||||
"You have been disconnected. See you next time %(user)s",
|
"You have been disconnected. See you next time %(user)s",
|
||||||
|
|
|
@ -126,6 +126,26 @@ def configuration(smtpd):
|
||||||
"PASSWORD": smtpd.config.login_password,
|
"PASSWORD": smtpd.config.login_password,
|
||||||
"FROM_ADDR": "admin@mydomain.tld",
|
"FROM_ADDR": "admin@mydomain.tld",
|
||||||
},
|
},
|
||||||
|
"LOGGING": {
|
||||||
|
"version": 1,
|
||||||
|
"formatters": {
|
||||||
|
"default": {
|
||||||
|
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"wsgi": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"stream": "ext://sys.stdout",
|
||||||
|
"formatter": "default",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {"level": "DEBUG", "handlers": ["wsgi"]},
|
||||||
|
"loggers": {
|
||||||
|
"faker": {"level": "WARNING"},
|
||||||
|
},
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return conf
|
return conf
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
|
@ -20,7 +21,7 @@ def test_index(testclient, user):
|
||||||
assert res.location == "/about"
|
assert res.location == "/about"
|
||||||
|
|
||||||
|
|
||||||
def test_signin_and_out(testclient, user):
|
def test_signin_and_out(testclient, user, caplog):
|
||||||
with testclient.session_transaction() as session:
|
with testclient.session_transaction() as session:
|
||||||
assert not session.get("user_id")
|
assert not session.get("user_id")
|
||||||
|
|
||||||
|
@ -40,6 +41,11 @@ def test_signin_and_out(testclient, user):
|
||||||
"success",
|
"success",
|
||||||
"Connection successful. Welcome John (johnny) Doe",
|
"Connection successful. Welcome John (johnny) Doe",
|
||||||
) in res.flashes
|
) in res.flashes
|
||||||
|
assert (
|
||||||
|
"canaille",
|
||||||
|
logging.INFO,
|
||||||
|
"Succeed login attempt for user from unknown IP",
|
||||||
|
) in caplog.record_tuples
|
||||||
res = res.follow(status=302)
|
res = res.follow(status=302)
|
||||||
res = res.follow(status=200)
|
res = res.follow(status=200)
|
||||||
|
|
||||||
|
@ -54,6 +60,11 @@ def test_signin_and_out(testclient, user):
|
||||||
"success",
|
"success",
|
||||||
"You have been disconnected. See you next time John (johnny) Doe",
|
"You have been disconnected. See you next time John (johnny) Doe",
|
||||||
) in res.flashes
|
) in res.flashes
|
||||||
|
assert (
|
||||||
|
"canaille",
|
||||||
|
logging.INFO,
|
||||||
|
"Logout user from unknown IP",
|
||||||
|
) in caplog.record_tuples
|
||||||
res = res.follow(status=302)
|
res = res.follow(status=302)
|
||||||
res = res.follow(status=200)
|
res = res.follow(status=200)
|
||||||
|
|
||||||
|
@ -74,7 +85,7 @@ def test_visitor_logout(testclient, user):
|
||||||
assert not session.get("user_id")
|
assert not session.get("user_id")
|
||||||
|
|
||||||
|
|
||||||
def test_signin_wrong_password(testclient, user):
|
def test_signin_wrong_password(testclient, user, caplog):
|
||||||
with testclient.session_transaction() as session:
|
with testclient.session_transaction() as session:
|
||||||
assert not session.get("user_id")
|
assert not session.get("user_id")
|
||||||
|
|
||||||
|
@ -86,6 +97,11 @@ def test_signin_wrong_password(testclient, user):
|
||||||
res.form["password"] = "incorrect horse"
|
res.form["password"] = "incorrect horse"
|
||||||
res = res.form.submit(status=200)
|
res = res.form.submit(status=200)
|
||||||
assert ("error", "Login failed, please check your information") in res.flashes
|
assert ("error", "Login failed, please check your information") in res.flashes
|
||||||
|
assert (
|
||||||
|
"canaille",
|
||||||
|
logging.INFO,
|
||||||
|
"Failed login attempt for user from unknown IP",
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
def test_signin_password_substring(testclient, user):
|
def test_signin_password_substring(testclient, user):
|
||||||
|
|
Loading…
Reference in a new issue