bca8cea9b84a701a02bee5af29b3eacd15885b4d
[catagits/Catalyst-Manual.git] / lib / Catalyst / Manual / Tutorial / 06_Authorization.pod
1 =head1 NAME
2
3 Catalyst::Manual::Tutorial::06_Authorization - Catalyst Tutorial - Chapter 6: Authorization
4
5
6 =head1 OVERVIEW
7
8 This is B<Chapter 6 of 10> for the Catalyst tutorial.
9
10 L<Tutorial Overview|Catalyst::Manual::Tutorial>
11
12 =over 4
13
14 =item 1
15
16 L<Introduction|Catalyst::Manual::Tutorial::01_Intro>
17
18 =item 2
19
20 L<Catalyst Basics|Catalyst::Manual::Tutorial::02_CatalystBasics>
21
22 =item 3
23
24 L<More Catalyst Basics|Catalyst::Manual::Tutorial::03_MoreCatalystBasics>
25
26 =item 4
27
28 L<Basic CRUD|Catalyst::Manual::Tutorial::04_BasicCRUD>
29
30 =item 5
31
32 L<Authentication|Catalyst::Manual::Tutorial::05_Authentication>
33
34 =item 6
35
36 B<06_Authorization>
37
38 =item 7
39
40 L<Debugging|Catalyst::Manual::Tutorial::07_Debugging>
41
42 =item 8
43
44 L<Testing|Catalyst::Manual::Tutorial::08_Testing>
45
46 =item 9
47
48 L<Advanced CRUD|Catalyst::Manual::Tutorial::09_AdvancedCRUD>
49
50 =item 10
51
52 L<Appendices|Catalyst::Manual::Tutorial::10_Appendices>
53
54 =back
55
56
57 =head1 DESCRIPTION
58
59 This chapter of the tutorial adds role-based authorization to the 
60 existing authentication implemented in Chapter 5.  It provides simple 
61 examples of how to use roles in both TT templates and controller 
62 actions.  The first half looks at basic authorization concepts. The 
63 second half looks at how moving your authorization code to your model 
64 can simplify your code and make things easier to maintain.
65
66 You can checkout the source code for this example from the catalyst
67 subversion repository as per the instructions in
68 L<Catalyst::Manual::Tutorial::01_Intro|Catalyst::Manual::Tutorial::01_Intro>.
69
70
71 =head1 BASIC AUTHORIZATION
72
73 In this section you learn the basics of how authorization works under 
74 Catalyst.
75
76
77 =head2 Update Plugins to Include Support for Authorization
78
79 Edit C<lib/MyApp.pm> and add C<Authorization::Roles> to the list:
80
81     # Load plugins
82     use Catalyst qw/
83                     -Debug
84                     ConfigLoader
85                     Static::Simple
86                 
87                     StackTrace
88                 
89                     Authentication
90                     Authorization::Roles
91         
92                     Session
93                     Session::Store::FastMmap
94                     Session::State::Cookie
95                     /;
96
97 Once again, include this additional plugin as a new dependency in 
98 the Makefile.PL file like this:
99
100     requires (
101         ...
102         'Catalyst::Plugin::Authorization::Roles' => '0',
103     );
104
105
106 =head2 Add Role-Specific Logic to the "Book List" Template
107
108 Open C<root/src/books/list.tt2> in your editor and add the following
109 lines to the bottom of the file:
110
111     ...
112     <p>Hello [% c.user.username %], you have the following roles:</p>
113     
114     <ul>
115       [% # Dump list of roles -%]
116       [% FOR role = c.user.roles %]<li>[% role %]</li>[% END %]
117     </ul>
118     
119     <p>
120     [% # Add some simple role-specific logic to template %]
121     [% # Use $c->check_user_roles() to check authz -%]
122     [% IF c.check_user_roles('user') %]
123       [% # Give normal users a link for 'logout' %]
124       <a href="[% c.uri_for('/logout') %]">User Logout</a>
125     [% END %]
126     
127     [% # Can also use $c->user->check_roles() to check authz -%]
128     [% IF c.check_user_roles('admin') %]
129       [% # Give admin users a link for 'create' %]
130       <a href="[% c.uri_for(c.controller.action_for('form_create')) %]">Admin Create</a>
131     [% END %]
132     </p>
133
134 This code displays a different combination of links depending on the
135 roles assigned to the user.  
136
137
138 =head2 Limit Books::add to 'admin' Users
139
140 C<IF> statements in TT templates simply control the output that is sent
141 to the user's browser; it provides no real enforcement (if users know or
142 guess the appropriate URLs, they are still perfectly free to hit any
143 action within your application).  We need to enhance the controller
144 logic to wrap restricted actions with role-validation logic.
145
146 For example, we might want to restrict the "formless create" action to
147 admin-level users by editing C<lib/MyApp/Controller/Books.pm> and
148 updating C<url_create> to match the following code:
149
150     =head2 url_create
151     
152     Create a book with the supplied title and rating,
153     with manual authorization
154     
155     =cut
156     
157     sub url_create :Chained('base') :PathPart('url_create') :Args(3) {
158         # In addition to self & context, get the title, rating & author_id args
159         # from the URL.  Note that Catalyst automatically puts extra information
160         # after the "/<controller_name>/<action_name/" into @_
161         my ($self, $c, $title, $rating, $author_id) = @_;
162     
163         # Check the user's roles
164         if ($c->check_user_roles('admin')) {
165             # Call create() on the book model object. Pass the table
166             # columns/field values we want to set as hash values
167             my $book = $c->model('DB::Book')->create({
168                     title   => $title,
169                     rating  => $rating
170                 });
171     
172             # Add a record to the join table for this book, mapping to
173             # appropriate author
174             $book->add_to_book_authors({author_id => $author_id});
175             # Note: Above is a shortcut for this:
176             # $book->create_related('book_authors', {author_id => $author_id});
177     
178             # Assign the Book object to the stash for display in the view
179             $c->stash->{book} = $book;
180     
181             # Set the TT template to use
182             $c->stash->{template} = 'books/create_done.tt2';
183         } else {
184             # Provide very simple feedback to the user.
185             $c->response->body('Unauthorized!');
186         }
187     }
188
189
190 To add authorization, we simply wrap the main code of this method in an
191 C<if> statement that calls C<check_user_roles>.  If the user does not
192 have the appropriate permissions, they receive an "Unauthorized!"
193 message.  Note that we intentionally chose to display the message this
194 way to demonstrate that TT templates will not be used if the response
195 body has already been set.  In reality you would probably want to use a
196 technique that maintains the visual continuity of your template layout
197 (for example, using the "status" or "error" message feature added in
198 Chapter 3 or C<detach> to an action that shows an "unauthorized" page).
199
200 B<TIP>: If you want to keep your existing C<url_create> method, you can
201 create a new copy and comment out the original by making it look like a
202 Pod comment.  For example, put something like C<=begin> before 
203 C<sub add : Local {> and C<=end> after the closing C<}>.
204
205
206 =head2 Try Out Authentication And Authorization
207
208 Press C<Ctrl-C> to kill the previous server instance (if it's still
209 running) and restart it:
210
211     $ script/myapp_server.pl
212
213 Now trying going to L<http://localhost:3000/books/list> and you should 
214 be taken to the login page (you might have to C<Shift+Reload> or 
215 C<Ctrl+Reload> your browser and/or click the "User Logout" link on the book 
216 list page).  Try logging in with both C<test01> and C<test02> (both 
217 use a password of C<mypass>) and notice how the roles information 
218 updates at the bottom of the "Book List" page. Also try the "User Logout"
219 link on the book list page.
220
221 Now the "url_create" URL will work if you are already logged in as user
222 C<test01>, but receive an authorization failure if you are logged in as
223 C<test02>.  Try:
224
225     http://localhost:3000/books/url_create/test/1/6
226
227 while logged in as each user.  Use one of the "logout" links (or go to 
228 L<http://localhost:3000/logout> in your browser directly) when you are 
229 done.
230
231
232 =head1 ENABLE MODEL-BASED AUTHORIZATION
233
234 Hopefully it's fairly obvious that adding detailed permission checking 
235 logic to our controllers and view templates isn't a very clean or 
236 scalable way to build role-based permissions into out application.  As 
237 with many other aspects of MVC web development, the goal is to have 
238 your controllers and views be an "thin" as possible, with all of the 
239 "fancy business logic" built into your model.
240
241 For example, let's add a method to our C<Books.pm> Result Class to 
242 check if a user is allowed to delete a book.  Open 
243 C<lib/MyApp/Schema/Result/Book.pm> and add the following method 
244 (be sure to add it below the "C<DO NOT MODIFY ...>" line):
245
246     =head2 delete_allowed_by
247     
248     Can the specified user delete the current book?
249     
250     =cut
251     
252     sub delete_allowed_by {
253         my ($self, $user) = @_;
254         
255         # Only allow delete if user has 'admin' role
256         return $user->has_role('admin');
257     }
258
259 Here we call a C<has_role> method on our user object, so we should add 
260 this method to our Result Class.  Open 
261 C<lib/MyApp/Schema/Result/User.pm> and add the following method below 
262 the "C<DO NOT MODIFY ...>" line:
263
264     =head 2 has_role
265     
266     Check if a user has the specified role
267     
268     =cut
269     
270     use Perl6::Junction qw/any/;
271     sub has_role {
272         my ($self, $role) = @_;
273     
274         # Does this user posses the required role?
275         return any(map { $_->role } $self->roles) eq $role;
276     }
277
278 Now we need to add some enforcement inside our controller.  Open
279 C<lib/MyApp/Controller/Books.pm> and update the C<delete> method to
280 match the following code:
281
282     =head2 delete
283     
284     Delete a book
285     
286     =cut
287     
288     sub delete :Chained('object') :PathPart('delete') :Args(0) {
289         my ($self, $c) = @_;
290     
291         # Check permissions
292         $c->detach('/error_noperms')
293             unless $c->stash->{object}->delete_allowed_by($c->user->get_object);
294     
295         # Use the book object saved by 'object' and delete it along
296         # with related 'book_authors' entries
297         $c->stash->{object}->delete;
298     
299         # Use 'flash' to save information across requests until it's read
300         $c->flash->{status_msg} = "Book deleted";
301     
302         # Redirect the user back to the list page
303         $c->response->redirect($c->uri_for($self->action_for('list')));
304     }
305
306 Here, we C<detach> to an error page if the user is lacking the 
307 appropriate permissions.  For this to work, we need to make 
308 arrangements for the '/error_noperms' action to work.  Open 
309 C<lib/MyApp/Controller/Root.pm> and add this method:
310
311     =head2 error_noperms
312     
313     Permissions error screen
314     
315     =cut
316         
317     sub error_noperms :Chained('/') :PathPart('error_noperms') :Args(0) {
318         my ($self, $c) = @_;
319     
320         $c->stash->{template} = 'error_noperms.tt2';
321     }
322
323 And also add the template file by putting the following text into
324 C<root/src/error_noperms.tt2>:
325
326     <span class="error">Permission Denied</span>
327
328 Then run the Catalyst development server script:
329
330     $ script/myapp_server.pl
331
332 Log in as C<test01> and create several new books using the C<url_create>
333 feature:
334
335     http://localhost:3000/books/url_create/Test/1/4
336
337 Then, while still logged in as C<test01>, click the "Delete" link next 
338 to one of these books.  The book should be removed and you should see 
339 the usual green "Book deleted" message.  Next, click the "User Logout" 
340 link and log back in as C<test02>.  Now try deleting one of the books. 
341 You should be taken to the red "Permission Denied" message on our 
342 error page.
343
344 Use one of the 'Logout' links (or go to the
345 L<http://localhost:3000/logout> URL directly) when you are done.
346
347
348 =head1 AUTHOR
349
350 Kennedy Clark, C<hkclark@gmail.com>
351
352 Please report any errors, issues or suggestions to the author.  The
353 most recent version of the Catalyst Tutorial can be found at
354 L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.80/trunk/lib/Catalyst/Manual/Tutorial/>.
355
356 Copyright 2006-2008, Kennedy Clark, under Creative Commons License
357 (L<http://creativecommons.org/licenses/by-sa/3.0/us/>).
358