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