diff --git a/configs/development.ini b/configs/development.ini index 554dcbc6..b44dd24e 100644 --- a/configs/development.ini +++ b/configs/development.ini @@ -52,7 +52,7 @@ use = egg:gunicorn#main ; allows to set RhodeCode under a prefix in server. ; eg https://server.com/custom_prefix. Enable `filter-with =` option below as well. ; And set your prefix like: `prefix = /custom_prefix` -; be sure to also set beaker.session.cookie_path = /custom_prefix if you need +; be sure to also set session.cookie_path = /custom_prefix if you need ; to make your cookies only work on prefix url [filter:proxy-prefix] use = egg:PasteDeploy#prefix @@ -114,7 +114,7 @@ debug_style = true ; encryption key used to encrypt social plugin tokens, ; remote_urls with credentials etc, if not set it defaults to -; `beaker.session.secret` +; `session.secret` #rhodecode.encrypted_values.secret = ; decryption strict mode (enabled by default). It controls if decryption raises @@ -599,51 +599,19 @@ rc_cache.cache_license.expiration_time = 300 #rc_cache.cache_license.arguments.key_prefix = custom-prefix- -; ############## -; BEAKER SESSION -; ############## +; ###################### +; SESSION CONFIGURATION +; ###################### -; beaker.session.type is type of storage options for the logged users sessions. Current allowed -; types are file, ext:redis, ext:database, ext:memcached -; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session -#beaker.session.type = file -#beaker.session.data_dir = /var/opt/rhodecode_data/sessions - -; Redis based sessions -beaker.session.type = ext:redis -beaker.session.url = redis://redis:6379/2 - -; DB based session, fast, and allows easy management over logged in users -#beaker.session.type = ext:database -#beaker.session.table_name = db_session -#beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode -#beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode -#beaker.session.sa.pool_recycle = 3600 -#beaker.session.sa.echo = false - -beaker.session.key = rhodecode -beaker.session.secret = develop-rc-uytcxaz -beaker.session.lock_dir = /data_ramdisk/lock - -; Secure encrypted cookie. Requires AES and AES python libraries -; you must disable beaker.session.secret to use this -#beaker.session.encrypt_key = key_for_encryption -#beaker.session.validate_key = validation_key - -; Sets session as invalid (also logging out user) if it haven not been -; accessed for given amount of time in seconds -beaker.session.timeout = 2592000 -beaker.session.httponly = true - -; Path to use for the cookie. Set to prefix if you use prefix middleware -#beaker.session.cookie_path = /custom_prefix - -; Set https secure cookie -beaker.session.secure = false - -; default cookie expiration time in seconds, set to `true` to set expire -; at browser close -#beaker.session.cookie_expires = 3600 +; Cookie-based signed sessions (no server-side session store needed) +session.secret = develop-rc-uytcxaz +session.key = rhodecode +session.timeout = 2592000 +session.httponly = true +session.secure = false +;session.cookie_path = /custom_prefix +;session.domain = +;session.samesite = Lax ; ############################# ; SEARCH INDEXING CONFIGURATION diff --git a/configs/production.ini b/configs/production.ini index c5c12504..ed2bbe78 100644 --- a/configs/production.ini +++ b/configs/production.ini @@ -46,7 +46,7 @@ use = egg:gunicorn#main ; allows to set RhodeCode under a prefix in server. ; eg https://server.com/custom_prefix. Enable `filter-with =` option below as well. ; And set your prefix like: `prefix = /custom_prefix` -; be sure to also set beaker.session.cookie_path = /custom_prefix if you need +; be sure to also set session.cookie_path = /custom_prefix if you need ; to make your cookies only work on prefix url [filter:proxy-prefix] use = egg:PasteDeploy#prefix @@ -76,7 +76,7 @@ use = egg:rhodecode-enterprise-ce ; encryption key used to encrypt social plugin tokens, ; remote_urls with credentials etc, if not set it defaults to -; `beaker.session.secret` +; `session.secret` #rhodecode.encrypted_values.secret = ; decryption strict mode (enabled by default). It controls if decryption raises @@ -561,51 +561,19 @@ rc_cache.cache_license.expiration_time = 300 #rc_cache.cache_license.arguments.key_prefix = custom-prefix- -; ############## -; BEAKER SESSION -; ############## +; ###################### +; SESSION CONFIGURATION +; ###################### -; beaker.session.type is type of storage options for the logged users sessions. Current allowed -; types are file, ext:redis, ext:database, ext:memcached -; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session -#beaker.session.type = file -#beaker.session.data_dir = /var/opt/rhodecode_data/sessions - -; Redis based sessions -beaker.session.type = ext:redis -beaker.session.url = redis://redis:6379/2 - -; DB based session, fast, and allows easy management over logged in users -#beaker.session.type = ext:database -#beaker.session.table_name = db_session -#beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode -#beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode -#beaker.session.sa.pool_recycle = 3600 -#beaker.session.sa.echo = false - -beaker.session.key = rhodecode -beaker.session.secret = production-rc-uytcxaz -beaker.session.lock_dir = /data_ramdisk/lock - -; Secure encrypted cookie. Requires AES and AES python libraries -; you must disable beaker.session.secret to use this -#beaker.session.encrypt_key = key_for_encryption -#beaker.session.validate_key = validation_key - -; Sets session as invalid (also logging out user) if it haven not been -; accessed for given amount of time in seconds -beaker.session.timeout = 2592000 -beaker.session.httponly = true - -; Path to use for the cookie. Set to prefix if you use prefix middleware -#beaker.session.cookie_path = /custom_prefix - -; Set https secure cookie -beaker.session.secure = false - -; default cookie expiration time in seconds, set to `true` to set expire -; at browser close -#beaker.session.cookie_expires = 3600 +; Cookie-based signed sessions (no server-side session store needed) +session.secret = production-rc-uytcxaz +session.key = rhodecode +session.timeout = 2592000 +session.httponly = true +session.secure = false +;session.cookie_path = /custom_prefix +;session.domain = +;session.samesite = Lax ; ############################# ; SEARCH INDEXING CONFIGURATION diff --git a/pyproject.toml b/pyproject.toml index 8a437904..5c066227 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,7 @@ main = "rhodecode.config.middleware:make_pyramid_app" [project.entry-points."pyramid.pshell_runner"] ipython = "rhodecode.lib.pyramid_shell:ipython_shell_runner" -[project.entry-points."beaker.backends"] -memorylru_base="rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerBase" -memorylru_debug="rhodecode.lib.memory_lru_dict:MemoryLRUNamespaceManagerDebug" +# beaker.backends entry points removed — beaker dependency eliminated [project.scripts] diff --git a/requirements.txt b/requirements.txt index 91bfe956..e0b09d03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ alembic==1.13.1 greenlet==3.3.0 typing_extensions==4.15.0 babel==2.12.1 -beaker==1.13.0 +# beaker removed — sessions are now cookie-only via pyramid.session.SignedCookieSessionFactory bleach==6.3.0 webencodings==0.5.1 celery==5.6.2 diff --git a/rhodecode/api/views/server_api.py b/rhodecode/api/views/server_api.py index 9373b4e5..d16e0ee5 100644 --- a/rhodecode/api/views/server_api.py +++ b/rhodecode/api/views/server_api.py @@ -294,7 +294,7 @@ def cleanup_sessions(request, apiuser, older_then=Optional(60)): older_than_seconds = 60 * 60 * 24 * older_then config = system_info.rhodecode_config().get_value()["value"]["config"] - session_model = user_sessions.get_session_handler(config.get("beaker.session.type", "memory"))(config) + session_model = user_sessions.get_session_handler("cookie")(config) backend = session_model.SESSION_TYPE try: diff --git a/rhodecode/apps/admin/views/sessions.py b/rhodecode/apps/admin/views/sessions.py index 41a7b633..2dab1b56 100644 --- a/rhodecode/apps/admin/views/sessions.py +++ b/rhodecode/apps/admin/views/sessions.py @@ -50,7 +50,7 @@ class AdminSessionSettingsView(BaseAppView): older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days config = system_info.rhodecode_config().get_value()["value"]["config"] - c.session_model = user_sessions.get_session_handler(config.get("beaker.session.type", "memory"))(config) + c.session_model = user_sessions.get_session_handler("cookie")(config) c.session_conf = c.session_model.config c.session_count = c.session_model.get_count() @@ -71,7 +71,7 @@ class AdminSessionSettingsView(BaseAppView): older_than_seconds = 60 * 60 * 24 * expire_days config = system_info.rhodecode_config().get_value()["value"]["config"] - session_model = user_sessions.get_session_handler(config.get("beaker.session.type", "memory"))(config) + session_model = user_sessions.get_session_handler("cookie")(config) try: session_model.clean_sessions(older_than_seconds=older_than_seconds) diff --git a/rhodecode/apps/home/tests/test_home.py b/rhodecode/apps/home/tests/test_home.py index f049b42e..a4dd9fa6 100644 --- a/rhodecode/apps/home/tests/test_home.py +++ b/rhodecode/apps/home/tests/test_home.py @@ -87,7 +87,7 @@ class TestHomeController(TestController): self.log_user() response = self.app.get(route_path("home")) - rhodecode_version_hash = calculate_version_hash({"beaker.session.secret": "test-rc-uytcxaz"}) + rhodecode_version_hash = calculate_version_hash({"session.secret": "test-rc-uytcxaz"}) response.mustcontain("style.css?ver={0}".format(rhodecode_version_hash)) response.mustcontain("scripts.min.js?ver={0}".format(rhodecode_version_hash)) diff --git a/rhodecode/config/config_maker.py b/rhodecode/config/config_maker.py index 6e17225e..8cdae00a 100644 --- a/rhodecode/config/config_maker.py +++ b/rhodecode/config/config_maker.py @@ -75,11 +75,7 @@ def sanitize_settings_and_apply_defaults(global_config, settings): ) log.debug("Using the following Mako template directories: %s", mako_directories) - # NOTE(marcink): fix redis requirement for schema of connection since 3.X - if "beaker.session.type" in settings and settings["beaker.session.type"] == "ext:redis": - raw_url = settings["beaker.session.url"] - if not raw_url.startswith(("redis://", "rediss://", "unix://")): - settings["beaker.session.url"] = "redis://" + raw_url + # NOTE: beaker session redis fixup removed — sessions are now cookie-only settings_maker.make_setting("__file__", global_config.get("__file__")) @@ -181,9 +177,9 @@ def sanitize_settings_and_apply_defaults(global_config, settings): settings_maker.make_setting("exception_tracker.send_email", False, parser="bool") settings_maker.make_setting("exception_tracker.email_prefix", "[RHODECODE ERROR]", default_when_empty=True) - # sessions, ensure file since no-value is memory - settings_maker.make_setting("beaker.session.type", "file") - settings_maker.make_setting("beaker.session.data_dir", jn(default_cache_dir, "session_data")) + # session defaults for signed cookie sessions + settings_maker.make_setting("session.secret", "change-me-in-production") + settings_maker.make_setting("session.timeout", "2592000") # cache_general settings_maker.make_setting("rc_cache.cache_general.backend", "dogpile.cache.rc.file_namespace") diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py index 58416111..06cc9e33 100644 --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -284,7 +284,7 @@ class BasicAuth(AuthBasicAuthenticator): def calculate_version_hash(config): - return sha1(config.get(b"beaker.session.secret", b"") + ascii_bytes(rhodecode.__version__))[:8] + return sha1(config.get(b"session.secret", config.get(b"beaker.session.secret", b"")) + ascii_bytes(rhodecode.__version__))[:8] def get_current_lang(request): diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py index 550946b6..2a90b9f2 100644 --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -199,7 +199,7 @@ Encryption signature verification failed. Please check your value of secret key, and/or encrypted value stored. Secret key stored inside .ini file: `rhodecode.encrypted_values.secret` or defaults to -`beaker.session.secret` +`session.secret` Probably the stored values were encrypted using a different secret then currently set in .ini file """ diff --git a/rhodecode/lib/memory_lru_dict.py b/rhodecode/lib/memory_lru_dict.py index 3bd0864b..a9ef72ce 100644 --- a/rhodecode/lib/memory_lru_dict.py +++ b/rhodecode/lib/memory_lru_dict.py @@ -20,7 +20,7 @@ Custom LRU memory manager for debugging purposes. It allows to track the keys and the state of LRU dict. -inrae.cache is licensed under LRUDict is licensed under ZPL license +repoze.lru is licensed under ZPL license This software is Copyright (c) Zope Corporation (tm) and Contributors. All rights reserved. """ @@ -28,7 +28,6 @@ Contributors. All rights reserved. import logging from repoze.lru import LRUCache -from beaker.container import MemoryNamespaceManager, AbstractDictionaryNSManager from rhodecode.lib.utils2 import safe_str log = logging.getLogger(__name__) @@ -71,39 +70,3 @@ class LRUDictDebug(LRUDict): def __getitem__(self, key): self._report_keys() return self.get(key) - - -class MemoryLRUNamespaceManagerBase(MemoryNamespaceManager): - default_max_items = 10000 - - def _get_factory(self, max_items): - def Factory(): - return LRUDict(int(max_items)) - - return Factory - - def __init__(self, namespace, **kwargs): - AbstractDictionaryNSManager.__init__(self, namespace) - if "max_items" in kwargs: - max_items = kwargs["max_items"] - else: - max_items = self.default_max_items - - Factory = self._get_factory(max_items) - - self.dictionary = MemoryNamespaceManager.namespaces.get(self.namespace, Factory) - - -class MemoryLRUNamespaceManagerDebug(MemoryLRUNamespaceManagerBase): - """ - A memory namespace manager that return with LRU dicts backend, - special debug for testing - """ - - default_max_items = 10000 - - def _get_factory(self, max_items): - def Factory(): - return LRUDictDebug(int(max_items)) - - return Factory diff --git a/rhodecode/lib/rc_beaker.py b/rhodecode/lib/rc_beaker.py index 3741b6d0..0782823d 100644 --- a/rhodecode/lib/rc_beaker.py +++ b/rhodecode/lib/rc_beaker.py @@ -1,170 +1,73 @@ -# Copyright (c) 2010 Agendaless Consulting and Contributors. -# (http://www.agendaless.com), All Rights Reserved -# License: BSD-derived (http://www.repoze.org/LICENSE.txt) -# With Patches from RhodeCode GmBH +# Copyright (C) 2010-2024 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ -import os - -from beaker import cache -from beaker.session import SessionObject, Session -from beaker.util import coerce_cache_params -from beaker.util import coerce_session_params - -from pyramid.interfaces import ISession +from pyramid.session import SignedCookieSessionFactory from pyramid.settings import asbool -from zope.interface import implementer - -from binascii import hexlify - - -class CustomSession(Session): - pass - - -def BeakerSessionFactoryConfig(**options): - """Return a Pyramid session factory using Beaker session settings - supplied directly as ``**options``""" - - class PyramidBeakerSessionObject(SessionObject): - _options = options - _cookie_on_exception = _options.pop("cookie_on_exception", True) - _constant_csrf_token = _options.pop("constant_csrf_token", False) - _sa_opts = _options.pop("sa_opts", {}) - - def __init__(self, request): - self._options["session_class"] = CustomSession - self._options["sa_opts"] = self._sa_opts - SessionObject.__init__(self, request.environ, **self._options) - - def session_callback(_request, _response): - exception = getattr(_request, "exception", None) - file_response = getattr(_request, "_file_response", None) - api_call = getattr(_request, "rpc_method", None) - - if file_response is not None: - return - if api_call is not None: - return - - if exception is not None and not self._cookie_on_exception: - return - - if self.accessed(): - self.persist() - headers = self.__dict__["_headers"] - if headers.get("set_cookie") and headers.get("cookie_out"): - _response.headerlist.append(("Set-Cookie", headers["cookie_out"])) - - request.add_response_callback(session_callback) - - # ISession API - - @property - def id(self): - # this is as inspected in SessionObject.__init__ - if self.__dict__["_params"].get("type") != "cookie": - return self._session().id - return None - - @property - def new(self): - return self.last_accessed is None - - changed = SessionObject.save - - # modifying dictionary methods - - @call_save - def clear(self): - return self._session().clear() - - @call_save - def update(self, d, **kw): - return self._session().update(d, **kw) - - @call_save - def setdefault(self, k, d=None): - return self._session().setdefault(k, d) - - @call_save - def pop(self, k, d=None): - return self._session().pop(k, d) - - @call_save - def popitem(self): - return self._session().popitem() - - __setitem__ = call_save(SessionObject.__setitem__) - __delitem__ = call_save(SessionObject.__delitem__) - - # Flash API methods - def flash(self, msg, queue="", allow_duplicate=True): - storage = self.setdefault(f"_f_{queue}", []) - if allow_duplicate or (msg not in storage): - storage.append(msg) - - def pop_flash(self, queue=""): - storage = self.pop(f"_f_{queue}", []) - return storage - - def peek_flash(self, queue=""): - storage = self.get(f"_f_{queue}", []) - return storage - - # CSRF API methods - def new_csrf_token(self): - token = self._constant_csrf_token or hexlify(os.urandom(20)).decode("ascii") - self["_csrft_"] = token - return token - - def get_csrf_token(self): - token = self.get("_csrft_", None) - if token is None: - token = self.new_csrf_token() - return token - - return implementer(ISession)(PyramidBeakerSessionObject) - - -def call_save(wrapped): - """By default, in non-auto-mode beaker badly wants people to - call save even though it should know something has changed when - a mutating method is called. This hack should be removed if - Beaker ever starts to do this by default.""" - - def save(session, *arg, **kw): - value = wrapped(session, *arg, **kw) - session.save() - return value - - save.__doc__ = wrapped.__doc__ - return save def session_factory_from_settings(settings): - """Return a Pyramid session factory using Beaker session settings - supplied from a Paste configuration file""" + """Return a Pyramid session factory using signed cookie session settings + supplied from a Paste configuration file.""" prefixes = ("session.", "beaker.session.") options = {} - # custom gather of our specific sqlalchemy session db configuration we need to translate this into a single entry - # dict because this is how beaker expects that. - sa_opts = {} - - # Pull out any config args meant for beaker session. if there are any for k, v in settings.items(): for prefix in prefixes: if k.startswith(prefix): - option_name = k[len(prefix) :] - if option_name == "cookie_on_exception": - v = asbool(v) - if option_name.startswith("sa."): - sa_opts[option_name] = v + option_name = k[len(prefix):] options[option_name] = v - options = coerce_session_params(options) - options["sa_opts"] = sa_opts - return BeakerSessionFactoryConfig(**options) + # Map ini config keys to SignedCookieSessionFactory kwargs + factory_kwargs = {} + + factory_kwargs["secret"] = options.get("secret", "") + + if "hashalg" in options: + factory_kwargs["hashalg"] = options["hashalg"] + + if "timeout" in options: + factory_kwargs["timeout"] = int(options["timeout"]) + + if "max_age" in options: + factory_kwargs["max_age"] = int(options["max_age"]) + + if "reissue_time" in options: + factory_kwargs["reissue_time"] = int(options["reissue_time"]) + + if "key" in options: + factory_kwargs["cookie_name"] = options["key"] + + if "secure" in options: + factory_kwargs["secure"] = asbool(options["secure"]) + + if "httponly" in options: + factory_kwargs["httponly"] = asbool(options["httponly"]) + + if "domain" in options: + factory_kwargs["domain"] = options["domain"] + + if "cookie_path" in options: + factory_kwargs["path"] = options["cookie_path"] + + if "samesite" in options: + factory_kwargs["samesite"] = options["samesite"] + + return SignedCookieSessionFactory(**factory_kwargs) def includeme(config): diff --git a/rhodecode/lib/system_info.py b/rhodecode/lib/system_info.py index 1b6777f8..0aea4aab 100644 --- a/rhodecode/lib/system_info.py +++ b/rhodecode/lib/system_info.py @@ -696,6 +696,7 @@ def rhodecode_server_config(): f"rhodecode_{LicenseModel.LICENSE_DB_KEY}", "sqlalchemy.db1.url", "channelstream.secret", + "session.secret", "beaker.session.secret", "rhodecode.encrypted_values.secret", "appenlight.api_key", @@ -751,6 +752,7 @@ def rhodecode_config(): "routes.map", "sqlalchemy.db1.url", "channelstream.secret", + "session.secret", "beaker.session.secret", "rhodecode.encrypted_values.secret", "rhodecode_auth_github_consumer_key", diff --git a/rhodecode/lib/user_sessions.py b/rhodecode/lib/user_sessions.py index 20f767d1..60fa31c1 100644 --- a/rhodecode/lib/user_sessions.py +++ b/rhodecode/lib/user_sessions.py @@ -16,226 +16,22 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -import os -import re -import time -import datetime - -import binascii -import dateutil -import dateutil.relativedelta -import pickle -import base64 - -from rhodecode.model.db import DbSession, Session - class CleanupCommand(Exception): pass -class BaseAuthSessions(object): - SESSION_TYPE = None +class CookieAuthSessions: + SESSION_TYPE = "cookie" NOT_AVAILABLE = "NOT AVAILABLE" def __init__(self, config): session_conf = {} for k, v in list(config.items()): - if k.startswith("beaker.session"): + if k.startswith("session.") or k.startswith("beaker.session"): session_conf[k] = v self.config = session_conf - def get_count(self): - raise NotImplementedError - - def get_expired_count(self, older_than_seconds=None): - raise NotImplementedError - - def clean_sessions(self, older_than_seconds=None): - raise NotImplementedError - - def _seconds_to_date(self, seconds): - return datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(seconds=seconds) - - -class DbAuthSessions(BaseAuthSessions): - SESSION_TYPE = "ext:database" - - def get_count(self): - return DbSession.query().count() - - def get_expired_count(self, older_than_seconds=None): - expiry_date = self._seconds_to_date(older_than_seconds) - return DbSession.query().filter(DbSession.accessed < expiry_date).count() - - def clean_sessions(self, older_than_seconds=None): - expiry_date = self._seconds_to_date(older_than_seconds) - to_remove = DbSession.query().filter(DbSession.accessed < expiry_date).count() - DbSession.query().filter(DbSession.accessed < expiry_date).delete() - Session().commit() - return to_remove - - -class FileAuthSessions(BaseAuthSessions): - SESSION_TYPE = "file sessions" - - def _get_sessions_dir(self): - data_dir = self.config.get("beaker.session.data_dir") - return data_dir - - def _count_on_filesystem(self, path, older_than=0, callback=None): - value = dict(percent=0, used=0, total=0, items=0, callbacks=0, path=path, text="") - items_count = 0 - used = 0 - callbacks = 0 - cur_time = time.time() - for root, dirs, files in os.walk(path): - for f in files: - final_path = os.path.join(root, f) - try: - mtime = os.stat(final_path).st_mtime - if (cur_time - mtime) > older_than: - items_count += 1 - if callback: - callback_res = callback(final_path) - callbacks += 1 - else: - used += os.path.getsize(final_path) - except OSError: - pass - value.update({"percent": 100, "used": used, "total": used, "items": items_count, "callbacks": callbacks}) - return value - - def get_count(self): - try: - sessions_dir = self._get_sessions_dir() - items_count = self._count_on_filesystem(sessions_dir)["items"] - except Exception: - items_count = self.NOT_AVAILABLE - return items_count - - def get_expired_count(self, older_than_seconds=0): - try: - sessions_dir = self._get_sessions_dir() - items_count = self._count_on_filesystem(sessions_dir, older_than=older_than_seconds)["items"] - except Exception: - items_count = self.NOT_AVAILABLE - return items_count - - def clean_sessions(self, older_than_seconds=0): - # find . -mtime +60 -exec rm {} \; - - sessions_dir = self._get_sessions_dir() - - def remove_item(path): - os.remove(path) - - stats = self._count_on_filesystem(sessions_dir, older_than=older_than_seconds, callback=remove_item) - return stats["callbacks"] - - -class MemcachedAuthSessions(BaseAuthSessions): - SESSION_TYPE = "ext:memcached" - _key_regex = re.compile(r"ITEM (.*_session) \[(.*); (.*)\]") - - def _get_client(self): - import memcache - - client = memcache.Client([self.config.get("beaker.session.url")]) - return client - - def _get_telnet_client(self, host, port): - import telnetlib - - client = telnetlib.Telnet(host, port, None) - return client - - def _run_telnet_cmd(self, client, cmd): - client.write("%s\n" % cmd) - return client.read_until("END") - - def key_details(self, client, slab_ids, limit=100): - """Return a list of tuples containing keys and details""" - cmd = "stats cachedump %s %s" - for slab_id in slab_ids: - yield from self._key_regex.finditer(self._run_telnet_cmd(client, cmd % (slab_id, limit))) - - def get_count(self): - client = self._get_client() - count = self.NOT_AVAILABLE - try: - slabs = [] - for server, slabs_data in client.get_slabs(): - slabs.extend(list(slabs_data.keys())) - - host, port = client.servers[0].address - telnet_client = self._get_telnet_client(host, port) - keys = self.key_details(telnet_client, slabs) - count = 0 - for _k in keys: - count += 1 - except Exception: - return count - - return count - - def get_expired_count(self, older_than_seconds=None): - return self.NOT_AVAILABLE - - def clean_sessions(self, older_than_seconds=None): - raise CleanupCommand("Cleanup for this session type not yet available") - - -class RedisAuthSessions(BaseAuthSessions): - SESSION_TYPE = "ext:redis" - - def _get_client(self): - import redis - - args = {"socket_timeout": 60, "decode_responses": False, "url": self.config.get("beaker.session.url")} - - client = redis.StrictRedis.from_url(**args) - return client - - def get_count(self): - client = self._get_client() - return len(client.keys("beaker_cache:*")) - - def get_expired_count(self, older_than_seconds=None): - expiry_date = self._seconds_to_date(older_than_seconds) - return self.NOT_AVAILABLE - - def clean_sessions(self, older_than_seconds=None): - client = self._get_client() - expiry_time = time.time() - older_than_seconds - deleted_keys = 0 - - for key in client.keys("beaker_cache:*"): - data = client.get(key) - if data: - accessed_time = 0 - - try: - data = base64.b64decode(data) - json_data = pickle.loads(data) - accessed_time = json_data["_accessed_time"] - except binascii.Error: - accessed_time = 0 - except pickle.UnpicklingError: - accessed_time = 0 - except KeyError: - accessed_time = 0 - - if accessed_time < expiry_time: - client.delete(key) - deleted_keys += 1 - - return deleted_keys - - -class MemoryAuthSessions(BaseAuthSessions): - SESSION_TYPE = "memory" - def get_count(self): return self.NOT_AVAILABLE @@ -243,19 +39,11 @@ class MemoryAuthSessions(BaseAuthSessions): return self.NOT_AVAILABLE def clean_sessions(self, older_than_seconds=None): - raise CleanupCommand("Cleanup for this session type not yet available") + raise CleanupCommand( + "Cookie-based sessions are client-side only. " + "Users will be logged out when their cookie expires." + ) def get_session_handler(session_type): - types = { - "file": FileAuthSessions, - "ext:memcached": MemcachedAuthSessions, - "ext:redis": RedisAuthSessions, - "ext:database": DbAuthSessions, - "memory": MemoryAuthSessions, - } - - try: - return types[session_type] - except KeyError: - raise ValueError(f"This type {session_type} is not supported") + return CookieAuthSessions diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py index 6938f7d0..c13559b7 100644 --- a/rhodecode/lib/utils2.py +++ b/rhodecode/lib/utils2.py @@ -282,7 +282,7 @@ def engine_from_config(configuration, prefix="sqlalchemy.", **kwargs): def get_encryption_key(config) -> bytes: secret = config.get("rhodecode.encrypted_values.secret") - default = config["beaker.session.secret"] + default = config.get("session.secret", config.get("beaker.session.secret", "")) enc_key = secret or default return safe_bytes(enc_key) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index a5b8e5e4..3f2f1ad2 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -117,7 +117,7 @@ log = logging.getLogger(__name__) # ============================================================================= # this is propagated from .ini file rhodecode.encrypted_values.secret or -# beaker.session.secret if first is not set. +# session.secret if first is not set. # and initialized at environment.py ENCRYPTION_KEY: bytes = b"" diff --git a/rhodecode/templates/admin/settings/settings_sessions.mako b/rhodecode/templates/admin/settings/settings_sessions.mako index 00704b59..f15fee7a 100644 --- a/rhodecode/templates/admin/settings/settings_sessions.mako +++ b/rhodecode/templates/admin/settings/settings_sessions.mako @@ -6,7 +6,7 @@ <% elems = [ (_('Session type'), c.session_model.SESSION_TYPE, ''), - (_('Session expiration period'), '{} seconds'.format(c.session_conf.get('beaker.session.timeout', 0)), ''), + (_('Session expiration period'), '{} seconds'.format(c.session_conf.get('session.timeout', c.session_conf.get('beaker.session.timeout', 0))), ''), (_('Total sessions'), c.session_count, ''), (_('Expired sessions ({} days)').format(c.cleanup_older_days ), c.session_expired_count, ''), diff --git a/rhodecode/templates/login.mako b/rhodecode/templates/login.mako index 3f5efae3..1e1a246c 100644 --- a/rhodecode/templates/login.mako +++ b/rhodecode/templates/login.mako @@ -60,7 +60,7 @@ %endif ${h.checkbox('remember', value=True, checked=defaults.get('remember'))} - <% timeout = request.registry.settings.get('beaker.session.timeout', '0') %> + <% timeout = request.registry.settings.get('session.timeout', request.registry.settings.get('beaker.session.timeout', '0')) %> % if timeout == '0': <% remember_label = _('Remember my indefinitely') %> % else: diff --git a/rhodecode/tests/rhodecode.ini b/rhodecode/tests/rhodecode.ini index 7a8726cb..cdeaef56 100644 --- a/rhodecode/tests/rhodecode.ini +++ b/rhodecode/tests/rhodecode.ini @@ -46,7 +46,7 @@ use = egg:gunicorn#main ; allows to set RhodeCode under a prefix in server. ; eg https://server.com/custom_prefix. Enable `filter-with =` option below as well. ; And set your prefix like: `prefix = /custom_prefix` -; be sure to also set beaker.session.cookie_path = /custom_prefix if you need +; be sure to also set session.cookie_path = /custom_prefix if you need ; to make your cookies only work on prefix url [filter:proxy-prefix] use = egg:PasteDeploy#prefix @@ -76,7 +76,7 @@ rhodecode.env_expand = false ; encryption key used to encrypt social plugin tokens, ; remote_urls with credentials etc, if not set it defaults to -; `beaker.session.secret` +; `session.secret` #rhodecode.encrypted_values.secret = ; decryption strict mode (enabled by default). It controls if decryption raises @@ -520,51 +520,16 @@ rc_cache.cache_repo.arguments.filename = %(here)s/.rc-test-data/cache-backend/ca #rc_cache.cache_repo.arguments.key_prefix = custom-prefix- -; ############## -; BEAKER SESSION -; ############## +; ###################### +; SESSION CONFIGURATION +; ###################### -; beaker.session.type is type of storage options for the logged users sessions. Current allowed -; types are file, ext:redis, ext:database, ext:memcached -; Fastest ones are ext:redis and ext:database, DO NOT use memory type for session -beaker.session.type = file -beaker.session.data_dir = %(here)s/.rc-test-data/data/sessions - -; Redis based sessions -#beaker.session.type = ext:redis -#beaker.session.url = redis://redis:6379/2 - -; DB based session, fast, and allows easy management over logged in users -#beaker.session.type = ext:database -#beaker.session.table_name = db_session -#beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode -#beaker.session.sa.url = mysql://root:secret@127.0.0.1/rhodecode -#beaker.session.sa.pool_recycle = 3600 -#beaker.session.sa.echo = false - -beaker.session.key = rhodecode -beaker.session.secret = test-rc-uytcxaz -beaker.session.lock_dir = %(here)s/.rc-test-data/data/sessions/lock - -; Secure encrypted cookie. Requires AES and AES python libraries -; you must disable beaker.session.secret to use this -#beaker.session.encrypt_key = key_for_encryption -#beaker.session.validate_key = validation_key - -; Sets session as invalid (also logging out user) if it haven not been -; accessed for given amount of time in seconds -beaker.session.timeout = 2592000 -beaker.session.httponly = true - -; Path to use for the cookie. Set to prefix if you use prefix middleware -#beaker.session.cookie_path = /custom_prefix - -; Set https secure cookie -beaker.session.secure = false - -; default cookie expiration time in seconds, set to `true` to set expire -; at browser close -#beaker.session.cookie_expires = 3600 +; Cookie-based signed sessions (no server-side session store needed) +session.secret = test-rc-uytcxaz +session.key = rhodecode +session.timeout = 2592000 +session.httponly = true +session.secure = false ; ############################# ; SEARCH INDEXING CONFIGURATION diff --git a/rhodecode/tests/utils.py b/rhodecode/tests/utils.py index 6ad0fb33..6362e139 100644 --- a/rhodecode/tests/utils.py +++ b/rhodecode/tests/utils.py @@ -116,8 +116,8 @@ class CustomTestResponse(TestResponse): """ from rhodecode.lib.rc_beaker import session_factory_from_settings - session = session_factory_from_settings(self.test_app._pyramid_settings) - return session(self.request) + factory = session_factory_from_settings(self.test_app._pyramid_settings) + return factory(self.request) class TestRequest(webob.BaseRequest):