def __init__(self, secret, serializer=None): if len(secret) != SecretBox.KEY_SIZE: raise ValueError( "Secret should be a random bytes string of length {}".format( SecretBox.KEY_SIZE ) ) self.box = SecretBox(secret) if serializer is None: serializer = PickleSerializer() self.serializer = serializer
class JSONSerializerWithPickleFallback(object): def __init__(self): self.json = JSONSerializer() self.pickle = PickleSerializer() def dumps(self, value): return self.json.dumps(value) def loads(self, value): try: return self.json.loads(value) except ValueError: return self.pickle.loads(value)
class JSONSerializerWithPickleFallback(object): def __init__(self): self.json = JSONSerializer() self.pickle = PickleSerializer() def dumps(self, value): # maybe catch serialization errors here and keep using pickle # while finding spots in your app that are not storing # JSON-serializable objects, falling back to pickle return self.json.dumps(value) def loads(self, value): try: return self.json.loads(value) except ValueError: return self.pickle.loads(value)
def __init__(self): self.json = JSONSerializer() self.pickle = PickleSerializer()
def _makeOne(self): from pyramid.session import PickleSerializer return PickleSerializer()
def EncryptedCookieSessionFactory( secret, cookie_name="session", max_age=None, path="/", domain=None, secure=False, httponly=False, timeout=1200, reissue_time=0, set_on_exception=True, serializer=None, ): """ Configure a :term:`session factory` which will provide encrypted cookie-based sessions. The return value of this function is a :term:`session factory` which may be used with the :meth:`pyramid.config.Configurator.set_session_factory` method. The session factory returned by this function will create sessions which are limited to storing fewer than 4000 bytes of data (as the payload must fit into a single cookie). Parameters: ``secret`` A string which is used to sign the cookie. The secret should be at least as long as the block size of the selected hash algorithm. For ``sha512`` this would mean a 128 bit (64 character) secret. It should be unique within the set of secret values provided to Pyramid for its various subsystems (see :ref:`admonishment_against_secret_sharing`). ``cookie_name`` The name of the cookie used for sessioning. Default: ``'session'``. ``max_age`` The maximum age of the cookie used for sessioning (in seconds). Default: ``None`` (browser scope). ``path`` The path used for the session cookie. Default: ``'/'``. ``domain`` The domain used for the session cookie. Default: ``None`` (no domain). ``secure`` The 'secure' flag of the session cookie. Default: ``False``. ``httponly`` Hide the cookie from Javascript by setting the 'HttpOnly' flag of the session cookie. Default: ``False``. ``timeout`` A number of seconds of inactivity before a session times out. If ``None`` then the cookie never expires. This lifetime only applies to the *value* within the cookie. Meaning that if the cookie expires due to a lower ``max_age``, then this setting has no effect. Default: ``1200``. ``reissue_time`` The number of seconds that must pass before the cookie is automatically reissued as the result of accessing the session. The duration is measured as the number of seconds since the last session cookie was issued and 'now'. If this value is ``0``, a new cookie will be reissued on every request accessing the session. If ``None`` then the cookie's lifetime will never be extended. A good rule of thumb: if you want auto-expired cookies based on inactivity: set the ``timeout`` value to 1200 (20 mins) and set the ``reissue_time`` value to perhaps a tenth of the ``timeout`` value (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower than the ``reissue_time`` value, as the ticket will never be reissued. However, such a configuration is not explicitly prevented. Default: ``0``. ``set_on_exception`` If ``True``, set a session cookie even if an exception occurs while rendering a view. Default: ``True``. ``serializer`` An object with two methods: ``loads`` and ``dumps``. The ``loads`` method should accept bytes and return a Python object. The ``dumps`` method should accept a Python object and return bytes. A ``ValueError`` should be raised for malformed inputs. If a serializer is not passed, the :class:`pyramid.session.PickleSerializer` serializer will be used. """ if serializer is None: serializer = PickleSerializer() encrypted_serializer = EncryptedSerializer( secret, serializer=serializer, ) return BaseCookieSessionFactory( encrypted_serializer, cookie_name=cookie_name, max_age=max_age, path=path, domain=domain, secure=secure, httponly=httponly, timeout=timeout, reissue_time=reissue_time, set_on_exception=set_on_exception, )
def main(global_config, testing=None, session=None, **settings): """ Return a WSGI application. Args: global_config (dict): A dictionary with two keys: __file__, a path to the ini file, and here, the path to the code. testing (str or None): If this app is contructed by the unit tests, they should set this to a username. session (sqlalchemy.orm.session.Session or None): If given, the session will be used instead of building a new one. settings (dictionary): Unused. Returns: pyramid.router.Router: A WSGI app. """ if settings: bodhi_config.load_config(settings) # Setup our bugtracker and buildsystem bugs.set_bugtracker() setup_buildsys() # Sessions & Caching session_factory = SignedCookieSessionFactory( bodhi_config['session.secret'], serializer=PickleSerializer(), ) # Construct a list of all groups we're interested in default = [] for key in ('important_groups', 'admin_packager_groups', 'mandatory_packager_groups', 'admin_groups'): default.extend(bodhi_config.get(key)) # pyramid_fas_openid looks for this setting bodhi_config['openid.groups'] = bodhi_config.get('openid.groups', default) config = Configurator(settings=bodhi_config, session_factory=session_factory) # Plugins config.include('pyramid_mako') config.include('cornice') # Lazy-loaded memoized request properties if session: config.registry.sessionmaker = lambda: session else: # Initialize the database scoped session initialize_db(bodhi_config) config.registry.sessionmaker = Session config.add_request_method(lambda x: Session, 'db', reify=True) config.add_request_method(get_user, 'user', reify=True) config.add_request_method(get_koji, 'koji', reify=True) config.add_request_method(get_cacheregion, 'cache', reify=True) config.add_request_method(get_buildinfo, 'buildinfo', reify=True) config.add_request_method(get_from_tag_inherited, 'from_tag_inherited', reify=True) config.add_request_method(get_releases, 'releases', reify=True) # Templating config.add_mako_renderer('.html', settings_prefix='mako.') config.add_static_view(f'static/v{pkg_resources.get_distribution("bodhi-server").version}', 'bodhi.server:static') from bodhi.server.renderers import rss config.add_renderer('rss', rss) config.add_renderer('jsonp', JSONP(param_name='callback')) # i18n config.add_translation_dirs('bodhi.server:locale/') # Authentication & Authorization if testing: # use a permissive security policy while running unit tests config.testing_securitypolicy(userid=testing, permissive=True) else: timeout = bodhi_config.get('authtkt.timeout') config.set_authentication_policy(AuthTktAuthenticationPolicy( bodhi_config['authtkt.secret'], callback=groupfinder, secure=bodhi_config['authtkt.secure'], hashalg='sha512', timeout=timeout, max_age=timeout, samesite='Strict')) config.set_authorization_policy(ACLAuthorizationPolicy()) # Collect metrics for endpoints config.add_tween( 'bodhi.server.services.metrics_tween.histo_tween_factory', over=EXCVIEW ) # Metrics Route config.add_route('prometheus_metric', '/metrics') # Frontpage config.add_route('home', '/') # Views for creating new objects config.add_route('new_update', '/updates/new') config.add_route('new_override', '/overrides/new') # Auto-completion search config.add_route('latest_candidates', '/latest_candidates') config.add_route('latest_builds', '/latest_builds') config.add_route('get_sidetags', '/get_sidetags') config.add_route('latest_builds_in_tag', '/latest_builds_in_tag') # Include the auth system (after loading the models) config.include("bodhi.server.auth") config.add_route('api_version', '/api_version') config.add_route('liveness', '/healthz/live') config.add_route('readyness', '/healthz/ready') # Legacy: Redirect the previously self-hosted documentation # https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/hybrid.html#using-subpath-in-a-route-pattern config.add_route("docs", "/docs/*subpath") config.scan('bodhi.server.views') config.scan('bodhi.server.services') config.scan('bodhi.server.webapp') # Though importing in the middle of this function is the darkest of evils, we cannot do it any # other way without a backwards-incompatible change. See # https://github.com/fedora-infra/bodhi/issues/2294 from bodhi.server import models from bodhi.server.views import generic # Let's put a cache on the home page stats, but only if it isn't already cached. The cache adds # an invalidate attribute to the method, so that's how we can tell. The server would not # encounter this function already having a cache in normal operation, but the unit tests do run # this function many times so we don't want them to cause it to cache a cache of the cache of # the cache… if not hasattr(generic._generate_home_page_stats, 'invalidate'): generic._generate_home_page_stats = get_cacheregion(None).cache_on_arguments()( generic._generate_home_page_stats) if bodhi_config['warm_cache_on_start']: log.info('Warming up caches…') # Let's warm up the Releases._all_releases cache. We can just call the function - we don't # need to capture the return value. models.Release.all_releases() # Let's warm up the home page cache by calling _generate_home_page_stats(). We can ignore # the return value. generic._generate_home_page_stats() # Let's close out the db session we used to warm the caches. Session.remove() log.info('Bodhi ready and at your service!') app = config.make_wsgi_app() return app
def PluggableSessionFactory( secret, cookie_name='session', max_age=None, path='/', domain=None, secure=False, httponly=False, set_on_exception=True, timeout=1200, reissue_time=0, hashalg='sha512', salt='pyramid_pluggable_session.', serializer=None, ): """ .. versionadded:: 1.5 Configure a :term:`session factory` which will provide signed cookie-based sessions. The return value of this function is a :term:`session factory`, which may be provided as the ``session_factory`` argument of a :class:`pyramid.config.Configurator` constructor, or used as the ``session_factory`` argument of the :meth:`pyramid.config.Configurator.set_session_factory` method. The session factory returned by this function will create sessions which are limited to storing fewer than 4000 bytes of data (as the payload must fit into a single cookie). Parameters: ``secret`` A string which is used to sign the cookie. The secret should be at least as long as the block size of the selected hash algorithm. For ``sha512`` this would mean a 128 bit (64 character) secret. It should be unique within the set of secret values provided to Pyramid for its various subsystems (see :ref:`admonishment_against_secret_sharing`). ``hashalg`` The HMAC digest algorithm to use for signing. The algorithm must be supported by the :mod:`hashlib` library. Default: ``'sha512'``. ``salt`` A namespace to avoid collisions between different uses of a shared secret. Reusing a secret for different parts of an application is strongly discouraged (see :ref:`admonishment_against_secret_sharing`). Default: ``'pyramid.session.'``. ``cookie_name`` The name of the cookie used for sessioning. Default: ``'session'``. ``max_age`` The maximum age of the cookie used for sessioning (in seconds). Default: ``None`` (browser scope). ``path`` The path used for the session cookie. Default: ``'/'``. ``domain`` The domain used for the session cookie. Default: ``None`` (no domain). ``secure`` The 'secure' flag of the session cookie. Default: ``False``. ``httponly`` Hide the cookie from Javascript by setting the 'HttpOnly' flag of the session cookie. Default: ``False``. ``timeout`` A number of seconds of inactivity before a session times out. If ``None`` then the cookie never expires. This lifetime only applies to the *value* within the cookie. Meaning that if the cookie expires due to a lower ``max_age``, then this setting has no effect. Default: ``1200``. ``reissue_time`` The number of seconds that must pass before the cookie is automatically reissued as the result of accessing the session. The duration is measured as the number of seconds since the last session cookie was issued and 'now'. If this value is ``0``, a new cookie will be reissued on every request accessing the session. If ``None`` then the cookie's lifetime will never be extended. A good rule of thumb: if you want auto-expired cookies based on inactivity: set the ``timeout`` value to 1200 (20 mins) and set the ``reissue_time`` value to perhaps a tenth of the ``timeout`` value (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower than the ``reissue_time`` value, as the ticket will never be reissued. However, such a configuration is not explicitly prevented. Default: ``0``. ``set_on_exception`` If ``True``, set a session cookie even if an exception occurs while rendering a view. Default: ``True``. ``serializer`` An object with two methods: ``loads`` and ``dumps``. The ``loads`` method should accept bytes and return a Python object. The ``dumps`` method should accept a Python object and return bytes. A ``ValueError`` should be raised for malformed inputs. If a serializer is not passed, the :class:`pyramid.session.PickleSerializer` serializer will be used. """ if serializer is None: serializer = PickleSerializer() signed_serializer = SignedSerializer( secret + '_internal_use', salt + '_internal_use', hashalg, serializer=serializer, ) serializer = signed_serializer @implementer(ISession) class PluggableSession(dict): """ Dictionary-like session object """ # configuration parameters _cookie_on_exception = set_on_exception _timeout = timeout _reissue_time = reissue_time # dirty flag _dirty = False def __init__(self, request): self._cookie = CookieHelper( secret, salt, cookie_name, secure=secure, max_age=max_age, httponly=httponly, path=path, domains=domain, hashalg=hashalg, ) self._session_id = None self.request = request reg = request.registry plug = reg.queryUtility(IPlugSession) if plug is None: raise RuntimeError( 'Unable to find any registered IPlugSession') now = time.time() created = renewed = now new = True value = None state = {} # Get the session_id self._session_id = self._cookie.bind(request).get_value() if self._session_id is not None: try: sess_val = plug.loads(self, request) value = serializer.loads(bytes_(sess_val)) except ValueError: value = None # Cleanup the session, since it failed to deserialize plug.clear(self, request) self._session_id = None if value is not None: try: rval, cval, sval = value renewed = float(rval) created = float(cval) state = sval new = False except (TypeError, ValueError): # value failed to unpack properly or renewed was not # a numeric type so we'll fail deserialization here state = {} # Clean up the session since it failed to unpack plug.clear(self, request) self._session_id = None if self._timeout is not None: if now - renewed > self._timeout: # expire the session because it was not renewed # before the timeout threshold state = {} # Session expired, cleanup this session plug.clear(self, request) self._session_id = None # Generate a new session id if self._session_id is None: self._generate_new_id() self.created = created self.accessed = renewed self.renewed = renewed self.new = new self._plug = plug dict.__init__(self, state) # ISession methods def changed(self): if not self._dirty: self._dirty = True def save_session_callback(request, response): self._save_session(response) self.request = None # explicitly break cycle for gc self.request.add_response_callback(save_session_callback) def invalidate(self): self._plug.clear(self, self.request) self._generate_new_id() now = time.time() self.created = self.renewed = now self.new = True self.clear() # non-modifying dictionary methods get = manage_accessed(dict.get) __getitem__ = manage_accessed(dict.__getitem__) items = manage_accessed(dict.items) values = manage_accessed(dict.values) keys = manage_accessed(dict.keys) __contains__ = manage_accessed(dict.__contains__) __len__ = manage_accessed(dict.__len__) __iter__ = manage_accessed(dict.__iter__) if not PY3: iteritems = manage_accessed(dict.iteritems) itervalues = manage_accessed(dict.itervalues) iterkeys = manage_accessed(dict.iterkeys) has_key = manage_accessed(dict.has_key) # modifying dictionary methods clear = manage_changed(dict.clear) update = manage_changed(dict.update) setdefault = manage_changed(dict.setdefault) pop = manage_changed(dict.pop) popitem = manage_changed(dict.popitem) __setitem__ = manage_changed(dict.__setitem__) __delitem__ = manage_changed(dict.__delitem__) # flash API methods @manage_changed def flash(self, msg, queue='', allow_duplicate=True): storage = self.setdefault('_f_' + queue, []) if allow_duplicate or (msg not in storage): storage.append(msg) @manage_changed def pop_flash(self, queue=''): storage = self.pop('_f_' + queue, []) return storage @manage_accessed def peek_flash(self, queue=''): storage = self.get('_f_' + queue, []) return storage # CSRF API methods @manage_changed def new_csrf_token(self): token = text_(binascii.hexlify(os.urandom(20))) self['_csrft_'] = token return token @manage_accessed def get_csrf_token(self): token = self.get('_csrft_', None) if token is None: token = self.new_csrf_token() return token # non-API methods def _save_session(self, response): if not self._cookie_on_exception: exception = getattr(self.request, 'exception', None) if exception is not None: # dont set a cookie during exceptions return False sess_val = native_( serializer.dumps((self.accessed, self.created, dict(self)))) self._plug.dumps(self, self.request, sess_val) self._cookie.set_cookies(response, self._session_id) return True def _generate_new_id(self): self._session_id = text_(binascii.hexlify(os.urandom(20))) return PluggableSession