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