Merge with pyramid-cubicweb
The following tasks have been done:
- merge packaging files
- merge documentation
- move pyramid_cubicweb package at cubicweb/pyramid and update imports
accordingly
- rename tests directory into test
- move pyramid-cubicweb README.rst into README.pyramid.rst until better idea
- add a test dependency on unreleased cubicweb-pyramid to have both py27 and
py34 tests pass
Closes #14023058.
--- a/MANIFEST.in Fri Sep 23 16:04:32 2016 +0200
+++ b/MANIFEST.in Mon Sep 26 14:52:12 2016 +0200
@@ -1,4 +1,5 @@
include README
+include README.pyramid.rst
include COPYING
include COPYING.LESSER
include pylintrc
@@ -13,7 +14,7 @@
recursive-include doc/book *
recursive-include doc/tools *.py
recursive-include doc/tutorials *.rst *.py
-include doc/api/*.rst
+recursive-include doc/api *.rst
recursive-include doc/_themes *
recursive-include doc/_static *
include doc/_templates/*.html
@@ -47,6 +48,7 @@
recursive-include cubicweb/ext/test/data *.py
recursive-include cubicweb/hooks/test/data-computed *.py
recursive-include cubicweb/hooks/test/data bootstrap_cubes *.py
+recursive-include cubicweb/pyramid/test/data bootstrap_cubes
recursive-include cubicweb/sobjects/test/data bootstrap_cubes *.py
recursive-include cubicweb/server/test/data bootstrap_cubes *.py source* *.conf.in *.ldif
recursive-include cubicweb/server/test/data-cwep002 *.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/README.pyramid.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,85 @@
+
+pyramid_cubicweb_ is one specific way of integrating CubicWeb_ with a
+Pyramid_ web application.
+
+Features
+========
+
+* provides a default route that let a cubicweb instance handle the request.
+
+Usage
+=====
+
+To use, install ``pyramid_cubicweb`` in your python environment, and
+then include_ the package::
+
+ config.include('pyramid_cubicweb')
+
+
+Configuration
+=============
+
+Requires the following `INI setting / environment variable`_:
+
+* `cubicweb.instance` / `CW_INSTANCE`: the cubicweb instance name
+
+Authentication cookies
+----------------------
+
+When using the `pyramid_cubicweb.auth` (CubicWeb AuthTkt
+authentication policy), which is the default in most cases, you may
+have to configure the behaviour of these authentication policies using
+standard's Pyramid configuration. You may want to configure in your
+``pyramid.ini``:
+
+:Session Authentication:
+
+ This is a `AuthTktAuthenticationPolicy`_ so you may overwrite default
+ configuration values by adding configuration entries using the prefix
+ ``cubicweb.auth.authtkt.session``. Default values are:
+
+ ::
+
+ cubicweb.auth.authtkt.session.hashalg = sha512
+ cubicweb.auth.authtkt.session.cookie_name = auth_tkt
+ cubicweb.auth.authtkt.session.timeout = 1200
+ cubicweb.auth.authtkt.session.reissue_time = 120
+ cubicweb.auth.authtkt.session.http_only = True
+ cubicweb.auth.authtkt.session.secure = True
+
+
+:Persistent Authentication:
+
+ This is also a `AuthTktAuthenticationPolicy`_. It is used when persistent
+ sessions are activated (typically when using the cubicweb-rememberme_
+ cube). You may overwrite default configuration values by adding
+ configuration entries using the prefix
+ ``cubicweb.auth.authtkt.persistent``. Default values are:
+
+ ::
+
+ cubicweb.auth.authtkt.persistent.hashalg = sha512
+ cubicweb.auth.authtkt.persistent.cookie_name = pauth_tkt
+ cubicweb.auth.authtkt.persistent.max_age = 3600*24*30
+ cubicweb.auth.authtkt.persistent.reissue_time = 3600*24
+ cubicweb.auth.authtkt.persistent.http_only = True
+ cubicweb.auth.authtkt.persistent.secure = True
+
+
+.. Warning:: Legacy timeout values from the instance's
+ ``all-in-one.conf`` are **not** used at all (``
+ http-session-time`` and ``cleanup-session-time``)
+
+Please refer to the documentation_ for more details (available in the
+``docs`` directory of the source code).
+
+.. _pyramid_cubicweb: https://www.cubicweb.org/project/pyramid-cubicweb
+.. _CubicWeb: https://www.cubicweb.org/
+.. _`cubicweb-rememberme`: \
+ https://www.cubicweb.org/project/cubicweb-rememberme
+.. _Pyramid: http://pypi.python.org/pypi/pyramid
+.. _include: http://docs.pylonsproject.org/projects/pyramid/en/latest/api/config.html#pyramid.config.Configurator.include
+.. _`INI setting / environment variable`: http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html#adding-a-custom-setting
+.. _documentation: http://pyramid-cubicweb.readthedocs.org/
+.. _AuthTktAuthenticationPolicy: \
+ http://docs.pylonsproject.org/projects/pyramid/en/latest/api/authentication.html#pyramid.authentication.AuthTktAuthenticationPolicy
--- a/cubicweb/__pkginfo__.py Fri Sep 23 16:04:32 2016 +0200
+++ b/cubicweb/__pkginfo__.py Mon Sep 26 14:52:12 2016 +0200
@@ -59,6 +59,11 @@
'pytz': '',
'Markdown': '',
'unittest2': '>= 0.7.0',
+ # pyramid dependencies
+ 'pyramid': '>= 1.5.0',
+ 'waitress': '>= 0.8.9',
+ 'wsgicors': '>= 0.3',
+ 'pyramid_multiauth': '',
}
__recommends__ = {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/__init__.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,193 @@
+import os
+from warnings import warn
+import wsgicors
+
+from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
+from pyramid.config import Configurator
+from pyramid.settings import asbool, aslist
+
+try:
+ from configparser import SafeConfigParser
+except ImportError:
+ from ConfigParser import SafeConfigParser
+
+
+def make_cubicweb_application(cwconfig, settings=None):
+ """
+ Create a pyramid-based CubicWeb instance from a cubicweb configuration.
+
+ It is initialy meant to be used by the 'pyramid' command of cubicweb-ctl.
+
+ :param cwconfig: A CubicWeb configuration
+ :returns: A Pyramid config object
+ """
+ settings = dict(settings) if settings else {}
+ settings.update(settings_from_cwconfig(cwconfig))
+ config = Configurator(settings=settings)
+ config.registry['cubicweb.config'] = cwconfig
+ config.include('cubicweb.pyramid')
+ return config
+
+def settings_from_cwconfig(cwconfig):
+ '''
+ Extract settings from pyramid.ini and pyramid-debug.ini (if in debug)
+
+ Can be used to configure middleware WSGI with settings from pyramid.ini files
+
+ :param cwconfig: A CubicWeb configuration
+ :returns: A settings dictionnary
+ '''
+ settings_filenames = [os.path.join(cwconfig.apphome, 'pyramid.ini')]
+ settings = {}
+ if cwconfig.debugmode:
+ settings_filenames.insert(
+ 0, os.path.join(cwconfig.apphome, 'pyramid-debug.ini'))
+
+ settings.update({
+ 'pyramid.debug_authorization': True,
+ 'pyramid.debug_notfound': True,
+ 'pyramid.debug_routematch': True,
+ 'pyramid.reload_templates': True,
+ })
+
+ for fname in settings_filenames:
+ if os.path.exists(fname):
+ cp = SafeConfigParser()
+ cp.read(fname)
+ settings.update(cp.items('main'))
+ break
+
+ return settings
+
+
+def wsgi_application_from_cwconfig(
+ cwconfig,
+ profile=False, profile_output=None, profile_dump_every=None):
+ """ Build a WSGI application from a cubicweb configuration
+
+ :param cwconfig: A CubicWeb configuration
+ :param profile: Enable profiling. See :ref:`profiling`.
+ :param profile_output: Profiling output filename. See :ref:`profiling`.
+ :param profile_dump_every: Profiling number of requests before dumping the
+ stats. See :ref:`profiling`.
+
+ :returns: A fully operationnal WSGI application
+ """
+ config = make_cubicweb_application(cwconfig)
+ profile = profile or asbool(config.registry.settings.get(
+ 'cubicweb.profile.enable', False))
+ if profile:
+ config.add_route('profile_ping', '_profile/ping')
+ config.add_route('profile_cnx', '_profile/cnx')
+ config.scan('cubicweb.pyramid.profile')
+ app = config.make_wsgi_app()
+ # This replaces completely web/cors.py, which is not used by
+ # cubicweb.pyramid anymore
+ app = wsgicors.CORS(
+ app,
+ origin=' '.join(cwconfig['access-control-allow-origin']),
+ headers=', '.join(cwconfig['access-control-allow-headers']),
+ methods=', '.join(cwconfig['access-control-allow-methods']),
+ credentials='true')
+
+ if profile:
+ from cubicweb.pyramid.profile import wsgi_profile
+ filename = profile_output or config.registry.settings.get(
+ 'cubicweb.profile.output', 'program.prof')
+ dump_every = profile_dump_every or config.registry.settings.get(
+ 'cubicweb.profile.dump_every', 100)
+ app = wsgi_profile(app, filename=filename, dump_every=dump_every)
+ return app
+
+
+def wsgi_application(instance_name=None, debug=None):
+ """ Build a WSGI application from a cubicweb instance name
+
+ :param instance_name: Name of the cubicweb instance (optional). If not
+ provided, :envvar:`CW_INSTANCE` must exists.
+ :param debug: Enable/disable the debug mode. If defined to True or False,
+ overrides :envvar:`CW_DEBUG`.
+
+ The following environment variables are used if they exist:
+
+ .. envvar:: CW_INSTANCE
+
+ A CubicWeb instance name.
+
+ .. envvar:: CW_DEBUG
+
+ If defined, the debugmode is enabled.
+
+ The function can be used as an entry-point for third-party wsgi containers.
+ Below is a sample uswgi configuration file:
+
+ .. code-block:: ini
+
+ [uwsgi]
+ http = 127.0.1.1:8080
+ env = CW_INSTANCE=myinstance
+ env = CW_DEBUG=1
+ module = cubicweb.pyramid:wsgi_application()
+ virtualenv = /home/user/.virtualenvs/myvirtualenv
+ processes = 1
+ threads = 8
+ stats = 127.0.0.1:9191
+ plugins = http,python
+
+ """
+ if instance_name is None:
+ instance_name = os.environ['CW_INSTANCE']
+ if debug is None:
+ debug = 'CW_DEBUG' in os.environ
+
+ cwconfig = cwcfg.config_for(instance_name, debugmode=debug)
+
+ return wsgi_application_from_cwconfig(cwconfig)
+
+
+def includeme(config):
+ """Set-up a CubicWeb instance.
+
+ The CubicWeb instance can be set in several ways:
+
+ - Provide an already loaded CubicWeb config instance in the registry:
+
+ .. code-block:: python
+
+ config.registry['cubicweb.config'] = your_config_instance
+
+ - Provide an instance name in the pyramid settings with
+ :confval:`cubicweb.instance`.
+
+ """
+ cwconfig = config.registry.get('cubicweb.config')
+
+ if cwconfig is None:
+ debugmode = asbool(
+ config.registry.settings.get('cubicweb.debug', False))
+ cwconfig = cwcfg.config_for(
+ config.registry.settings['cubicweb.instance'], debugmode=debugmode)
+ config.registry['cubicweb.config'] = cwconfig
+
+ if cwconfig.debugmode:
+ try:
+ config.include('pyramid_debugtoolbar')
+ except ImportError:
+ warn('pyramid_debugtoolbar package not available, install it to '
+ 'get UI debug features', RuntimeWarning)
+
+ config.registry['cubicweb.repository'] = repo = cwconfig.repository()
+ config.registry['cubicweb.registry'] = repo.vreg
+
+ if asbool(config.registry.settings.get('cubicweb.defaults', True)):
+ config.include('cubicweb.pyramid.defaults')
+
+ for name in aslist(config.registry.settings.get('cubicweb.includes', [])):
+ config.include(name)
+
+ config.include('cubicweb.pyramid.tools')
+ config.include('cubicweb.pyramid.predicates')
+ config.include('cubicweb.pyramid.core')
+
+ if asbool(config.registry.settings.get('cubicweb.bwcompat', True)):
+ config.include('cubicweb.pyramid.bwcompat')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/auth.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,180 @@
+import datetime
+import logging
+import warnings
+
+from zope.interface import implementer
+
+from pyramid.settings import asbool
+from pyramid.authorization import ACLAuthorizationPolicy
+from cubicweb.pyramid.core import get_principals
+from pyramid_multiauth import MultiAuthenticationPolicy
+
+from pyramid.authentication import AuthTktAuthenticationPolicy
+
+from pyramid.interfaces import IAuthenticationPolicy
+
+log = logging.getLogger(__name__)
+
+
+@implementer(IAuthenticationPolicy)
+class UpdateLoginTimeAuthenticationPolicy(object):
+ """An authentication policy that update the user last_login_time.
+
+ The update is done in the 'remember' method, which is called by the login
+ views login,
+
+ Usually used via :func:`includeme`.
+ """
+
+ def authenticated_userid(self, request):
+ pass
+
+ def effective_principals(self, request):
+ return ()
+
+ def remember(self, request, principal, **kw):
+ try:
+ repo = request.registry['cubicweb.repository']
+ with repo.internal_cnx() as cnx:
+ cnx.execute(
+ "SET U last_login_time %(now)s WHERE U eid %(user)s", {
+ 'now': datetime.datetime.now(),
+ 'user': principal})
+ cnx.commit()
+ except:
+ log.exception("Failed to update last_login_time")
+ return ()
+
+ def forget(self, request):
+ return ()
+
+
+class CWAuthTktAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ """
+ An authentication policy that inhibate the call the 'remember' if a
+ 'persistent' argument is passed to it, and is equal to the value that
+ was passed to the constructor.
+
+ This allow to combine two policies with different settings and select them
+ by just setting this argument.
+ """
+ def __init__(self, secret, persistent, defaults={}, prefix='', **settings):
+ self.persistent = persistent
+ unset = object()
+ kw = {}
+ # load string settings
+ for name in ('cookie_name', 'path', 'domain', 'hashalg'):
+ value = settings.get(prefix + name, defaults.get(name, unset))
+ if value is not unset:
+ kw[name] = value
+ # load boolean settings
+ for name in ('secure', 'include_ip', 'http_only', 'wild_domain',
+ 'parent_domain', 'debug'):
+ value = settings.get(prefix + name, defaults.get(name, unset))
+ if value is not unset:
+ kw[name] = asbool(value)
+ # load int settings
+ for name in ('timeout', 'reissue_time', 'max_age'):
+ value = settings.get(prefix + name, defaults.get(name, unset))
+ if value is not unset:
+ kw[name] = int(value)
+ super(CWAuthTktAuthenticationPolicy, self).__init__(secret, **kw)
+
+ def remember(self, request, principals, **kw):
+ if 'persistent' not in kw or kw.pop('persistent') == self.persistent:
+ return super(CWAuthTktAuthenticationPolicy, self).remember(
+ request, principals, **kw)
+ else:
+ return ()
+
+
+def includeme(config):
+ """ Activate the CubicWeb AuthTkt authentication policy.
+
+ Usually called via ``config.include('cubicweb.pyramid.auth')``.
+
+ See also :ref:`defaults_module`
+ """
+ settings = config.registry.settings
+
+ policies = []
+
+ if asbool(settings.get('cubicweb.auth.update_login_time', True)):
+ policies.append(UpdateLoginTimeAuthenticationPolicy())
+
+ if asbool(settings.get('cubicweb.auth.authtkt', True)):
+ session_prefix = 'cubicweb.auth.authtkt.session.'
+ persistent_prefix = 'cubicweb.auth.authtkt.persistent.'
+
+ try:
+ secret = config.registry['cubicweb.config']['pyramid-auth-secret']
+ warnings.warn(
+ "pyramid-auth-secret from all-in-one is now "
+ "cubicweb.auth.authtkt.[session|persistent].secret",
+ DeprecationWarning)
+ except:
+ secret = 'notsosecret'
+
+ session_secret = settings.get(
+ session_prefix + 'secret', secret)
+ persistent_secret = settings.get(
+ persistent_prefix + 'secret', secret)
+
+ if 'notsosecret' in (session_secret, persistent_secret):
+ warnings.warn('''
+
+ !! SECURITY WARNING !!
+
+ The authentication cookies are signed with a static secret key.
+
+ Configure the following options in your pyramid.ini file:
+
+ - cubicweb.auth.authtkt.session.secret
+ - cubicweb.auth.authtkt.persistent.secret
+
+ YOU SHOULD STOP THIS INSTANCE unless your really know what you
+ are doing !!
+
+ ''')
+
+ policies.append(
+ CWAuthTktAuthenticationPolicy(
+ session_secret, False,
+ defaults={
+ 'hashalg': 'sha512',
+ 'cookie_name': 'auth_tkt',
+ 'timeout': 1200,
+ 'reissue_time': 120,
+ 'http_only': True,
+ 'secure': True
+ },
+ prefix=session_prefix,
+ **settings
+ )
+ )
+
+ policies.append(
+ CWAuthTktAuthenticationPolicy(
+ persistent_secret, True,
+ defaults={
+ 'hashalg': 'sha512',
+ 'cookie_name': 'pauth_tkt',
+ 'max_age': 3600*24*30,
+ 'reissue_time': 3600*24,
+ 'http_only': True,
+ 'secure': True
+ },
+ prefix=persistent_prefix,
+ **settings
+ )
+ )
+
+ kw = {}
+ if asbool(settings.get('cubicweb.auth.groups_principals', True)):
+ kw['callback'] = get_principals
+
+ authpolicy = MultiAuthenticationPolicy(policies, **kw)
+ config.registry['cubicweb.authpolicy'] = authpolicy
+
+ config.set_authentication_policy(authpolicy)
+ config.set_authorization_policy(ACLAuthorizationPolicy())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/bwcompat.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,213 @@
+import sys
+import logging
+
+from pyramid import security
+from pyramid import tweens
+from pyramid.httpexceptions import HTTPSeeOther
+from pyramid import httpexceptions
+from pyramid.settings import asbool
+
+import cubicweb
+import cubicweb.web
+
+from cubicweb.web.application import CubicWebPublisher
+
+from cubicweb.web import LogOut, PublishException
+
+from cubicweb.pyramid.core import cw_to_pyramid
+
+
+log = logging.getLogger(__name__)
+
+
+class PyramidSessionHandler(object):
+ """A CW Session handler that rely on the pyramid API to fetch the needed
+ informations.
+
+ It implements the :class:`cubicweb.web.application.CookieSessionHandler`
+ API.
+ """
+
+ def __init__(self, appli):
+ self.appli = appli
+
+ def get_session(self, req):
+ return req._request.cw_session
+
+ def logout(self, req, goto_url):
+ raise LogOut(url=goto_url)
+
+
+class CubicWebPyramidHandler(object):
+ """ A Pyramid request handler that rely on a cubicweb instance to do the
+ whole job
+
+ :param appli: A CubicWeb 'Application' object.
+ """
+ def __init__(self, appli):
+ self.appli = appli
+
+ def __call__(self, request):
+ """
+ Handler that mimics what CubicWebPublisher.main_handle_request and
+ CubicWebPublisher.core_handle do
+ """
+
+ # XXX The main handler of CW forbid anonymous https connections
+ # I guess we can drop this "feature" but in doubt I leave this comment
+ # so we don't forget about it. (cdevienne)
+
+ req = request.cw_request
+ vreg = request.registry['cubicweb.registry']
+
+ try:
+ content = None
+ try:
+ with cw_to_pyramid(request):
+ ctrlid, rset = self.appli.url_resolver.process(req,
+ req.path)
+
+ try:
+ controller = vreg['controllers'].select(
+ ctrlid, req, appli=self.appli)
+ except cubicweb.NoSelectableObject:
+ raise httpexceptions.HTTPUnauthorized(
+ req._('not authorized'))
+
+ req.update_search_state()
+ content = controller.publish(rset=rset)
+
+ # XXX this auto-commit should be handled by the cw_request
+ # cleanup or the pyramid transaction manager.
+ # It is kept here to have the ValidationError handling bw
+ # compatible
+ if req.cnx:
+ txuuid = req.cnx.commit()
+ # commited = True
+ if txuuid is not None:
+ req.data['last_undoable_transaction'] = txuuid
+ except cubicweb.web.ValidationError as ex:
+ # XXX The validation_error_handler implementation is light, we
+ # should redo it better in cw_to_pyramid, so it can be properly
+ # handled when raised from a cubicweb view.
+ # BUT the real handling of validation errors should be done
+ # earlier in the controllers, not here. In the end, the
+ # ValidationError should never by handled here.
+ content = self.appli.validation_error_handler(req, ex)
+ except cubicweb.web.RemoteCallFailed as ex:
+ # XXX The default pyramid error handler (or one that we provide
+ # for this exception) should be enough
+ # content = self.appli.ajax_error_handler(req, ex)
+ raise
+
+ if content is not None:
+ request.response.body = content
+
+
+ except LogOut as ex:
+ # The actual 'logging out' logic should be in separated function
+ # that is accessible by the pyramid views
+ headers = security.forget(request)
+ raise HTTPSeeOther(ex.url, headers=headers)
+ except cubicweb.AuthenticationError:
+ # Will occur upon access to req.cnx which is a
+ # cubicweb.dbapi._NeedAuthAccessMock.
+ if not content:
+ content = vreg['views'].main_template(req, 'login')
+ request.response.status_code = 403
+ request.response.body = content
+ finally:
+ # XXX CubicWebPyramidRequest.headers_out should
+ # access directly the pyramid response headers.
+ request.response.headers.clear()
+ for k, v in req.headers_out.getAllRawHeaders():
+ for item in v:
+ request.response.headers.add(k, item)
+
+ return request.response
+
+ def error_handler(self, exc, request):
+ req = request.cw_request
+ if isinstance(exc, httpexceptions.HTTPException):
+ request.response = exc
+ elif isinstance(exc, PublishException) and exc.status is not None:
+ request.response = httpexceptions.exception_response(exc.status)
+ else:
+ request.response = httpexceptions.HTTPInternalServerError()
+ request.response.cache_control = 'no-cache'
+ vreg = request.registry['cubicweb.registry']
+ excinfo = sys.exc_info()
+ req.reset_message()
+ if req.ajax_request:
+ content = self.appli.ajax_error_handler(req, exc)
+ else:
+ try:
+ req.data['ex'] = exc
+ req.data['excinfo'] = excinfo
+ errview = vreg['views'].select('error', req)
+ template = self.appli.main_template_id(req)
+ content = vreg['views'].main_template(req, template, view=errview)
+ except Exception:
+ content = vreg['views'].main_template(req, 'error-template')
+ log.exception(exc)
+ request.response.body = content
+ return request.response
+
+
+class TweenHandler(object):
+ """ A Pyramid tween handler that submit unhandled requests to a Cubicweb
+ handler.
+
+ The CubicWeb handler to use is expected to be in the pyramid registry, at
+ key ``'cubicweb.handler'``.
+ """
+ def __init__(self, handler, registry):
+ self.handler = handler
+ self.cwhandler = registry['cubicweb.handler']
+
+ def __call__(self, request):
+ if request.path.startswith('/https/'):
+ request.environ['PATH_INFO'] = request.environ['PATH_INFO'][6:]
+ assert not request.path.startswith('/https/')
+ request.scheme = 'https'
+ try:
+ response = self.handler(request)
+ except httpexceptions.HTTPNotFound:
+ response = self.cwhandler(request)
+ return response
+
+
+def includeme(config):
+ """ Set up a tween app that will handle the request if the main application
+ raises a HTTPNotFound exception.
+
+ This is to keep legacy compatibility for cubes that makes use of the
+ cubicweb urlresolvers.
+
+ It provides, for now, support for cubicweb controllers, but this feature
+ will be reimplemented separatly in a less compatible way.
+
+ It is automatically included by the configuration system, but can be
+ disabled in the :ref:`pyramid_settings`:
+
+ .. code-block:: ini
+
+ cubicweb.bwcompat = no
+ """
+ cwconfig = config.registry['cubicweb.config']
+ repository = config.registry['cubicweb.repository']
+ cwappli = CubicWebPublisher(
+ repository, cwconfig,
+ session_handler_fact=PyramidSessionHandler)
+ cwhandler = CubicWebPyramidHandler(cwappli)
+
+ config.registry['cubicweb.appli'] = cwappli
+ config.registry['cubicweb.handler'] = cwhandler
+
+ config.add_tween(
+ 'cubicweb.pyramid.bwcompat.TweenHandler', under=tweens.EXCVIEW)
+ if asbool(config.registry.settings.get(
+ 'cubicweb.bwcompat.errorhandler', True)):
+ config.add_view(cwhandler.error_handler, context=Exception)
+ # XXX why do i need this?
+ config.add_view(cwhandler.error_handler, context=httpexceptions.HTTPForbidden)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/core.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,400 @@
+import itertools
+
+from contextlib import contextmanager
+from warnings import warn
+from cgi import FieldStorage
+
+import rql
+
+from cubicweb.web.request import CubicWebRequestBase
+from cubicweb import repoapi
+
+import cubicweb
+import cubicweb.web
+from cubicweb.server import session as cwsession
+
+from pyramid import httpexceptions
+
+from cubicweb.pyramid import tools
+
+import logging
+
+log = logging.getLogger(__name__)
+
+
+CW_321 = cubicweb.__pkginfo__.numversion >= (3, 21, 0)
+
+
+class Connection(cwsession.Connection):
+ """ A specialised Connection that access the session data through a
+ property.
+
+ This behavior makes sure the actual session data is not loaded until
+ actually accessed.
+ """
+ def __init__(self, session, *args, **kw):
+ super(Connection, self).__init__(session, *args, **kw)
+ self._session = session
+
+ def _get_session_data(self):
+ return self._session.data
+
+ def _set_session_data(self, data):
+ pass
+
+ _session_data = property(_get_session_data, _set_session_data)
+
+
+class Session(cwsession.Session):
+ """ A Session that access the session data through a property.
+
+ Along with :class:`Connection`, it avoid any load of the pyramid session
+ data until it is actually accessed.
+ """
+ def __init__(self, pyramid_request, user, repo):
+ super(Session, self).__init__(user, repo)
+ self._pyramid_request = pyramid_request
+
+ def get_data(self):
+ if not getattr(self, '_protect_data_access', False):
+ self._data_accessed = True
+ return self._pyramid_request.session
+
+ def set_data(self, data):
+ if getattr(self, '_data_accessed', False):
+ self._pyramid_request.session.clear()
+ self._pyramid_request.session.update(data)
+
+ data = property(get_data, set_data)
+
+ def new_cnx(self):
+ self._protect_data_access = True
+ try:
+ return Connection(self)
+ finally:
+ self._protect_data_access = False
+
+
+def cw_headers(request):
+ return itertools.chain(
+ *[[(k, item) for item in v]
+ for k, v in request.cw_request.headers_out.getAllRawHeaders()])
+
+
+@contextmanager
+def cw_to_pyramid(request):
+ """ Context manager to wrap a call to the cubicweb API.
+
+ All CW exceptions will be transformed into their pyramid equivalent.
+ When needed, some CW reponse bits may be converted too (mainly headers)"""
+ try:
+ yield
+ except cubicweb.web.Redirect as ex:
+ assert 300 <= ex.status < 400
+ raise httpexceptions.status_map[ex.status](
+ ex.location, headers=cw_headers(request))
+ except cubicweb.web.StatusResponse as ex:
+ warn('[3.16] StatusResponse is deprecated use req.status_out',
+ DeprecationWarning, stacklevel=2)
+ request.body = ex.content
+ request.status_int = ex.status
+ except cubicweb.web.Unauthorized as ex:
+ raise httpexceptions.HTTPForbidden(
+ request.cw_request._(
+ 'You\'re not authorized to access this page. '
+ 'If you think you should, please contact the site '
+ 'administrator.'),
+ headers=cw_headers(request))
+ except cubicweb.web.Forbidden:
+ raise httpexceptions.HTTPForbidden(
+ request.cw_request._(
+ 'This action is forbidden. '
+ 'If you think it should be allowed, please contact the site '
+ 'administrator.'),
+ headers=cw_headers(request))
+ except (rql.BadRQLQuery, cubicweb.web.RequestError) as ex:
+ raise
+
+
+class CubicWebPyramidRequest(CubicWebRequestBase):
+ """ A CubicWeb request that only wraps a pyramid request.
+
+ :param request: A pyramid request
+
+ """
+ def __init__(self, request):
+ self._request = request
+
+ self.path = request.upath_info
+
+ vreg = request.registry['cubicweb.registry']
+ https = request.scheme == 'https'
+
+ post = request.params.mixed()
+ headers_in = request.headers
+
+ super(CubicWebPyramidRequest, self).__init__(vreg, https, post,
+ headers=headers_in)
+
+ self.content = request.body_file_seekable
+
+ def setup_params(self, params):
+ self.form = {}
+ for param, val in params.items():
+ if param in self.no_script_form_params and val:
+ val = self.no_script_form_param(param, val)
+ if isinstance(val, FieldStorage) and val.file:
+ val = (val.filename, val.file)
+ if param == '_cwmsgid':
+ self.set_message_id(val)
+ elif param == '__message':
+ warn('[3.13] __message in request parameter is deprecated '
+ '(may only be given to .build_url). Seeing this message '
+ 'usualy means your application hold some <form> where '
+ 'you should replace use of __message hidden input by '
+ 'form.set_message, so new _cwmsgid mechanism is properly '
+ 'used',
+ DeprecationWarning)
+ self.set_message(val)
+ else:
+ self.form[param] = val
+
+ def is_secure(self):
+ return self._request.scheme == 'https'
+
+ def relative_path(self, includeparams=True):
+ path = self._request.path[1:]
+ if includeparams and self._request.query_string:
+ return '%s?%s' % (path, self._request.query_string)
+ return path
+
+ def instance_uri(self):
+ return self._request.application_url
+
+ def get_full_path(self):
+ path = self._request.path
+ if self._request.query_string:
+ return '%s?%s' % (path, self._request.query_string)
+ return path
+
+ def http_method(self):
+ return self._request.method
+
+ def _set_status_out(self, value):
+ self._request.response.status_int = value
+
+ def _get_status_out(self):
+ return self._request.response.status_int
+
+ status_out = property(_get_status_out, _set_status_out)
+
+ @property
+ def message(self):
+ """Returns a '<br>' joined list of the cubicweb current message and the
+ default pyramid flash queue messages.
+ """
+ return u'\n<br>\n'.join(
+ self._request.session.pop_flash()
+ + self._request.session.pop_flash('cubicweb'))
+
+ def set_message(self, msg):
+ self.reset_message()
+ self._request.session.flash(msg, 'cubicweb')
+
+ def set_message_id(self, msgid):
+ self.reset_message()
+ self.set_message(
+ self._request.session.pop(msgid, u''))
+
+ def reset_message(self):
+ self._request.session.pop_flash('cubicweb')
+
+
+def render_view(request, vid, **kwargs):
+ """ Helper function to render a CubicWeb view.
+
+ :param request: A pyramid request
+ :param vid: A CubicWeb view id
+ :param **kwargs: Keyword arguments to select and instanciate the view
+ :returns: The rendered view content
+ """
+ vreg = request.registry['cubicweb.registry']
+ # XXX The select() function could, know how to handle a pyramid
+ # request, and feed it directly to the views that supports it.
+ # On the other hand, we could refine the View concept and decide it works
+ # with a cnx, and never with a WebRequest
+
+ with cw_to_pyramid(request):
+ view = vreg['views'].select(vid, request.cw_request, **kwargs)
+ view.set_stream()
+ view.render()
+ return view._stream.getvalue()
+
+
+def _cw_cnx(request):
+ """ Obtains a cw session from a pyramid request
+
+ The connection will be commited or rolled-back in a request finish
+ callback (this is temporary, we should make use of the transaction manager
+ in a later version).
+
+ Not meant for direct use, use ``request.cw_cnx`` instead.
+
+ :param request: A pyramid request
+ :returns type: :class:`cubicweb.server.session.Connection`
+ """
+ session = request.cw_session
+ if session is None:
+ return None
+
+ if CW_321:
+ cnx = session.new_cnx()
+
+ def commit_state(cnx):
+ return cnx.commit_state
+ else:
+ cnx = repoapi.ClientConnection(session)
+
+ def commit_state(cnx):
+ return cnx._cnx.commit_state
+
+ def cleanup(request):
+ try:
+ if (request.exception is not None and not isinstance(
+ request.exception, (
+ httpexceptions.HTTPSuccessful,
+ httpexceptions.HTTPRedirection))):
+ cnx.rollback()
+ elif commit_state(cnx) == 'uncommitable':
+ cnx.rollback()
+ else:
+ cnx.commit()
+ finally:
+ cnx.__exit__(None, None, None)
+
+ request.add_finished_callback(cleanup)
+ cnx.__enter__()
+ return cnx
+
+
+def repo_connect(request, repo, eid):
+ """A lightweight version of
+ :meth:`cubicweb.server.repository.Repository.connect` that does not keep
+ track of opened sessions, removing the need of closing them"""
+ user = tools.cached_build_user(repo, eid)
+ session = Session(request, user, repo)
+ tools.cnx_attach_entity(session, user)
+ # Calling the hooks should be done only once, disabling it completely for
+ # now
+ #with session.new_cnx() as cnx:
+ #repo.hm.call_hooks('session_open', cnx)
+ #cnx.commit()
+ # repo._sessions[session.sessionid] = session
+ return session
+
+
+def _cw_session(request):
+ """Obtains a cw session from a pyramid request
+
+ :param request: A pyramid request
+ :returns type: :class:`cubicweb.server.session.Session`
+
+ Not meant for direct use, use ``request.cw_session`` instead.
+ """
+ repo = request.registry['cubicweb.repository']
+
+ if not request.authenticated_userid:
+ eid = request.registry.get('cubicweb.anonymous_eid')
+ if eid is None:
+ return None
+ session = repo_connect(request, repo, eid=eid)
+ else:
+ session = request._cw_cached_session
+
+ return session
+
+
+def _cw_request(request):
+ """ Obtains a CubicWeb request wrapper for the pyramid request.
+
+ :param request: A pyramid request
+ :return: A CubicWeb request
+ :returns type: :class:`CubicWebPyramidRequest`
+
+ Not meant for direct use, use ``request.cw_request`` instead.
+
+ """
+ req = CubicWebPyramidRequest(request)
+ cnx = request.cw_cnx
+ if cnx is not None:
+ req.set_cnx(request.cw_cnx)
+ return req
+
+
+def get_principals(login, request):
+ """ Returns the group names of the authenticated user.
+
+ This function is meant to be used as an authentication policy callback.
+
+ It also pre-open the cubicweb session and put it in
+ request._cw_cached_session for later usage by :func:`_cw_session`.
+
+ .. note::
+
+ If the default authentication policy is not used, make sure this
+ function gets called by the active authentication policy.
+
+ :param login: A cubicweb user eid
+ :param request: A pyramid request
+ :returns: A list of group names
+ """
+ repo = request.registry['cubicweb.repository']
+
+ try:
+ session = repo_connect(request, repo, eid=login)
+ request._cw_cached_session = session
+ except:
+ log.exception("Failed")
+ raise
+
+ return session.user.groups
+
+
+def includeme(config):
+ """ Enables the core features of Pyramid CubicWeb.
+
+ Automatically called by the 'pyramid' command, or via
+ ``config.include('cubicweb.pyramid.code')``. In the later case,
+ the following registry entries must be defined first:
+
+ 'cubicweb.config'
+ A cubicweb 'config' instance.
+
+ 'cubicweb.repository'
+ The correponding cubicweb repository.
+
+ 'cubicweb.registry'
+ The vreg.
+ """
+ repo = config.registry['cubicweb.repository']
+
+ with repo.internal_cnx() as cnx:
+ login = config.registry['cubicweb.config'].anonymous_user()[0]
+ if login is not None:
+ config.registry['cubicweb.anonymous_eid'] = cnx.find(
+ 'CWUser', login=login).one().eid
+
+ config.add_request_method(
+ _cw_session, name='cw_session', property=True, reify=True)
+ config.add_request_method(
+ _cw_cnx, name='cw_cnx', property=True, reify=True)
+ config.add_request_method(
+ _cw_request, name='cw_request', property=True, reify=True)
+
+ cwcfg = config.registry['cubicweb.config']
+ for cube in cwcfg.cubes():
+ pkgname = 'cubes.' + cube
+ mod = __import__(pkgname)
+ mod = getattr(mod, cube)
+ if hasattr(mod, 'includeme'):
+ config.include('cubes.' + cube)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/defaults.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,24 @@
+""" Defaults for a classical CubicWeb instance. """
+
+
+def includeme(config):
+ """ Enable the defaults that make the application behave like a classical
+ CubicWeb instance.
+
+ The following modules get included:
+
+ - :func:`cubicweb.pyramid.session <cubicweb.pyramid.session.includeme>`
+ - :func:`cubicweb.pyramid.auth <cubicweb.pyramid.auth.includeme>`
+ - :func:`cubicweb.pyramid.login <cubicweb.pyramid.login.includeme>`
+
+ It is automatically included by the configuration system, unless the
+ following entry is added to the :ref:`pyramid_settings`:
+
+ .. code-block:: ini
+
+ cubicweb.defaults = no
+
+ """
+ config.include('cubicweb.pyramid.session')
+ config.include('cubicweb.pyramid.auth')
+ config.include('cubicweb.pyramid.login')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/init_instance.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,10 @@
+from cubicweb.cwconfig import CubicWebConfiguration
+
+
+def includeme(config):
+ appid = config.registry.settings['cubicweb.instance']
+ cwconfig = CubicWebConfiguration.config_for(appid)
+
+ config.registry['cubicweb.config'] = cwconfig
+ config.registry['cubicweb.repository'] = repo = cwconfig.repository()
+ config.registry['cubicweb.registry'] = repo.vreg
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/login.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,81 @@
+""" Provide login views that reproduce a classical CubicWeb behavior"""
+from pyramid import security
+from pyramid.httpexceptions import HTTPSeeOther
+from pyramid.view import view_config
+from pyramid.settings import asbool
+
+import cubicweb
+
+from cubicweb.pyramid.core import render_view
+
+
+@view_config(route_name='login')
+def login_form(request):
+ """ Default view for the 'login' route.
+
+ Display the 'login' CubicWeb view, which is should be a login form"""
+ request.response.text = render_view(request, 'login')
+ return request.response
+
+
+@view_config(route_name='login', request_param=('__login', '__password'))
+def login_password_login(request):
+ """ Handle GET/POST of __login/__password on the 'login' route.
+
+ The authentication itself is delegated to the CubicWeb repository.
+
+ Request parameters:
+
+ :param __login: The user login (or email if :confval:`allow-email-login` is
+ on.
+ :param __password: The user password
+ :param __setauthcookie: (optional) If defined and equal to '1', set the
+ authentication cookie maxage to 1 week.
+
+ If not, the authentication cookie is a session
+ cookie.
+ """
+ repo = request.registry['cubicweb.repository']
+
+ user_eid = None
+
+ login = request.params['__login']
+ password = request.params['__password']
+
+ try:
+ with repo.internal_cnx() as cnx:
+ user = repo.authenticate_user(cnx, login, password=password)
+ user_eid = user.eid
+ except cubicweb.AuthenticationError:
+ request.cw_request.set_message(request.cw_request._(
+ "Authentication failed. Please check your credentials."))
+ request.cw_request.post = dict(request.params)
+ del request.cw_request.post['__password']
+ request.response.status_code = 403
+ return login_form(request)
+
+ headers = security.remember(
+ request, user_eid,
+ persistent=asbool(request.params.get('__setauthcookie', False)))
+
+ new_path = request.params.get('postlogin_path', '')
+
+ if new_path == 'login':
+ new_path = ''
+
+ url = request.cw_request.build_url(new_path)
+ raise HTTPSeeOther(url, headers=headers)
+
+
+@view_config(route_name='login', effective_principals=security.Authenticated)
+def login_already_loggedin(request):
+ """ 'login' route view for Authenticated users.
+
+ Simply redirect the user to '/'."""
+ raise HTTPSeeOther('/')
+
+
+def includeme(config):
+ """ Create the 'login' route ('/login') and load this module views"""
+ config.add_route('login', '/login')
+ config.scan('cubicweb.pyramid.login')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/predicates.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,22 @@
+"""Contains predicates used in Pyramid views.
+"""
+
+
+class MatchIsETypePredicate(object):
+ """A predicate that match if a given etype exist in schema.
+ """
+ def __init__(self, matchname, config):
+ self.matchname = matchname
+
+ def text(self):
+ return 'match_is_etype = %s' % self.matchname
+
+ phash = text
+
+ def __call__(self, info, request):
+ return info['match'][self.matchname].lower() in \
+ request.registry['cubicweb.registry'].case_insensitive_etypes
+
+
+def includeme(config):
+ config.add_route_predicate('match_is_etype', MatchIsETypePredicate)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/profile.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,63 @@
+""" Tools for profiling.
+
+See :ref:`profiling`."""
+from __future__ import print_function
+
+import cProfile
+import itertools
+
+from pyramid.view import view_config
+
+
+@view_config(route_name='profile_ping')
+def ping(request):
+ """ View that handle '/_profile/ping'
+
+ It simply reply 'ping', without requiring connection to the repository.
+ It is a useful as a comparison point to evaluate the actual overhead of
+ more costly views.
+ """
+ request.response.text = u'pong'
+ return request.response
+
+
+@view_config(route_name='profile_cnx')
+def cnx(request):
+ """ View that handle '/_profile/cnx'
+
+ Same as :func:`ping`, but it first ask for a connection to the repository.
+ Useful to evaluate the overhead of opening a connection.
+ """
+ request.cw_cnx
+ request.response.text = u'pong'
+ return request.response
+
+
+def wsgi_profile(app, filename='program.prof', dump_every=50):
+ """ A WSGI middleware for profiling
+
+ It enable the profiler before passing the request to the underlying
+ application, and disable it just after.
+
+ The stats will be dumped after ``dump_every`` requests
+
+ :param filename: The filename to dump the stats to.
+ :param dump_every: Number of requests after which to dump the stats.
+ """
+
+ profile = cProfile.Profile()
+
+ counter = itertools.count(1)
+
+ def application(environ, start_response):
+ profile.enable()
+ try:
+ return app(environ, start_response)
+ finally:
+ profile.disable()
+ if not counter.next() % dump_every:
+ print("Dump profile stats to %s" % filename)
+ profile.create_stats()
+ profile.dump_stats(filename)
+
+ return application
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/resources.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,74 @@
+"""Contains resources classes.
+"""
+from six import text_type
+
+from rql import TypeResolverException
+
+from pyramid.decorator import reify
+from pyramid.httpexceptions import HTTPNotFound
+
+
+class EntityResource(object):
+
+ """A resource class for an entity. It provide method to retrieve an entity
+ by eid.
+ """
+
+ @classmethod
+ def from_eid(cls):
+ def factory(request):
+ return cls(request, None, None, request.matchdict['eid'])
+ return factory
+
+ def __init__(self, request, cls, attrname, value):
+ self.request = request
+ self.cls = cls
+ self.attrname = attrname
+ self.value = value
+
+ @reify
+ def rset(self):
+ req = self.request.cw_request
+ if self.cls is None:
+ return req.execute('Any X WHERE X eid %(x)s',
+ {'x': int(self.value)})
+ st = self.cls.fetch_rqlst(self.request.cw_cnx.user, ordermethod=None)
+ st.add_constant_restriction(st.get_variable('X'), self.attrname,
+ 'x', 'Substitute')
+ if self.attrname == 'eid':
+ try:
+ rset = req.execute(st.as_string(), {'x': int(self.value)})
+ except (ValueError, TypeResolverException):
+ # conflicting eid/type
+ raise HTTPNotFound()
+ else:
+ rset = req.execute(st.as_string(), {'x': text_type(self.value)})
+ return rset
+
+
+class ETypeResource(object):
+
+ """A resource for etype.
+ """
+ @classmethod
+ def from_match(cls, matchname):
+ def factory(request):
+ return cls(request, request.matchdict[matchname])
+ return factory
+
+ def __init__(self, request, etype):
+ vreg = request.registry['cubicweb.registry']
+
+ self.request = request
+ self.etype = vreg.case_insensitive_etypes[etype.lower()]
+ self.cls = vreg['etypes'].etype_class(self.etype)
+
+ def __getitem__(self, value):
+ attrname = self.cls.cw_rest_attr_info()[0]
+ return EntityResource(self.request, self.cls, attrname, value)
+
+ @reify
+ def rset(self):
+ rql = self.cls.fetch_rql(self.request.cw_cnx.user)
+ rset = self.request.cw_request.execute(rql)
+ return rset
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/rest_api.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,24 @@
+from __future__ import absolute_import
+
+
+from pyramid.httpexceptions import HTTPNotFound
+from pyramid.view import view_config
+from cubicweb.pyramid.resources import EntityResource, ETypeResource
+from cubicweb.pyramid.predicates import MatchIsETypePredicate
+
+
+@view_config(
+ route_name='cwentities',
+ context=EntityResource,
+ request_method='DELETE')
+def delete_entity(context, request):
+ context.rset.one().cw_delete()
+ request.response.status_int = 204
+ return request.response
+
+
+def includeme(config):
+ config.add_route(
+ 'cwentities', '/{etype}/*traverse',
+ factory=ETypeResource.from_match('etype'), match_is_etype='etype')
+ config.scan(__name__)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/session.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,189 @@
+import warnings
+import logging
+from contextlib import contextmanager
+
+from pyramid.compat import pickle
+from pyramid.session import SignedCookieSessionFactory
+
+from cubicweb import Binary
+
+
+log = logging.getLogger(__name__)
+
+
+def logerrors(logger):
+ def wrap(fn):
+ def newfn(*args, **kw):
+ try:
+ return fn(*args, **kw)
+ except:
+ logger.exception("Error in %s" % fn.__name__)
+ return newfn
+ return wrap
+
+
+@contextmanager
+def unsafe_cnx_context_manager(request):
+ """Return a connection for use as a context manager, with security disabled
+
+ If request has an attached connection, its security will be deactived in the context manager's
+ scope, else a new internal connection is returned.
+ """
+ cnx = request.cw_cnx
+ if cnx is None:
+ with request.registry['cubicweb.repository'].internal_cnx() as cnx:
+ yield cnx
+ else:
+ with cnx.security_enabled(read=False, write=False):
+ yield cnx
+
+
+def CWSessionFactory(
+ secret,
+ cookie_name='session',
+ max_age=None,
+ path='/',
+ domain=None,
+ secure=False,
+ httponly=True,
+ set_on_exception=True,
+ timeout=1200,
+ reissue_time=120,
+ hashalg='sha512',
+ salt='pyramid.session.',
+ serializer=None):
+ """ A pyramid session factory that store session data in the CubicWeb
+ database.
+
+ Storage is done with the 'CWSession' entity, which is provided by the
+ 'pyramid' cube.
+
+ .. warning::
+
+ Although it provides a sane default behavior, this session storage has
+ a serious overhead because it uses RQL to access the database.
+
+ Using pure SQL would improve a bit (it is roughly twice faster), but it
+ is still pretty slow and thus not an immediate priority.
+
+ It is recommended to use faster session factory
+ (pyramid_redis_sessions_ for example) if you need speed.
+
+ .. _pyramid_redis_sessions: http://pyramid-redis-sessions.readthedocs.org/
+ en/latest/index.html
+ """
+
+ SignedCookieSession = SignedCookieSessionFactory(
+ secret,
+ cookie_name=cookie_name,
+ max_age=max_age,
+ path=path,
+ domain=domain,
+ secure=secure,
+ httponly=httponly,
+ set_on_exception=set_on_exception,
+ timeout=timeout,
+ reissue_time=reissue_time,
+ hashalg=hashalg,
+ salt=salt,
+ serializer=serializer)
+
+ class CWSession(SignedCookieSession):
+ def __init__(self, request):
+ # _set_accessed will be called by the super __init__.
+ # Setting _loaded to True inhibates it.
+ self._loaded = True
+
+ # the super __init__ will load a single value in the dictionnary,
+ # the session id.
+ super(CWSession, self).__init__(request)
+
+ # Remove the session id from the dict
+ self.sessioneid = self.pop('sessioneid', None)
+ self.repo = request.registry['cubicweb.repository']
+
+ # We need to lazy-load only for existing sessions
+ self._loaded = self.sessioneid is None
+
+ @logerrors(log)
+ def _set_accessed(self, value):
+ self._accessed = value
+
+ if self._loaded:
+ return
+
+ with unsafe_cnx_context_manager(self.request) as cnx:
+ value_rset = cnx.execute('Any D WHERE X eid %(x)s, X cwsessiondata D',
+ {'x': self.sessioneid})
+ value = value_rset[0][0]
+ if value:
+ # Use directly dict.update to avoir _set_accessed to be
+ # recursively called
+ dict.update(self, pickle.load(value))
+
+ self._loaded = True
+
+ def _get_accessed(self):
+ return self._accessed
+
+ accessed = property(_get_accessed, _set_accessed)
+
+ @logerrors(log)
+ def _set_cookie(self, response):
+ # Save the value in the database
+ data = Binary(pickle.dumps(dict(self)))
+ sessioneid = self.sessioneid
+
+ with unsafe_cnx_context_manager(self.request) as cnx:
+ if not sessioneid:
+ session = cnx.create_entity(
+ 'CWSession', cwsessiondata=data)
+ sessioneid = session.eid
+ else:
+ session = cnx.entity_from_eid(sessioneid)
+ session.cw_set(cwsessiondata=data)
+ cnx.commit()
+
+ # Only if needed actually set the cookie
+ if self.new or self.accessed - self.renewed > self._reissue_time:
+ dict.clear(self)
+ dict.__setitem__(self, 'sessioneid', sessioneid)
+ return super(CWSession, self)._set_cookie(response)
+
+ return True
+
+ return CWSession
+
+
+def includeme(config):
+ """ Activate the CubicWeb session factory.
+
+ Usually called via ``config.include('cubicweb.pyramid.auth')``.
+
+ See also :ref:`defaults_module`
+ """
+ settings = config.registry.settings
+ secret = settings.get('cubicweb.session.secret', '')
+ if not secret:
+ secret = config.registry['cubicweb.config'].get('pyramid-session-secret')
+ warnings.warn('''
+ Please migrate pyramid-session-secret from
+ all-in-one.conf to cubicweb.session.secret config entry in
+ your pyramid.ini file.
+ ''')
+ if not secret:
+ secret = 'notsosecret'
+ warnings.warn('''
+
+ !! WARNING !! !! WARNING !!
+
+ The session cookies are signed with a static secret key.
+ To put your own secret key, edit your pyramid.ini file
+ and set the 'cubicweb.session.secret' key.
+
+ YOU SHOULD STOP THIS INSTANCE unless your really know what you
+ are doing !!
+
+ ''')
+ session_factory = CWSessionFactory(secret)
+ config.set_session_factory(session_factory)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/__init__.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,30 @@
+import webtest
+
+from cubicweb.devtools.webtest import CubicWebTestTC
+
+from cubicweb.pyramid import make_cubicweb_application
+
+
+class PyramidCWTest(CubicWebTestTC):
+ settings = {}
+
+ @classmethod
+ def init_config(cls, config):
+ super(PyramidCWTest, cls).init_config(config)
+ config.global_set_option('https-url', 'https://localhost.local/')
+ config.global_set_option('anonymous-user', 'anon')
+ config.https_uiprops = None
+ config.https_datadir_url = None
+
+ def setUp(self):
+ # Skip CubicWebTestTC setUp
+ super(CubicWebTestTC, self).setUp()
+ config = make_cubicweb_application(self.config, self.settings)
+ self.includeme(config)
+ self.pyr_registry = config.registry
+ self.webapp = webtest.TestApp(
+ config.make_wsgi_app(),
+ extra_environ={'wsgi.url_scheme': 'https'})
+
+ def includeme(self, config):
+ pass
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/data/bootstrap_cubes Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,1 @@
+pyramid
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_bw_request.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+from io import BytesIO
+
+import webtest
+
+import pyramid.request
+
+from cubicweb.pyramid.core import CubicWebPyramidRequest
+from cubicweb.pyramid.test import PyramidCWTest
+
+
+class WSGIAppTest(PyramidCWTest):
+ def make_request(self, path, environ=None, **kw):
+ r = webtest.app.TestRequest.blank(path, environ, **kw)
+
+ request = pyramid.request.Request(r.environ)
+ request.registry = self.pyr_registry
+
+ return request
+
+ def test_content_type(self):
+ req = CubicWebPyramidRequest(
+ self.make_request('/', {'CONTENT_TYPE': 'text/plain'}))
+
+ self.assertEqual('text/plain', req.get_header('Content-Type'))
+
+ def test_content_body(self):
+ req = CubicWebPyramidRequest(
+ self.make_request('/', {
+ 'CONTENT_LENGTH': 12,
+ 'CONTENT_TYPE': 'text/plain',
+ 'wsgi.input': BytesIO(b'some content')}))
+
+ self.assertEqual(b'some content', req.content.read())
+
+ def test_http_scheme(self):
+ req = CubicWebPyramidRequest(
+ self.make_request('/', {
+ 'wsgi.url_scheme': 'http'}))
+
+ self.assertFalse(req.https)
+
+ def test_https_scheme(self):
+ req = CubicWebPyramidRequest(
+ self.make_request('/', {
+ 'wsgi.url_scheme': 'https'}))
+
+ self.assertTrue(req.https)
+
+ def test_https_prefix(self):
+ r = self.webapp.get('/https/')
+ self.assertIn('https://', r.text)
+
+ def test_big_content(self):
+ content = b'x'*100001
+
+ req = CubicWebPyramidRequest(
+ self.make_request('/', {
+ 'CONTENT_LENGTH': len(content),
+ 'CONTENT_TYPE': 'text/plain',
+ 'wsgi.input': BytesIO(content)}))
+
+ self.assertEqual(content, req.content.read())
+
+ def test_post(self):
+ self.webapp.post(
+ '/',
+ params={'__login': self.admlogin, '__password': self.admpassword})
+
+ def test_get_multiple_variables(self):
+ req = CubicWebPyramidRequest(
+ self.make_request('/?arg=1&arg=2'))
+
+ self.assertEqual([u'1', u'2'], req.form['arg'])
+
+ def test_post_multiple_variables(self):
+ req = CubicWebPyramidRequest(
+ self.make_request('/', POST='arg=1&arg=2'))
+
+ self.assertEqual([u'1', u'2'], req.form['arg'])
+
+ def test_post_files(self):
+ content_type, params = self.webapp.encode_multipart(
+ (), (('filefield', 'aname', b'acontent'),))
+ req = CubicWebPyramidRequest(
+ self.make_request('/', POST=params, content_type=content_type))
+ self.assertIn('filefield', req.form)
+ fieldvalue = req.form['filefield']
+ self.assertEqual(u'aname', fieldvalue[0])
+ self.assertEqual(b'acontent', fieldvalue[1].read())
+
+ def test_post_unicode_urlencoded(self):
+ params = 'arg=%C3%A9'
+ req = CubicWebPyramidRequest(
+ self.make_request(
+ '/', POST=params,
+ content_type='application/x-www-form-urlencoded'))
+ self.assertEqual(u"é", req.form['arg'])
+
+
+if __name__ == '__main__':
+ from unittest import main
+ main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_core.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,49 @@
+from cubicweb.pyramid.test import PyramidCWTest
+
+from cubicweb.view import View
+from cubicweb.web import Redirect
+from cubicweb import ValidationError
+
+
+class Redirector(View):
+ __regid__ = 'redirector'
+
+ def call(self, rset=None):
+ self._cw.set_header('Cache-Control', 'no-cache')
+ raise Redirect('http://example.org')
+
+
+def put_in_uncommitable_state(request):
+ try:
+ request.cw_cnx.execute('SET U login NULL WHERE U login "anon"')
+ except ValidationError:
+ pass
+ request.response.body = b'OK'
+ return request.response
+
+
+class CoreTest(PyramidCWTest):
+ anonymous_allowed = True
+
+ def includeme(self, config):
+ config.add_route('uncommitable', '/uncommitable')
+ config.add_view(put_in_uncommitable_state, route_name='uncommitable')
+
+ def test_cw_to_pyramid_copy_headers_on_redirect(self):
+ self.vreg.register(Redirector)
+ try:
+ res = self.webapp.get('/?vid=redirector', expect_errors=True)
+ self.assertEqual(res.status_int, 303)
+ self.assertEqual(res.headers['Cache-Control'], 'no-cache')
+ finally:
+ self.vreg.unregister(Redirector)
+
+ def test_uncommitable_cnx(self):
+ res = self.webapp.get('/uncommitable')
+ self.assertEqual(res.text, 'OK')
+ self.assertEqual(res.status_int, 200)
+
+
+if __name__ == '__main__':
+ from unittest import main
+ main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_login.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,51 @@
+from cubicweb.pyramid.test import PyramidCWTest
+
+
+class LoginTest(PyramidCWTest):
+ def test_login_form(self):
+ res = self.webapp.get('/login')
+ self.assertIn('__login', res.text)
+
+ def test_login_password_login(self):
+ res = self.webapp.post('/login', {
+ '__login': self.admlogin, '__password': self.admpassword})
+ self.assertEqual(res.status_int, 303)
+
+ res = self.webapp.get('/login')
+ self.assertEqual(res.status_int, 303)
+
+ def test_login_password_login_cookie_expires(self):
+ res = self.webapp.post('/login', {
+ '__login': self.admlogin, '__password': self.admpassword})
+ self.assertEqual(res.status_int, 303)
+
+ cookies = self.webapp.cookiejar._cookies['localhost.local']['/']
+ self.assertNotIn('pauth_tkt', cookies)
+ self.assertIn('auth_tkt', cookies)
+ self.assertIsNone(cookies['auth_tkt'].expires)
+
+ res = self.webapp.get('/logout')
+ self.assertEqual(res.status_int, 303)
+
+ self.assertNotIn('auth_tkt', cookies)
+ self.assertNotIn('pauth_tkt', cookies)
+
+ res = self.webapp.post('/login', {
+ '__login': self.admlogin, '__password': self.admpassword,
+ '__setauthcookie': 1})
+ self.assertEqual(res.status_int, 303)
+
+ cookies = self.webapp.cookiejar._cookies['localhost.local']['/']
+ self.assertNotIn('auth_tkt', cookies)
+ self.assertIn('pauth_tkt', cookies)
+ self.assertIsNotNone(cookies['pauth_tkt'].expires)
+
+ def test_login_bad_password(self):
+ res = self.webapp.post('/login', {
+ '__login': self.admlogin, '__password': 'empty'}, status=403)
+ self.assertIn('Authentication failed', res.text)
+
+
+if __name__ == '__main__':
+ from unittest import main
+ main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_rest_api.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,59 @@
+from __future__ import absolute_import
+
+from cubicweb.pyramid.rest_api import EntityResource
+from cubicweb.pyramid.core import CubicWebPyramidRequest
+from pyramid.view import view_config
+
+from cubicweb.pyramid.test import PyramidCWTest
+
+
+class RestApiTest(PyramidCWTest):
+ def includeme(self, config):
+ config.include('cubicweb.pyramid.rest_api')
+ config.include('cubicweb.pyramid.test.test_rest_api')
+
+ def test_delete(self):
+ with self.admin_access.repo_cnx() as cnx:
+ cnx.create_entity('CWGroup', name=u'tmp')
+ cnx.commit()
+
+ self.login()
+ res = self.webapp.delete('/cwgroup/tmp')
+ self.assertEqual(res.status_int, 204)
+
+ with self.admin_access.repo_cnx() as cnx:
+ self.assertEqual(cnx.find('CWGroup', name=u'tmp').rowcount, 0)
+
+ def test_rql_execute(self):
+ with self.admin_access.repo_cnx() as cnx:
+ cnx.create_entity('CWGroup', name=u'tmp')
+ cnx.commit()
+ self.login()
+ params = {'test_rql_execute': 'test'}
+ self.webapp.get('/cwgroup/tmp', params=params)
+
+
+@view_config(
+ route_name='cwentities',
+ context=EntityResource,
+ request_method='GET',
+ request_param=('test_rql_execute',)
+)
+def rql_execute_view(context, request):
+ """Return 500 response if rset.req is not a CubicWeb request.
+ """
+ if isinstance(context.rset.req, CubicWebPyramidRequest):
+ request.response.status_int = 204
+ else:
+ request.response.status_int = 500
+ request.response.text = 'rset.req is not a CubicWeb request'
+ return request.response
+
+
+def includeme(config):
+ config.scan(__name__)
+
+
+if __name__ == '__main__':
+ from unittest import main
+ main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_tools.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,31 @@
+from cubicweb.pyramid.test import PyramidCWTest
+from cubicweb.pyramid import tools
+
+
+class ToolsTest(PyramidCWTest):
+ anonymous_allowed = True
+
+ def test_clone_user(self):
+ with self.admin_access.repo_cnx() as cnx:
+ user = cnx.find('CWUser', login='anon').one()
+ user.login # fill the cache
+ clone = tools.clone_user(self.repo, user)
+
+ self.assertEqual(clone.eid, user.eid)
+ self.assertEqual(clone.login, user.login)
+
+ self.assertEqual(clone.cw_rset.rows, user.cw_rset.rows)
+ self.assertEqual(clone.cw_rset.rql, user.cw_rset.rql)
+
+ def test_cnx_attach_entity(self):
+ with self.admin_access.repo_cnx() as cnx:
+ user = cnx.find('CWUser', login='anon').one()
+
+ with self.admin_access.repo_cnx() as cnx:
+ tools.cnx_attach_entity(cnx, user)
+ self.assertEqual(user.login, 'anon')
+
+
+if __name__ == '__main__':
+ from unittest import main
+ main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/tools.py Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,79 @@
+"""Various tools.
+
+.. warning::
+
+ This module should be considered as internal implementation details. Use
+ with caution, as the API may change without notice.
+"""
+
+#: A short-term cache for user clones.
+#: used by cached_build_user to speed-up repetitive calls to build_user
+#: The expiration is handled in a dumb and brutal way: the whole cache is
+#: cleared every 5 minutes.
+_user_cache = {}
+
+
+def clone_user(repo, user):
+ """Clone a CWUser instance.
+
+ .. warning::
+
+ The returned clone is detached from any cnx.
+ Before using it in any way, it should be attached to a cnx that has not
+ this user already loaded.
+ """
+ CWUser = repo.vreg['etypes'].etype_class('CWUser')
+ clone = CWUser(
+ None,
+ rset=user.cw_rset.copy(),
+ row=user.cw_row,
+ col=user.cw_col,
+ groups=set(user._groups) if hasattr(user, '_groups') else None,
+ properties=dict(user._properties)
+ if hasattr(user, '_properties') else None)
+ clone.cw_attr_cache = dict(user.cw_attr_cache)
+ return clone
+
+
+def cnx_attach_entity(cnx, entity):
+ """Attach an entity to a cnx."""
+ entity._cw = cnx
+ if entity.cw_rset:
+ entity.cw_rset.req = cnx
+
+
+def cached_build_user(repo, eid):
+ """Cached version of
+ :meth:`cubicweb.server.repository.Repository._build_user`
+ """
+ with repo.internal_cnx() as cnx:
+ if eid in _user_cache:
+ entity = clone_user(repo, _user_cache[eid])
+ # XXX the cnx is needed here so that the CWUser instance has an
+ # access to the vreg, which it needs when its 'prefered_language'
+ # property is accessed.
+ # If this property did not need a cnx to access a vreg, we could
+ # avoid the internal_cnx() and save more time.
+ cnx_attach_entity(cnx, entity)
+ return entity
+
+ user = repo._build_user(cnx, eid)
+ user.cw_clear_relation_cache()
+ _user_cache[eid] = clone_user(repo, user)
+ return user
+
+
+def clear_cache():
+ """Clear the user cache"""
+ _user_cache.clear()
+
+
+def includeme(config):
+ """Start the cache maintenance loop task.
+
+ Automatically included by :func:`cubicweb.pyramid.make_cubicweb_application`.
+ """
+ repo = config.registry['cubicweb.repository']
+ interval = int(config.registry.settings.get(
+ 'cubicweb.usercache.expiration_time', 60*5))
+ repo.looping_task(interval, clear_cache)
--- a/cubicweb/test/unittest_cwconfig.py Fri Sep 23 16:04:32 2016 +0200
+++ b/cubicweb/test/unittest_cwconfig.py Mon Sep 26 14:52:12 2016 +0200
@@ -81,7 +81,7 @@
expected_cubes = [
'card', 'comment', 'cubicweb_comment', 'cubicweb_email', 'file',
'cubicweb_file', 'cubicweb_forge', 'localperms',
- 'cubicweb_mycube', 'tag',
+ 'cubicweb_mycube', 'pyramid', 'tag',
]
self._test_available_cubes(expected_cubes)
mock_iter_entry_points.assert_called_once_with(
@@ -168,7 +168,7 @@
# local cubes
'comment', 'email', 'file', 'forge', 'mycube',
# test dependencies
- 'card', 'file', 'localperms', 'tag',
+ 'card', 'file', 'localperms', 'pyramid', 'tag',
]))
self._test_available_cubes(expected_cubes)
--- a/debian/control Fri Sep 23 16:04:32 2016 +0200
+++ b/debian/control Mon Sep 26 14:52:12 2016 +0200
@@ -18,6 +18,9 @@
python-rql (>= 0.34.0),
python-yams (>= 0.44.0),
python-lxml,
+ python-setuptools,
+ python-pyramid,
+ python-waitress,
Standards-Version: 3.9.1
Homepage: https://www.cubicweb.org
X-Python-Version: >= 2.6
@@ -120,6 +123,26 @@
This package provides only the twisted server part of the library.
+Package: cubicweb-pyramid
+Architecture: all
+Depends:
+ ${misc:Depends},
+ ${python:Depends},
+ cubicweb-web (= ${source:Version}),
+ cubicweb-ctl (= ${source:Version}),
+ python-pyramid (>= 1.5.0),
+ python-pyramid-multiauth,
+ python-waitress (>= 0.8.9),
+ python-wsgicors,
+Recommends:
+ python-pyramid-debugtoolbar
+Description: Integrate CubicWeb with a Pyramid application
+ Provides pyramid extensions to load a CubicWeb instance and serve it through
+ the pyramid stack.
+ .
+ It prefigures what CubicWeb 4.0 will be.
+
+
Package: cubicweb-web
Architecture: all
Depends:
--- a/debian/copyright Fri Sep 23 16:04:32 2016 +0200
+++ b/debian/copyright Mon Sep 26 14:52:12 2016 +0200
@@ -5,11 +5,13 @@
Upstream Author:
Logilab <contact@logilab.fr>
+ Christophe de Vienne
Copyright:
Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ Copyright (c) 2014 Unlish
License:
@@ -43,4 +45,3 @@
The rights to each pictogram in the social extension are either
trademarked or copyrighted by the respective company.
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/pydist-overrides Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,1 @@
+cubicweb cubicweb-common
--- a/debian/rules Fri Sep 23 16:04:32 2016 +0200
+++ b/debian/rules Mon Sep 26 14:52:12 2016 +0200
@@ -55,6 +55,7 @@
rm -rf debian/cubicweb-twisted/usr/lib/python2*/*-packages/cubicweb/etwist/test
rm -rf debian/cubicweb-common/usr/lib/python2*/*-packages/cubicweb/ext/test
rm -rf debian/cubicweb-common/usr/lib/python2*/*-packages/cubicweb/entities/test
+ rm -rf debian/cubicweb-pyramid/usr/lib/python2*/*-packages/cubicweb/pyramid/tests
# Build architecture-independent files here.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,17 @@
+:mod:`cubicweb.pyramid`
+=======================
+
+.. automodule:: cubicweb.pyramid
+
+ .. autofunction:: make_cubicweb_application
+
+ .. autofunction:: wsgi_application_from_cwconfig
+
+ .. autofunction:: wsgi_application
+
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ pyramid/*
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/auth.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,12 @@
+.. _auth_module:
+
+:mod:`cubicweb.pyramid.auth`
+----------------------------
+
+.. automodule:: cubicweb.pyramid.auth
+
+ .. autofunction:: includeme
+
+ .. autoclass:: UpdateLoginTimeAuthenticationPolicy
+ :show-inheritance:
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/authplugin.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,10 @@
+.. _authplugin_module:
+
+:mod:`cubicweb.pyramid.authplugin`
+----------------------------------
+
+.. automodule:: cubicweb.pyramid.authplugin
+
+ .. autoclass:: DirectAuthentifier
+ :show-inheritance:
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/bwcompat.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,19 @@
+.. _bwcompat_module:
+
+:mod:`cubicweb.pyramid.bwcompat`
+--------------------------------
+
+.. automodule:: cubicweb.pyramid.bwcompat
+
+ .. autofunction:: includeme
+
+ .. autoclass:: PyramidSessionHandler
+ :members:
+
+ .. autoclass:: CubicWebPyramidHandler
+ :members:
+
+ .. automethod:: __call__
+
+ .. autoclass:: TweenHandler
+ :members:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/core.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,23 @@
+.. _core_module:
+
+:mod:`cubicweb.pyramid.core`
+----------------------------
+
+.. automodule:: cubicweb.pyramid.core
+
+ .. autofunction:: includeme
+
+ .. autofunction:: cw_to_pyramid
+
+ .. autofunction:: render_view
+
+ .. autofunction:: repo_connect
+ .. autofunction:: get_principals
+
+ .. autoclass:: CubicWebPyramidRequest
+ :show-inheritance:
+ :members:
+
+ .. autofunction:: _cw_session
+ .. autofunction:: _cw_cnx
+ .. autofunction:: _cw_request
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/defaults.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,8 @@
+.. _defaults_module:
+
+:mod:`cubicweb.pyramid.defaults`
+--------------------------------
+
+.. automodule:: cubicweb.pyramid.defaults
+
+ .. autofunction:: includeme
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/login.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,16 @@
+.. _login_module:
+
+:mod:`cubicweb.pyramid.login`
+-----------------------------
+
+.. automodule:: cubicweb.pyramid.login
+
+ .. autofunction:: includeme
+
+
+ Views
+ -----
+
+ .. autofunction:: login_form
+ .. autofunction:: login_password_login
+ .. autofunction:: login_already_loggedin
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/profile.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,19 @@
+.. _profile_module:
+
+:mod:`cubicweb.pyramid.profile`
+===============================
+
+.. automodule:: cubicweb.pyramid.profile
+
+ Views
+ -----
+
+ .. autofunction:: ping
+
+ .. autofunction:: cnx
+
+ WSGI
+ ----
+
+ .. autofunction:: wsgi_profile
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/session.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,10 @@
+.. _session_module:
+
+:mod:`cubicweb.pyramid.session`
+-------------------------------
+
+.. automodule:: cubicweb.pyramid.session
+
+ .. autofunction:: includeme
+
+ .. autofunction:: CWSessionFactory
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/api/pyramid/tools.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,13 @@
+.. _tools_module:
+
+:mod:`cubicweb.pyramid.tools`
+----------------------------
+
+.. automodule:: cubicweb.pyramid.tools
+
+ .. autofunction:: includeme
+
+ .. autofunction:: clone_user
+ .. autofunction:: cnx_attach_entity
+ .. autofunction:: cached_build_user
+ .. autofunction:: clear_cache
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/auth.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,40 @@
+Authentication
+==============
+
+Overview
+--------
+
+A default authentication stack is provided by the :mod:`cubicweb.pyramid.auth`
+module, which is included by :mod:`cubicweb.pyramid.default`.
+
+The authentication stack is built around `pyramid_multiauth`_, and provides a
+few default policies that reproduce the default cubicweb behavior.
+
+.. note::
+
+ Note that this module only provides an authentication policy, not the views
+ that handle the login form. See :ref:`login_module`
+
+Customize
+---------
+
+The default policies can be individually deactivated, as well as the default
+authentication callback that returns the current user groups as :term:`principals`.
+
+The following settings can be set to `False`:
+
+- :confval:`cubicweb.auth.update_login_time`. Activate the policy that update
+ the user `login_time` when `remember` is called.
+- :confval:`cubicweb.auth.authtkt` and all its subvalues.
+- :confval:`cubicweb.auth.groups_principals`
+
+Additionnal policies can be added by accessing the MultiAuthenticationPolicy
+instance in the registry:
+
+.. code-block:: python
+
+ mypolicy = SomePolicy()
+ authpolicy = config.registry['cubicweb.authpolicy']
+ authpolicy._policies.append(mypolicy)
+
+.. _pyramid_multiauth: https://github.com/mozilla-services/pyramid_multiauth
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/ctl.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,63 @@
+.. _cubicweb-ctl_pyramid:
+
+The 'pyramid' command
+=====================
+
+.. program:: cubicweb-ctl pyramid
+
+The 'pyramid' command is a replacement for the 'start' command of :ref:`cubicweb-ctl`.
+It provides the same options and a few other ones.
+
+.. note::
+
+ The 'pyramid' command is provided by the ``pyramid`` cube.
+
+Options
+-------
+
+
+.. option:: --no-daemon
+
+ Run the server in the foreground.
+
+.. option:: --debug-mode
+
+ Activate the repository debug mode (logs in the console and the debug
+ toolbar). Implies :option:`--no-daemon`.
+
+ Also force the following pyramid options:
+
+ .. code-block:: ini
+
+ pyramid.debug_authorization = yes
+ pyramid.debug_notfound = yes
+ pyramid.debug_routematch = yes
+ pyramid.reload_templates = yes
+
+.. option:: -D, --debug
+
+ Equals to :option:`--debug-mode` :option:`--no-daemon` :option:`--reload`
+
+.. option:: --reload
+
+ Restart the server if any source file is changed
+
+.. option:: --reload-interval=RELOAD_INTERVAL
+
+ Interval, in seconds, between file modifications checks [current: 1]
+
+.. option:: -l <log level>, --loglevel=<log level>
+
+ Set the loglevel. debug if -D is set, error otherwise
+
+.. option:: -p, --profile
+
+ Enable profiling. See :ref:`profiling`.
+
+.. option:: --profile-output=PROFILE_OUTPUT
+
+ Profiling output file (default: "program.prof")
+
+.. option:: --profile-dump-every=N
+
+ Dump profile stats to ouput every N requests (default: 100)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/index.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,36 @@
+================
+Pyramid Cubicweb
+================
+
+Pyramid Cubicweb is an attempt to rebase the CubicWeb framework on pyramid.
+
+It can be used in two different ways:
+
+- Within CubicWeb, through the 'pyramid' cube and the
+ :ref:`pyramid command <cubicweb-ctl_pyramid>`.
+ In this mode, the Pyramid CubicWeb replaces some parts of
+ CubicWeb and make the pyramid api available to the cubes.
+
+- Within a pyramid application, it provides easy access to a CubicWeb
+ instance and registry.
+
+Narrative Documentation
+=======================
+
+.. toctree::
+ :maxdepth: 2
+
+ quickstart
+ ctl
+ settings
+ auth
+ profiling
+
+Api Documentation
+=================
+
+.. toctree::
+ :maxdepth: 2
+ :glob:
+
+ ../../api/pyramid
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/profiling.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,52 @@
+.. _profiling:
+
+Profiling
+=========
+
+Profiling of requests by the pyramid debug toolbar can be a little restrictive
+when a specific url needs thin profiling that includes the whole pyramid
+dispatch.
+
+Pyramid CubicWeb provides facilities to profile requests as a
+:func:`wsgi middleware <cubicweb.pyramid.profile.wsgi_profile>`, and a few
+views that facilitate profiling of basic features.
+
+The views and the wsgi middleware are activated when the 'profile' option is
+given. This can be done on the command line
+(:option:`cubicweb-ctl pyramid --profile`) or in the :ref:`pyramid_settings`.
+
+Views
+-----
+
+The following routes and corresponding views are provided when profiling is on:
+
+- ``/_profile/ping``: Reply 'ping' without doing anything else. See also
+ :func:`cubicweb.pyramid.profile.ping`.
+
+- ``/_profile/cnx``: Reply 'ping' after getting a cnx. See also
+ :func:`cubicweb.pyramid.profile.cnx`.
+
+Typical Usage
+-------------
+
+Let's say we want to measure the cost of having a ``cnx``.
+
+- Start the application with profile enabled:
+
+ .. code-block:: console
+
+ $ cubicweb-ctl pyramid --no-daemon --profile --profile-dump-every 100
+
+- Use 'ab' or any other http benchmark tool to throw a lot of requests:
+
+ .. code-block:: console
+
+ $ ab -c 1 -n 100 http://localhost:8080/_profile/cnx
+
+- Analyse the results. I personnaly fancy SnakeViz_:
+
+ .. code-block:: console
+
+ $ snakeviz program.prof
+
+.. _SnakeViz: http://jiffyclub.github.io/snakeviz/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/quickstart.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,59 @@
+Quick start
+===========
+
+.. highlight:: bash
+
+Prerequites
+-----------
+
+- Install everything (here with pip, possibly in a virtualenv)::
+
+ pip install pyramid-cubicweb cubicweb-pyramid pyramid_debugtoolbar
+
+- Have a working Cubicweb instance, for example:
+
+
+ - Make sure CubicWeb is in user mode::
+
+ export CW_MODE=user
+
+ - Create a CubicWeb instance, and install the 'pyramid' cube on it (see
+ :ref:`configenv` for more details on this step)::
+
+ cubicweb-ctl create pyramid myinstance
+
+- Edit your ``~/etc/cubicweb.d/myinstance/all-in-one.conf`` and set values for
+ :confval:`pyramid-auth-secret` and :confval:`pyramid-session-secret`.
+ *required if cubicweb.pyramid.auth and pyramid_cubiweb.session get
+ included, which is the default*
+
+From CubicWeb
+-------------
+
+- Start the instance with the :ref:`'pyramid' command <cubicweb-ctl_pyramid>`
+ instead of 'start'::
+
+ cubicweb-ctl pyramid --debug myinstance
+
+In a pyramid application
+------------------------
+
+- Create a pyramid application
+
+- Include cubicweb.pyramid:
+
+ .. code-block:: python
+
+ def includeme(config):
+ # ...
+ config.include('cubicweb.pyramid')
+ # ...
+
+- Configure the instance name (in the .ini file):
+
+ .. code-block:: ini
+
+ cubicweb.instance = myinstance
+
+- Configure the base-url and https-url in all-in-one.conf to match the ones
+ of the pyramid configuration (this is a temporary limitation).
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/pyramid/settings.rst Mon Sep 26 14:52:12 2016 +0200
@@ -0,0 +1,146 @@
+Settings
+========
+
+.. _cubicweb_settings:
+
+Cubicweb Settings
+-----------------
+
+Pyramid CubicWeb will **not** make use of the configuration entries
+found in the cubicweb configuration (a.k.a. `all-in-one.conf`) for any
+pyramid related configuration value.
+
+
+.. _pyramid_settings:
+
+Pyramid Settings
+----------------
+
+If a ``pyramid.ini`` file is found in the instance home directory (where the
+``all-in-one.conf`` file is), its ``[main]`` section will be read and used as the
+``settings`` of the pyramid Configurator.
+
+This configuration file is almost the same as the one read by ``pserve``, which
+allow to easily add any pyramid extension and configure it.
+
+A typical ``pyramid.ini`` file is:
+
+.. code-block:: ini
+
+ [main]
+ pyramid.includes =
+ pyramid_redis_sessions
+
+ cubicweb.defaults = no
+ cubicweb.includes =
+ cubicweb.pyramid.auth
+ cubicweb.pyramid.login
+
+ cubicweb.profile = no
+
+ redis.sessions.secret = your_cookie_signing_secret
+ redis.sessions.timeout = 1200
+
+ redis.sessions.host = mywheezy
+
+The Pyramid CubicWeb specific configuration entries are:
+
+.. confval:: cubicweb.instance (string)
+
+ A CubicWeb instance name. Useful when the application is not run by
+ :ref:`cubicweb-ctl_pyramid`.
+
+.. confval:: cubicweb.debug (bool)
+
+ Enables the cubicweb debugmode. Works only if the instance is setup by
+ :confval:`cubicweb.instance`.
+
+ Unlike when the debugmode is set by the :option:`cubicweb-ctl pyramid --debug-mode`
+ command, the pyramid debug options are untouched.
+
+.. confval:: cubicweb.includes (list)
+
+ Same as ``pyramid.includes``, but the includes are done after the cubicweb
+ specific registry entries are initialized.
+
+ Useful to include extensions that requires these entries.
+
+.. confval:: cubicweb.bwcompat (bool)
+
+ (True) Enable/disable backward compatibility. See :ref:`bwcompat_module`.
+
+.. confval:: cubicweb.bwcompat.errorhandler (bool)
+
+ (True) Enable/disable the backward compatibility error handler.
+ Set to 'no' if you need to define your own error handlers.
+
+.. confval:: cubicweb.defaults (bool)
+
+ (True) Enable/disable defaults. See :ref:`defaults_module`.
+
+.. confval:: cubicweb.profile (bool)
+
+ (False) Enable/disable profiling. See :ref:`profiling`.
+
+.. confval:: cubicweb.auth.update_login_time (bool)
+
+ (True) Add a :class:`cubicweb.pyramid.auth.UpdateLoginTimeAuthenticationPolicy`
+ policy, that update the CWUser.login_time attribute when a user login.
+
+.. confval:: cubicweb.auth.authtkt (bool)
+
+ (True) Enables the 2 cookie-base auth policies, which activate/deactivate
+ depending on the `persistent` argument passed to `remember`.
+
+ The default login views set persistent to True if a `__setauthcookie`
+ parameters is passed to them, and evals to True in
+ :func:`pyramid.settings.asbool`.
+
+ The configuration values of the policies are arguments for
+ :class:`pyramid.authentication.AuthTktAuthenticationPolicy`.
+
+ The first policy handles session authentication. It doesn't get
+ activated if `remember()` is called with `persistent=False`:
+
+ .. confval:: cubicweb.auth.authtkt.session.cookie_name (str)
+
+ ('auth_tkt') The cookie name. Must be different from the persistent
+ authentication cookie name.
+
+ .. confval:: cubicweb.auth.authtkt.session.timeout (int)
+
+ (1200) Cookie timeout.
+
+ .. confval:: cubicweb.auth.authtkt.session.reissue_time (int)
+
+ (120) Reissue time.
+
+ The second policy handles persistent authentication. It doesn't get
+ activated if `remember()` is called with `persistent=True`:
+
+ .. confval:: cubicweb.auth.authtkt.persistent.cookie_name (str)
+
+ ('auth_tkt') The cookie name. Must be different from the session
+ authentication cookie name.
+
+ .. confval:: cubicweb.auth.authtkt.persistent.max_age (int)
+
+ (30 days) Max age in seconds.
+
+ .. confval:: cubicweb.auth.authtkt.persistent.reissue_time (int)
+
+ (1 day) Reissue time in seconds.
+
+ Both policies set the ``secure`` flag to ``True`` by default, meaning that
+ cookies will only be sent back over a secure connection (see
+ `Authentication Policies documentation`_ for details). This can be
+ configured through :confval:`cubicweb.auth.authtkt.persistent.secure` and
+ :confval:`cubicweb.auth.authtkt.session.secure` configuration options.
+
+ .. _`Authentication Policies documentation`: \
+ http://docs.pylonsproject.org/projects/pyramid/en/latest/api/authentication.html
+
+.. confval:: cubicweb.auth.groups_principals (bool)
+
+ (True) Setup a callback on the authentication stack that inject the user
+ groups in the principals.
--- a/doc/conf.py Fri Sep 23 16:04:32 2016 +0200
+++ b/doc/conf.py Mon Sep 26 14:52:12 2016 +0200
@@ -224,3 +224,8 @@
.. |yams| replace:: *Yams*
.. |rql| replace:: *RQL*
"""
+
+def setup(app):
+ app.add_object_type('confval', 'confval',
+ objname='configuration value',
+ indextemplate='pair: %s; configuration value')
--- a/doc/index.rst Fri Sep 23 16:04:32 2016 +0200
+++ b/doc/index.rst Mon Sep 26 14:52:12 2016 +0200
@@ -73,6 +73,7 @@
book/devrepo/index
book/devweb/index
+ book/pyramid/index
.. toctree::
:maxdepth: 2
--- a/requirements/test-misc.txt Fri Sep 23 16:04:32 2016 +0200
+++ b/requirements/test-misc.txt Mon Sep 26 14:52:12 2016 +0200
@@ -20,5 +20,8 @@
## cubicweb/hooks/test
psycopg2
+## cubicweb/pyramid/test
+http://hg.logilab.org/review/cubes/pyramid/archive/4808ab6b1c9c.tar.bz2
+
## cubicweb/sobject/test
cubicweb-comment
--- a/tox.ini Fri Sep 23 16:04:32 2016 +0200
+++ b/tox.ini Mon Sep 26 14:52:12 2016 +0200
@@ -15,7 +15,7 @@
commands =
py34: touch {envdir}/share/cubicweb/cubes/__init__.py
misc: {envpython} -m pip install --upgrade --no-deps --quiet git+git://github.com/logilab/yapps@master#egg=yapps
- misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/test {toxinidir}/cubicweb/dataimport/test {toxinidir}/cubicweb/devtools/test {toxinidir}/cubicweb/entities/test {toxinidir}/cubicweb/ext/test {toxinidir}/cubicweb/hooks/test {toxinidir}/cubicweb/sobjects/test {toxinidir}/cubicweb/wsgi/test
+ misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/test {toxinidir}/cubicweb/dataimport/test {toxinidir}/cubicweb/devtools/test {toxinidir}/cubicweb/entities/test {toxinidir}/cubicweb/ext/test {toxinidir}/cubicweb/hooks/test {toxinidir}/cubicweb/sobjects/test {toxinidir}/cubicweb/wsgi/test {toxinidir}/cubicweb/pyramid/test
py27-misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/etwist/test
server: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/server/test
web: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/web/test
@@ -139,5 +139,25 @@
cubicweb/web/views/json.py,
cubicweb/web/views/searchrestriction.py,
cubicweb/xy.py,
+ cubicweb/pyramid/auth.py,
+ cubicweb/pyramid/bwcompat.py,
+ cubicweb/pyramid/core.py,
+ cubicweb/pyramid/defaults.py,
+ cubicweb/pyramid/init_instance.py,
+ cubicweb/pyramid/__init__.py,
+ cubicweb/pyramid/login.py,
+ cubicweb/pyramid/predicates.py,
+ cubicweb/pyramid/profile.py,
+ cubicweb/pyramid/resources.py,
+ cubicweb/pyramid/rest_api.py,
+ cubicweb/pyramid/session.py,
+ cubicweb/pyramid/tools.py,
+ cubicweb/pyramid/test/__init__.py,
+ cubicweb/pyramid/test/test_bw_request.py,
+ cubicweb/pyramid/test/test_core.py,
+ cubicweb/pyramid/test/test_login.py,
+ cubicweb/pyramid/test/test_rest_api.py,
+ cubicweb/pyramid/test/test_tools.py,
+
# vim: wrap sts=2 sw=2