# HG changeset patch # User Christophe de Vienne # Date 1406821712 -7200 # Node ID b0b8942cdb809aa90e7810d18e83a2918bf095e7 # Parent 6ba31f0c7d5a45f7ee1d5bc4a1b89b689c3a5442 Separate into 4 modules * init_instance: load the cubicweb repository from the `pyramid_cubicweb.instance` configuration key * defaults: provides cw-like defaults for the authentication and session management * core: make cubicweb use the authentication and session management of pyramid. It assumes the application provides the auth policies and session factory, and that the `cubicweb.*` registry entries are correctly initialised. This is this only required module or pyramid_cubicweb, the other ones are optional if the application provides its own versions of what they do. * bwcompat: provides a catchall route that delegate the request handling to an old-fashion cubicweb publisher (ie using url_resolver and controllers). Related to #4291173 diff -r 6ba31f0c7d5a -r b0b8942cdb80 pyramid_cubicweb/__init__.py --- a/pyramid_cubicweb/__init__.py Tue Jul 22 23:46:09 2014 +0200 +++ b/pyramid_cubicweb/__init__.py Thu Jul 31 17:48:32 2014 +0200 @@ -1,248 +0,0 @@ -from contextlib import contextmanager -from warnings import warn - -import rql - -from cubicweb.web.request import CubicWebRequestBase -from cubicweb.cwconfig import CubicWebConfiguration -from cubicweb import repoapi - -import cubicweb -import cubicweb.web - -from pyramid import security, httpexceptions -from pyramid.httpexceptions import HTTPSeeOther - -from pyramid_cubicweb import authplugin - -import logging - -log = logging.getLogger(__name__) - - -@contextmanager -def cw_to_pyramid(request): - """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) - 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.')) - 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.')) - except (rql.BadRQLQuery, cubicweb.web.RequestError) as ex: - raise - - -class CubicWebPyramidRequest(CubicWebRequestBase): - def __init__(self, request): - self._request = request - - self.path = request.upath_info - - vreg = request.registry['cubicweb.appli'].vreg - https = request.scheme == 'https' - - post = request.params - headers_in = request.headers - - super(CubicWebPyramidRequest, self).__init__(vreg, https, post, - headers=headers_in) - - 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) - - -def render_view(request, vid, **kwargs): - 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 login(request): - repo = request.registry['cubicweb.repository'] - - response = request.response - user_eid = None - - if '__login' in request.params: - 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: - raise - - if user_eid is not None: - headers = security.remember(request, user_eid) - - raise HTTPSeeOther( - request.params.get('postlogin_path', '/'), - headers=headers) - - response.headerlist.extend(headers) - - response.text = render_view(request, 'login') - return response - - -def _cw_cnx(request): - cnx = repoapi.ClientConnection(request.cw_session) - - def cleanup(request): - if request.exception is not None: - cnx.rollback() - else: - cnx.commit() - cnx.__exit__(None, None, None) - - request.add_finished_callback(cleanup) - cnx.__enter__() - return cnx - - -def _cw_close_session(request): - request.cw_session.close() - - -def _cw_session(request): - """Obtains a cw session from a pyramid request""" - repo = request.registry['cubicweb.repository'] - config = request.registry['cubicweb.config'] - - if not request.authenticated_userid: - login, password = config.anonymous_user() - sessionid = repo.connect(login, password=password) - session = repo._sessions[sessionid] - request.add_finished_callback(_cw_close_session) - else: - session = request._cw_cached_session - - # XXX Ideally we store the cw session data in the pyramid session. - # BUT some data in the cw session data dictionnary makes pyramid fail. - session.data = request.session - - return session - - -def _cw_request(request): - req = CubicWebPyramidRequest(request) - req.set_cnx(request.cw_cnx) - return req - - -def get_principals(login, request): - repo = request.registry['cubicweb.repository'] - - try: - sessionid = repo.connect( - str(login), __pyramid_directauth=authplugin.EXT_TOKEN) - session = repo._sessions[sessionid] - request._cw_cached_session = session - request.add_finished_callback(_cw_close_session) - except: - log.exception("Failed") - raise - - return session.user.groups - - -from pyramid.authentication import SessionAuthenticationPolicy -from pyramid.authorization import ACLAuthorizationPolicy -from pyramid.session import SignedCookieSessionFactory - - -def hello_world(request): - request.response.text = \ - u"Hello %s" % request.cw_cnx.user.login - return request.response - - -def includeme(config): - appid = config.registry.settings['cubicweb.instance'] - cwconfig = CubicWebConfiguration.config_for(appid) - - config.set_session_factory( - SignedCookieSessionFactory( - secret=config.registry.settings['session.secret'] - )) - - config.set_authentication_policy( - SessionAuthenticationPolicy(callback=get_principals)) - config.set_authorization_policy(ACLAuthorizationPolicy()) - - config.registry['cubicweb.config'] = cwconfig - config.registry['cubicweb.repository'] = repo = cwconfig.repository() - config.registry['cubicweb.registry'] = repo.vreg - - repo.system_source.add_authentifier(authplugin.DirectAuthentifier()) - - 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) - - config.add_route('login', '/login') - config.add_view(login, route_name='login') - - config.add_route('hello', '/hello') - config.add_view(hello_world, route_name='hello') - - config.include('pyramid_cubicweb.handler') diff -r 6ba31f0c7d5a -r b0b8942cdb80 pyramid_cubicweb/bwcompat.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyramid_cubicweb/bwcompat.py Thu Jul 31 17:48:32 2014 +0200 @@ -0,0 +1,123 @@ +from pyramid import security +from pyramid.httpexceptions import HTTPSeeOther +from pyramid import httpexceptions + +import cubicweb +import cubicweb.web + +from cubicweb.web.application import CubicWebPublisher + +from cubicweb.web import LogOut, cors + +from pyramid_cubicweb.core import cw_to_pyramid + + +class PyramidSessionHandler(object): + """A CW Session handler that rely on the pyramid API to fetch the needed + informations""" + + 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): + 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: + try: + with cw_to_pyramid(request): + cors.process_request(req, vreg.config) + 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 cors.CORSPreflight: + request.response.status_int = 200 + 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 + + # 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) + + 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 AuthenticationError: + # XXX I don't think it makes sens to catch this ex here (cdevienne) + + return request.response + + +def includeme(config): + # Set up a defaut route to handle non-catched urls. + # This is to keep legacy compatibility for cubes that makes use of the + # cubicweb controllers. + cwconfig = config.registry['cubicweb.config'] + repository = config.registry['cubicweb.repository'] + cwappli = CubicWebPublisher( + repository, cwconfig, + session_handler_fact=PyramidSessionHandler) + handler = CubicWebPyramidHandler(cwappli) + + config.registry['cubicweb.appli'] = cwappli + config.registry['cubicweb.handler'] = handler + + config.add_route('catchall', pattern='*path') + config.add_view(handler, route_name='catchall') diff -r 6ba31f0c7d5a -r b0b8942cdb80 pyramid_cubicweb/core.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyramid_cubicweb/core.py Thu Jul 31 17:48:32 2014 +0200 @@ -0,0 +1,223 @@ +from contextlib import contextmanager +from warnings import warn + +import rql + +from cubicweb.web.request import CubicWebRequestBase +from cubicweb import repoapi + +import cubicweb +import cubicweb.web + +from pyramid import security, httpexceptions +from pyramid.httpexceptions import HTTPSeeOther + +from pyramid_cubicweb import authplugin + +import logging + +log = logging.getLogger(__name__) + + +@contextmanager +def cw_to_pyramid(request): + """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) + 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.')) + 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.')) + except (rql.BadRQLQuery, cubicweb.web.RequestError) as ex: + raise + + +class CubicWebPyramidRequest(CubicWebRequestBase): + def __init__(self, request): + self._request = request + + self.path = request.upath_info + + vreg = request.registry['cubicweb.appli'].vreg + https = request.scheme == 'https' + + post = request.params + headers_in = request.headers + + super(CubicWebPyramidRequest, self).__init__(vreg, https, post, + headers=headers_in) + + 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) + + +def render_view(request, vid, **kwargs): + 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 login(request): + repo = request.registry['cubicweb.repository'] + + response = request.response + user_eid = None + + if '__login' in request.params: + 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: + raise + + if user_eid is not None: + headers = security.remember(request, user_eid) + + raise HTTPSeeOther( + request.params.get('postlogin_path', '/'), + headers=headers) + + response.headerlist.extend(headers) + + response.text = render_view(request, 'login') + return response + + +def _cw_cnx(request): + cnx = repoapi.ClientConnection(request.cw_session) + + def cleanup(request): + if request.exception is not None: + cnx.rollback() + else: + cnx.commit() + cnx.__exit__(None, None, None) + + request.add_finished_callback(cleanup) + cnx.__enter__() + return cnx + + +def _cw_close_session(request): + request.cw_session.close() + + +def _cw_session(request): + """Obtains a cw session from a pyramid request""" + repo = request.registry['cubicweb.repository'] + config = request.registry['cubicweb.config'] + + if not request.authenticated_userid: + login, password = config.anonymous_user() + sessionid = repo.connect(login, password=password) + session = repo._sessions[sessionid] + request.add_finished_callback(_cw_close_session) + else: + session = request._cw_cached_session + + # XXX Ideally we store the cw session data in the pyramid session. + # BUT some data in the cw session data dictionnary makes pyramid fail. + session.data = request.session + + return session + + +def _cw_request(request): + req = CubicWebPyramidRequest(request) + req.set_cnx(request.cw_cnx) + return req + + +def get_principals(login, request): + repo = request.registry['cubicweb.repository'] + + try: + sessionid = repo.connect( + str(login), __pyramid_directauth=authplugin.EXT_TOKEN) + session = repo._sessions[sessionid] + request._cw_cached_session = session + request.add_finished_callback(_cw_close_session) + except: + log.exception("Failed") + raise + + return session.user.groups + + +def hello_world(request): + request.response.text = \ + u"Hello %s" % request.cw_cnx.user.login + return request.response + + +def includeme(config): + repo = config.registry['cubicweb.repository'] + + repo.system_source.add_authentifier(authplugin.DirectAuthentifier()) + + 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) + + config.add_route('login', '/login') + config.add_view(login, route_name='login') diff -r 6ba31f0c7d5a -r b0b8942cdb80 pyramid_cubicweb/defaults.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyramid_cubicweb/defaults.py Thu Jul 31 17:48:32 2014 +0200 @@ -0,0 +1,16 @@ +from pyramid.authentication import SessionAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.session import SignedCookieSessionFactory + +from pyramid_cubicweb.core import get_principals + + +def includeme(config): + config.set_session_factory( + SignedCookieSessionFactory( + secret=config.registry.settings['session.secret'] + )) + + config.set_authentication_policy( + SessionAuthenticationPolicy(callback=get_principals)) + config.set_authorization_policy(ACLAuthorizationPolicy()) diff -r 6ba31f0c7d5a -r b0b8942cdb80 pyramid_cubicweb/handler.py --- a/pyramid_cubicweb/handler.py Tue Jul 22 23:46:09 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,123 +0,0 @@ -from pyramid import security -from pyramid.httpexceptions import HTTPSeeOther -from pyramid import httpexceptions - -import cubicweb -import cubicweb.web - -from cubicweb.web.application import CubicWebPublisher - -from cubicweb.web import LogOut, cors - -from pyramid_cubicweb import cw_to_pyramid - - -class PyramidSessionHandler(object): - """A CW Session handler that rely on the pyramid API to fetch the needed - informations""" - - 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): - 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: - try: - with cw_to_pyramid(request): - cors.process_request(req, vreg.config) - 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 cors.CORSPreflight: - request.response.status_int = 200 - 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 - - # 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) - - 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 AuthenticationError: - # XXX I don't think it makes sens to catch this ex here (cdevienne) - - return request.response - - -def includeme(config): - # Set up a defaut route to handle non-catched urls. - # This is to keep legacy compatibility for cubes that makes use of the - # cubicweb controllers. - cwconfig = config.registry['cubicweb.config'] - repository = config.registry['cubicweb.repository'] - cwappli = CubicWebPublisher( - repository, cwconfig, - session_handler_fact=PyramidSessionHandler) - handler = CubicWebPyramidHandler(cwappli) - - config.registry['cubicweb.appli'] = cwappli - config.registry['cubicweb.handler'] = handler - - config.add_route('catchall', pattern='*path') - config.add_view(handler, route_name='catchall') diff -r 6ba31f0c7d5a -r b0b8942cdb80 pyramid_cubicweb/init_instance.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pyramid_cubicweb/init_instance.py Thu Jul 31 17:48:32 2014 +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 6ba31f0c7d5a -r b0b8942cdb80 sampleapp/development.ini --- a/sampleapp/development.ini Tue Jul 22 23:46:09 2014 +0200 +++ b/sampleapp/development.ini Thu Jul 31 17:48:32 2014 +0200 @@ -13,7 +13,6 @@ pyramid.default_locale_name = en pyramid.includes = pyramid_debugtoolbar - pyramid_cubicweb cubicweb.instance = test diff -r 6ba31f0c7d5a -r b0b8942cdb80 sampleapp/sampleapp/__init__.py --- a/sampleapp/sampleapp/__init__.py Tue Jul 22 23:46:09 2014 +0200 +++ b/sampleapp/sampleapp/__init__.py Thu Jul 31 17:48:32 2014 +0200 @@ -5,6 +5,10 @@ """ This function returns a Pyramid WSGI application. """ config = Configurator(settings=settings) + config.include('pyramid_cubicweb.init_instance') + config.include('pyramid_cubicweb.defaults') + config.include('pyramid_cubicweb.core') + config.include('pyramid_cubicweb.bwcompat') # config.add_static_view('static', 'static', cache_max_age=3600) # config.add_route('home', '/') # config.scan()