diff --git a/pecan/__init__.py b/pecan/__init__.py index 436e961..2eb229f 100644 --- a/pecan/__init__.py +++ b/pecan/__init__.py @@ -1,2 +1,25 @@ +from paste.urlparser import StaticURLParser +from paste.cascade import Cascade + from pecan import Pecan, request, override_template -from decorators import expose \ No newline at end of file +from decorators import expose + + +def make_app(root, renderers = None, + default_renderer = None, + template_path = None, + hooks = None, + static_root = None): + + kw = {} + if renderers is not None: kw['renderers'] = renderers + if default_renderer is not None: kw['default_renderer'] = default_renderer + if template_path is not None: kw['template_path'] = template_path + if hooks is not None: kw['hooks'] = hooks + + app = Pecan(root, **kw) + + if static_root: + app = Cascade([StaticURLParser(static_root), app]) + + return app \ No newline at end of file diff --git a/pecan/pecan.py b/pecan/pecan.py index 5f8d680..06d5bec 100644 --- a/pecan/pecan.py +++ b/pecan/pecan.py @@ -71,6 +71,11 @@ class Pecan(object): response.status = 404 return response(environ, start_response) + # handle security + if controller.pecan.get('secured', False): + if not controller.pecan['check_permissions'](): + raise exc.HTTPUnauthorized + # determine content type if content_type is None: content_type = controller.pecan.get('content_type', 'text/html') diff --git a/pecan/secure.py b/pecan/secure.py new file mode 100644 index 0000000..edb4a81 --- /dev/null +++ b/pecan/secure.py @@ -0,0 +1,50 @@ +from webob.exc import HTTPUnauthorized +from inspect import getmembers, ismethod + +from routing import iscontroller + + +__all__ = ['secure', 'unlocked', 'SecureController'] + + +def unlocked(func): + if not hasattr(func, 'pecan'): func.pecan = {} + func.pecan['unlocked'] = True + return func + + +def secure(check_permissions): + def wrap(func): + if not hasattr(func, 'pecan'): func.pecan = {} + func.pecan['secured'] = True + func.pecan['check_permissions'] = check_permissions + return func + return wrap + + +def walk_controller(root_class, controller): + if hasattr(controller, '_lookup'): + # TODO: what about this? + controller._check_security = root_class._perform_validation + + if not isinstance(controller, (int, dict)): + for name, value in getmembers(controller): + if name == 'controller': continue + + if ismethod(value): + if iscontroller(value) and not value.pecan.get('unlocked', False): + value.pecan['secured'] = True + value.pecan['check_permissions'] = root_class.check_permissions + elif hasattr(value, '__class__'): + if name.startswith('__') and name.endswith('__'): continue + walk_controller(root_class, value) + + +class SecureController(object): + class __metaclass__(type): + def __init__(cls, name, bases, dict_): + walk_controller(cls, cls) + + @classmethod + def check_permissions(cls): + return True \ No newline at end of file diff --git a/setup.py b/setup.py index a6330f7..2759b72 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,8 @@ setup( "Kajiki >= 0.2.2", "Mako >= 0.3", "py >= 1.3.4", - "WebTest >= 1.2.2" + "WebTest >= 1.2.2", + "Paste >= 1.7.5.1" ], entry_points = """ # -*- Entry points: -*- diff --git a/tests/static/test.txt b/tests/static/test.txt new file mode 100644 index 0000000..c6defe5 --- /dev/null +++ b/tests/static/test.txt @@ -0,0 +1,9 @@ +This is a test text file. + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +commodo consequat. Duis aute irure dolor in reprehenderit in voluptate +velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint +occaecat cupidatat non proident, sunt in culpa qui officia deserunt +mollit anim id est laborum. \ No newline at end of file diff --git a/tests/test_secure.py b/tests/test_secure.py new file mode 100644 index 0000000..d34c7c7 --- /dev/null +++ b/tests/test_secure.py @@ -0,0 +1,61 @@ +from pecan import expose, make_app +from pecan.secure import secure, unlocked, SecureController +from webob import exc +from webtest import TestApp +from py.test import raises + +class TestSecure(object): + + def test_simple_secure(self): + authorized = False + + class SecretController(SecureController): + @expose() + def index(self): + return 'Index' + + @expose() + @unlocked + def allowed(self): + return 'Allowed!' + + @classmethod + def check_permissions(self): + return authorized + + class RootController(object): + @expose() + def index(self): + return 'Hello, World!' + + @expose() + @secure(lambda: False) + def locked(self): + return 'No dice!' + + @expose() + @secure(lambda: True) + def unlocked(self): + return 'Sure thing' + + secret = SecretController() + + + app = TestApp(make_app(RootController(), static_root='tests/static')) + response = app.get('/') + assert response.status_int == 200 + assert response.body == 'Hello, World!' + + response = app.get('/unlocked') + assert response.status_int == 200 + assert response.body == 'Sure thing' + + with raises(exc.HTTPUnauthorized): + response = app.get('/locked') + + with raises(exc.HTTPUnauthorized): + response = app.get('/secret') + + response = app.get('/secret/allowed') + assert response.status_int == 200 + assert response.body == 'Allowed!' \ No newline at end of file diff --git a/tests/test_static.py b/tests/test_static.py new file mode 100644 index 0000000..4b21461 --- /dev/null +++ b/tests/test_static.py @@ -0,0 +1,21 @@ +from pecan import expose, make_app +from webtest import TestApp + +class TestStatic(object): + + def test_simple_static(self): + class RootController(object): + @expose() + def index(self): + return 'Hello, World!' + + # make sure Cascade is working properly + app = TestApp(make_app(RootController(), static_root='tests/static')) + response = app.get('/index.html') + assert response.status_int == 200 + assert response.body == 'Hello, World!' + + # get a static resource + response = app.get('/test.txt') + assert response.status_int == 200 + assert response.body == open('tests/static/test.txt', 'rb').read() \ No newline at end of file