Remove beaker dependency, switch to Pyramid SignedCookieSessionFactory
Replace server-side session backends (redis, file, database, memcached) with client-side signed cookie sessions. Users will need to re-authenticate after deploy. - Rewrite rc_beaker.py to use pyramid.session.SignedCookieSessionFactory - Gut user_sessions.py to cookie-only stub - Drop beaker.container from memory_lru_dict.py (pure repoze.lru) - Migrate ini configs from beaker.session.* to session.* namespace - Remove beaker==1.13.0 from requirements.txt - Remove beaker.backends entry points from pyproject.toml - Preserve backward-compat fallbacks for encryption key resolution
This commit is contained in:
parent
ceb23e4d47
commit
4f73837822
20 changed files with 125 additions and 574 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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):
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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""
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ''),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue