示例#1
0
    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
示例#2
0
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)
示例#3
0
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)
示例#4
0
 def __init__(self):
     self.json = JSONSerializer()
     self.pickle = PickleSerializer()
示例#5
0
    def _makeOne(self):
        from pyramid.session import PickleSerializer

        return PickleSerializer()
示例#6
0
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,
    )
示例#7
0
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
示例#8
0
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