From 1d3a768dfe366e827203a846a746fb5bfcc4d304 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Tue, 16 Jul 2024 20:44:12 +0200 Subject: [PATCH] Update login routine (remember me working) --- cps/MyLoginManager.py | 24 +- cps/cw_login/login_manager.py | 47 +- cps/ub.py | 20 +- cps/usermanagement.py | 17 +- test/Calibre-Web TestSummary_Linux.html | 18776 +--------------------- 5 files changed, 937 insertions(+), 17947 deletions(-) diff --git a/cps/MyLoginManager.py b/cps/MyLoginManager.py index 5568ba91a..419b2e90c 100644 --- a/cps/MyLoginManager.py +++ b/cps/MyLoginManager.py @@ -19,12 +19,12 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import ast +import hashlib +from .cw_login import LoginManager +from flask import session -from .cw_login import LoginManager, confirm_login -from flask import session, current_app -from .cw_login.utils import decode_cookie -from .cw_login.signals import user_loaded_from_cookie class MyLoginManager(LoginManager): @@ -36,19 +36,5 @@ class MyLoginManager(LoginManager): return super(). _session_protection_failed() return False - def _load_user_from_remember_cookie(self, cookie): - user_id = decode_cookie(cookie) - if user_id is not None: - session["_user_id"] = user_id - session["_fresh"] = False - user = None - if self._user_callback: - user = self._user_callback(user_id, None, None) - if user is not None: - app = current_app._get_current_object() - user_loaded_from_cookie.send(app, user=user) - # if session was restored from remember me cookie make login valid - confirm_login() - return user - return None + diff --git a/cps/cw_login/login_manager.py b/cps/cw_login/login_manager.py index 9149f722b..bf0efbc75 100644 --- a/cps/cw_login/login_manager.py +++ b/cps/cw_login/login_manager.py @@ -1,5 +1,6 @@ from datetime import datetime from datetime import timedelta +import hashlib from flask import abort from flask import current_app @@ -9,6 +10,8 @@ from flask import has_app_context from flask import redirect from flask import request from flask import session +from itsdangerous import URLSafeSerializer +from flask.json.tag import TaggedJSONSerializer from .config import AUTH_HEADER_NAME from .config import COOKIE_DURATION @@ -32,8 +35,7 @@ from .signals import user_needs_refresh from .signals import user_unauthorized from .utils import _create_identifier from .utils import _user_context_processor -from .utils import decode_cookie -from .utils import encode_cookie +from .utils import confirm_login from .utils import expand_login_view from .utils import login_url as make_login_url from .utils import make_next_param @@ -323,7 +325,7 @@ class LoginManager: if self._user_callback is None and self._request_callback is None: raise Exception( "Missing user_loader or request_loader. Refer to " - "http://flask-login.readthedocs.io/#how-it-works " + "https://flask-login.readthedocs.io/#how-it-works " "for more info." ) @@ -361,7 +363,8 @@ class LoginManager: elif header_name in request.headers: header = request.headers[header_name] user = self._load_user_from_header(header) - + if not user: + self._update_request_context_with_user() return self._update_request_context_with_user(user) def _session_protection_failed(self): @@ -393,16 +396,32 @@ class LoginManager: return False def _load_user_from_remember_cookie(self, cookie): - user_id = decode_cookie(cookie) - if user_id is not None: - session["_user_id"] = user_id + signer_kwargs = dict( + key_derivation="hmac", digest_method=staticmethod(hashlib.sha1) + ) + try: + remember_dict = URLSafeSerializer( + current_app.secret_key, + salt="remember", + serializer=TaggedJSONSerializer(), + signer_kwargs=signer_kwargs, + ).loads(cookie) + except Exception: + return None + + if remember_dict['user'] is not None: + session["_user_id"] = remember_dict['user'] + if "_random" not in session: + session["_random"] = remember_dict['random'] session["_fresh"] = False user = None if self._user_callback: - user = self._user_callback(user_id) + user = self._user_callback(remember_dict['user'], session["_random"], None) if user is not None: app = current_app._get_current_object() user_loaded_from_cookie.send(app, user=user) + # if session was restored from remember me cookie make login valid + confirm_login() return user return None @@ -461,7 +480,17 @@ class LoginManager: duration = config.get("REMEMBER_COOKIE_DURATION", COOKIE_DURATION) # prepare data - data = encode_cookie(str(session["_user_id"])) + max_age = int(current_app.permanent_session_lifetime.total_seconds()) + signer_kwargs = dict( + key_derivation="hmac", digest_method=staticmethod(hashlib.sha1) + ) + # save + data = URLSafeSerializer( + current_app.secret_key, + salt="remember", + serializer=TaggedJSONSerializer(), + signer_kwargs=signer_kwargs, + ).dumps({"user":session["_user_id"], "random":session["_random"]}) if isinstance(duration, int): duration = timedelta(seconds=duration) diff --git a/cps/ub.py b/cps/ub.py index 70f0dd16c..a9570dd7f 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -74,10 +74,9 @@ def store_user_session(): _user = flask_session.get('_user_id', "") _id = flask_session.get('_id', "") _random = flask_session.get('_random', "") - if flask_session.get('_user_id', ""): try: - if not check_user_session(_user, _id): + if not check_user_session(_user, _id, _random): expiry = int((datetime.datetime.now() + datetime.timedelta(days=31)).timestamp()) user_session = User_Sessions(_user, _id, _random, expiry) session.add(user_session) @@ -103,10 +102,12 @@ def delete_user_session(user_id, session_key): log.exception(ex) -def check_user_session(user_id, session_key): +def check_user_session(user_id, session_key, random): try: found = session.query(User_Sessions).filter(User_Sessions.user_id==user_id, - User_Sessions.session_key==session_key).one_or_none() + User_Sessions.session_key==session_key, + User_Sessions.random == random, + ).one_or_none() if found is not None: new_expiry = int((datetime.datetime.now() + datetime.timedelta(days=31)).timestamp()) if new_expiry - found.expiry > 86400: @@ -614,10 +615,13 @@ def migrate_Database(_session): def clean_database(_session): # Remove expired remote login tokens now = datetime.datetime.now() - _session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\ - filter(RemoteAuthToken.token_type != 1).delete() - _session.commit() - + try: + _session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\ + filter(RemoteAuthToken.token_type != 1).delete() + _session.commit() + except exc.OperationalError: # Database is not writeable + print('Settings database is not writeable. Exiting...') + sys.exit(2) # Save downloaded books per user in calibre-web's own database diff --git a/cps/usermanagement.py b/cps/usermanagement.py index e42f70f0b..31c37a933 100644 --- a/cps/usermanagement.py +++ b/cps/usermanagement.py @@ -40,7 +40,6 @@ def verify_password(username, password): if user.name.lower() == "guest": if config.config_anonbrowse == 1: return user - limiter.check() if config.config_login_type == constants.LOGIN_LDAP and services.ldap: login_result, error = services.ldap.bind_user(user.name, password) if login_result: @@ -48,9 +47,11 @@ def verify_password(username, password): return user if error is not None: log.error(error) - elif check_password_hash(str(user.password), password): - [limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] - return user + else: + limiter.check() + if check_password_hash(str(user.password), password): + [limiter.limiter.storage.clear(k.key) for k in limiter.current_limits] + return user ip_address = request.headers.get('X-Forwarded-For', request.remote_addr) log.warning('OPDS Login failed for user "%s" IP-address: %s', username, ip_address) return None @@ -127,9 +128,13 @@ def load_user_from_reverse_proxy_header(req): @lm.user_loader def load_user(user_id, random, session_key): user = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() - if random and session_key: + if session_key: entry = ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.random == random, - ub.User_Sessions.session_key == session_key).first() + ub.User_Sessions.session_key == session_key).first() + if not entry or entry.user_id != user.id: + return None + elif random: + entry = ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.random == random).first() if not entry or entry.user_id != user.id: return None return user diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index ae5d4d36c..665d9993b 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2024-07-14 21:21:23

+

Start Time: 2024-07-15 20:39:37

-

Stop Time: 2024-07-15 02:27:32

+

Stop Time: 2024-07-16 03:41:10

-

Duration: 4h 2 min

+

Duration: 5h 50 min

@@ -592,7 +592,7 @@
Traceback (most recent call last):
-  File "/home/ozzie/Development/calibre-web-test/test/test_cli.py", line 400, in test_settingsdb_not_writeable
+  File "/home/ozzie/Development/calibre-web-test/test/test_cli.py", line 431, in test_settingsdb_not_writeable
     self.assertEqual(result, 2)
 AssertionError: 1 != 2
@@ -2032,11 +2032,11 @@ IndexError: list index out of range - + TestEditBooksOnGdrive 18 - 18 - 0 + 17 + 1 0 0 @@ -2199,11 +2199,31 @@ IndexError: list index out of range - +
TestEditBooksOnGdrive - test_watch_metadata
- PASS + +
+ FAIL +
+ + + + @@ -2847,12 +2867,12 @@ IndexError: list index out of range - + TestLdapLogin 13 - 13 - 0 - 0 + 11 + 1 + 1 0 Detail @@ -2960,30 +2980,81 @@ IndexError: list index out of range - +
TestLdapLogin - test_ldap_opds_anonymous
- PASS + +
+ FAIL +
+ + + + - +
TestLdapLogin - test_ldap_opds_download_book
- PASS + +
+ ERROR +
+ + + + - + TestSecurity 5 - 3 - 2 + 5 + 0 0 0 @@ -3002,31 +3073,11 @@ IndexError: list index out of range - +
TestSecurity - test_opds_limit
- -
- FAIL -
- - - - + PASS @@ -3049,1593 +3100,11 @@ AssertionError: 429 != 401 - +
TestSecurity - test_register_limit
- -
- FAIL -
- - - - - - - - - - - _ErrorHolder - 18 - 0 - 0 - 18 - 0 - - Detail - - - - - - - -
tearDownClass (test_limiter)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_oauth)
- - -
- ERROR -
- - - - - - - - - - -
setUpClass (test_opds_feed)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_pdf_metadata)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_reader)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_readonly_db)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_register)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_reverse_proxy)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_shelf)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_split_library)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_thumbnail_env)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_thumbnails)
- - -
- ERROR -
- - - - - - - - - - -
setUpClass (test_updater)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_upload_epubs)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_user_list)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_user_load)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_user_template)
- - -
- ERROR -
- - - - - - - - - - -
tearDownClass (test_visiblilitys)
- - -
- ERROR -
- - - - + PASS @@ -4649,13 +3118,13 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p 0 0 - Detail + Detail - +
TestCalibreWebListOrders - test_author_sort
@@ -4664,7 +3133,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_download_sort
@@ -4673,7 +3142,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_format_sort
@@ -4682,7 +3151,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_formats_click_none
@@ -4691,7 +3160,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_lang_sort
@@ -4700,7 +3169,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_language_click_none
@@ -4709,7 +3178,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_order_authors_all_links
@@ -4718,7 +3187,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_order_series_all_links
@@ -4727,7 +3196,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_publisher_click_none
@@ -4736,7 +3205,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_publisher_sort
@@ -4745,7 +3214,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_ratings_click_none
@@ -4754,7 +3223,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_ratings_sort
@@ -4763,7 +3232,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_series_click_none
@@ -4772,7 +3241,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_series_sort
@@ -4781,7 +3250,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_tags_click_none
@@ -4790,7 +3259,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestCalibreWebListOrders - test_tags_sort
@@ -4808,13 +3277,13 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p 0 1 - Detail + Detail - +
TestLogging - test_access_log_recover
@@ -4823,7 +3292,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestLogging - test_debug_log
@@ -4832,7 +3301,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestLogging - test_debuginfo_download
@@ -4841,7 +3310,7 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestLogging - test_failed_login
@@ -4850,19 +3319,19 @@ urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='localhost', p - +
TestLogging - test_failed_register
- SKIP + SKIP
-