summaryrefslogtreecommitdiff
path: root/docs/source/secure_controller.rst
blob: 65709b5eeb14b383b8fb43c096611e0364631548 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
.. _secure_controller:

Security and Authentication
===========================
Pecan provides no out-of-the-box support for authentication, but it does give 
you the necessary tools to handle authentication and authorization as you see
fit.

In Pecan, you can wrap entire controller subtrees *or* individual method calls
with function calls to determine access and secure portions of your 
application.

Pecan's ``secure`` decorator secures a method or class depending on invocation.

To decorate a method, use one argument::

    secure('<check_permissions_method>')

To secure a class, invoke with two arguments::

    secure(object_instance, '<check_permissions_method>')

::

    from pecan import expose
    from pecan.secure import secure
    
    class HighlyClassifiedController(object):
        pass

    class UnclassifiedController(object):
        pass

    class RootController(object):
        
        @classmethod
        def check_permissions(cls):
            if user_is_admin():
                return True
            return False
    
        @expose()
        def index(self):
          #
          # This controller is unlocked to everyone,
          # and will not run any security checks.
          #
          return dict()
    
        @secure('check_permissions')
        @expose()
        def topsecret(self):
            #
            # This controller is top-secret, and should
            # only be reachable by administrators.
            #
            return dict()
    
        highly_classified = secure(HighlyClassifiedController(), 'check_permissions')
        unclassified = UnclassifiedController()

Alternatively, the same functionality can also be accomplished by subclassing 
Pecan's ``SecureController`` class. Implementations of ``SecureController`` 
should extend the ``check_permissions`` classmethod to return a ``True``
or ``False`` value (depending on whether or not the user has permissions to 
the controller branch)::

    from pecan import expose
    from pecan.secure import SecureController, unlocked
    
    class HighlyClassifiedController(object):
        pass

    class UnclassifiedController(object):
        pass

    class RootController(SecureController):
        
        @classmethod
        def check_permissions(cls):
            if user_is_admin():
                return True
            return False
    
        @expose()
        @unlocked
        def index(self):
          #
          # This controller is unlocked to everyone,
          # and will not run any security checks.
          #
          return dict()
    
        @expose()
        def topsecret(self):
            #
            # This controller is top-secret, and should
            # only be reachable by administrators.
            #
            return dict()
    
        highly_classified = HighlyClassifiedController()
        unclassified = unlocked(UnclassifiedController())


Also note the use of the ``@unlocked`` decorator in the above example, which 
can be used similarly to explicitly unlock a controller for public access 
without any security checks.


Writing Authentication/Authorization Methods
--------------------------------------------
The ``check_permissions`` method should be used to determine user 
authentication and authorization.  The code you implement here could range 
from simple session assertions (the existing user is authenticated as an 
administrator) to connecting to an LDAP service.


More on ``secure``
------------------
The ``secure`` method has several advanced uses that allow you to create
robust security policies for your application.

First, you can pass via a string the name of either a classmethod or an 
instance method of the controller to use as the ``check_permission`` method.
Instance methods are particularly useful if you wish to authorize access to
attributes of a particular model instance.  Consider the following example
of a basic virtual filesystem::

    from pecan import expose
    from pecan.secure import secure
    
    from myapp.session import get_current_user
    from myapp.model import FileObject
    
    class FileController(object):
        def __init__(self, name):
            self.file_object = FileObject(name)
    
        def read_access(self):
            self.file_object.read_access(get_current_user())
    
        def write_access(self):
            self.file_object.write_access(get_current_user())
    
        @secure('write_access')
        @expose()
        def upload_file(self):
            pass
    
        @secure('read_access')
        @expose()
        def download_file(self):
            pass 
    
    class RootController(object):
        @expose()
        def _lookup(self, name, *remainder):
            return FileController(name), remainder


The ``secure`` method also accepts a function instead of a string. When 
passing a function,  make sure that the function is imported from another 
file or defined in the same file before the class definition -- otherwise 
you will likely get error during module import. ::

    from pecan import expose
    from pecan.secure import secure

    from myapp.auth import user_authenitcated

    class RootController(object):
        @secure(user_authenticated)
        @expose()
        def index(self):
            return 'Logged in'


You can also use the ``secure`` method to change the behavior of a 
``SecureController``. Decorating a method or wrapping a subcontroller tells 
Pecan to use another security function other than the default controller 
method. This is useful for situations where you want a different level or 
type of security.

::

    from pecan import expose
    from pecan.secure import SecureController, secure

    from myapp.auth import user_authenticated, admin_user

    class ApiController(object):
        pass

    class RootController(SecureController):
        @classmethod
        def check_permissions(cls):
            return user_authenticated()

        @classmethod
        def check_api_permissions(cls):
            return admin_user()

        @expose()
        def index(self):
            return 'logged in user'

        api = secure(ApiController(), 'check_api_permissions')

In the example above, pecan will *only* call ``admin_user`` when a request is
made for ``/api/``.


Multiple Secure Controllers
---------------------------
Pecan allows you to have nested secure controllers. In the example below, when
a request is made for ``/admin/index/``, Pecan first calls 
``check_permissions`` on the RootController and then calls 
``check_permissions`` on the AdminController. The ability to nest 
``SecureController`` instances allows you to protect controllers with an 
increasing level of protection. ::

    from pecan import expose
    from pecan.secure import SecureController

    from myapp.auth import user_logged_in, is_admin

    class AdminController(SecureController):
        @classmethod
        def check_permissions(cls):
            return is_admin()

        @expose()
        def index(self):
            return 'admin dashboard'

    class RootController(SecureController):
        @classmethod
        def check_permissions(cls):
            return user_logged_in

        @expose()
        def index(self):
            return 'user dashboard'