# HG changeset patch # User Yann Voté # Date 1474894332 -7200 # Node ID faf279e332980ae0abc433aba899f4128b324da1 # Parent 1817f8946c22afbc69f4e999b95b3c1a62111c92# Parent 1400aee10df4a7e8de57499e87c932990e6f9558 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. diff -r 1817f8946c22 -r faf279e33298 MANIFEST.in --- 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 diff -r 1817f8946c22 -r faf279e33298 README.pyramid.rst --- /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 diff -r 1817f8946c22 -r faf279e33298 cubicweb/__pkginfo__.py --- 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__ = { diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/__init__.py --- /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') diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/auth.py --- /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()) diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/bwcompat.py --- /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) diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/core.py --- /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
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 '
' joined list of the cubicweb current message and the + default pyramid flash queue messages. + """ + return u'\n
\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) diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/defaults.py --- /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 ` + - :func:`cubicweb.pyramid.auth ` + - :func:`cubicweb.pyramid.login ` + + 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') diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/init_instance.py --- /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 diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/login.py --- /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') diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/predicates.py --- /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) diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/profile.py --- /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 diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/resources.py --- /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 diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/rest_api.py --- /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__) diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/session.py --- /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) diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/test/__init__.py --- /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 diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/test/data/bootstrap_cubes --- /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 diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/test/test_bw_request.py --- /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() diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/test/test_core.py --- /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() diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/test/test_login.py --- /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() diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/test/test_rest_api.py --- /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() diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/test/test_tools.py --- /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() diff -r 1817f8946c22 -r faf279e33298 cubicweb/pyramid/tools.py --- /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) diff -r 1817f8946c22 -r faf279e33298 cubicweb/test/unittest_cwconfig.py --- 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) diff -r 1817f8946c22 -r faf279e33298 debian/control --- 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: diff -r 1817f8946c22 -r faf279e33298 debian/copyright --- 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 + 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. - diff -r 1817f8946c22 -r faf279e33298 debian/pydist-overrides --- /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 diff -r 1817f8946c22 -r faf279e33298 debian/rules --- 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. diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid.rst --- /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/* + diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/auth.rst --- /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: diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/authplugin.rst --- /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: diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/bwcompat.rst --- /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: diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/core.rst --- /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 diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/defaults.rst --- /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 diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/login.rst --- /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 diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/profile.rst --- /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 + diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/session.rst --- /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 diff -r 1817f8946c22 -r faf279e33298 doc/api/pyramid/tools.rst --- /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 diff -r 1817f8946c22 -r faf279e33298 doc/book/pyramid/auth.rst --- /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 diff -r 1817f8946c22 -r faf279e33298 doc/book/pyramid/ctl.rst --- /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 , --loglevel= + + 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) diff -r 1817f8946c22 -r faf279e33298 doc/book/pyramid/index.rst --- /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 `. + 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 diff -r 1817f8946c22 -r faf279e33298 doc/book/pyramid/profiling.rst --- /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 `, 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/ diff -r 1817f8946c22 -r faf279e33298 doc/book/pyramid/quickstart.rst --- /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 ` + 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). diff -r 1817f8946c22 -r faf279e33298 doc/book/pyramid/settings.rst --- /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. diff -r 1817f8946c22 -r faf279e33298 doc/conf.py --- 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') diff -r 1817f8946c22 -r faf279e33298 doc/index.rst --- 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 diff -r 1817f8946c22 -r faf279e33298 requirements/test-misc.txt --- 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 diff -r 1817f8946c22 -r faf279e33298 tox.ini --- 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