diff --git a/atst/app.py b/atst/app.py index c729c65a..ccecd534 100644 --- a/atst/app.py +++ b/atst/app.py @@ -2,7 +2,7 @@ import os import re import pathlib from configparser import ConfigParser -from flask import Flask, request, g +from flask import Flask, request, g, session from flask_session import Session import redis from unipath import Path @@ -30,6 +30,7 @@ from atst.utils.form_cache import FormCache from atst.utils.json import CustomJSONEncoder from atst.queue import queue from atst.utils.notification_sender import NotificationSender +from atst.utils.session_limiter import SessionLimiter from logging.config import dictConfig from atst.utils.logging import JsonFormatter, RequestContextFilter @@ -70,6 +71,7 @@ def make_app(config): db.init_app(app) csrf.init_app(app) Session(app) + make_session_limiter(app, session, config) assets_environment.init_app(app) make_error_pages(app) @@ -162,6 +164,9 @@ def map_config(config): "DISABLE_CRL_CHECK": config.getboolean("default", "DISABLE_CRL_CHECK"), "CRL_FAIL_OPEN": config.getboolean("default", "CRL_FAIL_OPEN"), "LOG_JSON": config.getboolean("default", "LOG_JSON"), + "LIMIT_CONCURRENT_SESSIONS": config.getboolean( + "default", "LIMIT_CONCURRENT_SESSIONS" + ), } @@ -253,6 +258,10 @@ def make_notification_sender(app): app.notification_sender = NotificationSender(queue) +def make_session_limiter(app, session, config): + app.session_limiter = SessionLimiter(config, session, app.redis) + + def apply_json_logger(): dictConfig( { diff --git a/atst/domain/users.py b/atst/domain/users.py index 43dbdb86..daf69c68 100644 --- a/atst/domain/users.py +++ b/atst/domain/users.py @@ -89,7 +89,6 @@ class Users(object): db.session.add(user) db.session.commit() - @classmethod def update_last_session_id(cls, user, session_id): user.last_session_id = session_id diff --git a/atst/routes/__init__.py b/atst/routes/__init__.py index 7dbecddd..94acb897 100644 --- a/atst/routes/__init__.py +++ b/atst/routes/__init__.py @@ -8,7 +8,7 @@ from flask import ( url_for, request, make_response, - current_app as app + current_app as app, ) from jinja2.exceptions import TemplateNotFound @@ -123,11 +123,9 @@ def redirect_after_login_url(): def current_user_setup(user): - session_id = session.sid - app.redis.delete("session:{}".format(user.last_session_id)) session["user_id"] = user.id session["last_login"] = user.last_login - Users.update_last_session_id(user, session_id) + app.session_limiter.on_login(user) Users.update_last_login(user) diff --git a/atst/utils/session_limiter.py b/atst/utils/session_limiter.py new file mode 100644 index 00000000..cae43c5d --- /dev/null +++ b/atst/utils/session_limiter.py @@ -0,0 +1,19 @@ +from atst.domain.users import Users + + +class SessionLimiter(object): + def __init__(self, config, session, redis): + self.limit_logins = config["LIMIT_CONCURRENT_SESSIONS"] + self.session = session + self.redis = redis + + def on_login(self, user): + if not self.limit_logins: + return + + session_id = self.session.sid + self._delete_session(user.last_session_id) + Users.update_last_session_id(user, session_id) + + def _delete_session(self, session_id): + self.redis.delete("session:{}".format(session_id)) diff --git a/config/base.ini b/config/base.ini index 36f1e0a7..ce98fbf2 100644 --- a/config/base.ini +++ b/config/base.ini @@ -34,3 +34,4 @@ STORAGE_SECRET='' STORAGE_PROVIDER=LOCAL STORAGE_CRL_ARCHIVE_NAME = dod_crls.tar.bz WTF_CSRF_ENABLED = true +LIMIT_CONCURRENT_SESSIONS = false diff --git a/config/prod.ini b/config/prod.ini index bbbf8f8b..ebc1dc40 100644 --- a/config/prod.ini +++ b/config/prod.ini @@ -1,3 +1,4 @@ [default] SESSION_COOKIE_SECURE=True SESSION_COOKIE_DOMAIN=atat.codes +LIMIT_CONCURRENT_SESSIONS=True