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