Commit | Line | Data |
d442cc9f |
1 | =head1 NAME |
2 | |
3533daff |
3 | Catalyst::Manual::Tutorial::Authorization - Catalyst Tutorial - Part 6: Authorization |
d442cc9f |
4 | |
5 | |
6 | =head1 OVERVIEW |
7 | |
3533daff |
8 | This is B<Part 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 | |
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 | |
3533daff |
24 | L<More Catalyst Basics|Catalyst::Manual::Tutorial::MoreCatalystBasics> |
d442cc9f |
25 | |
26 | =item 4 |
27 | |
3533daff |
28 | L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD> |
d442cc9f |
29 | |
30 | =item 5 |
31 | |
3533daff |
32 | L<Authentication|Catalyst::Manual::Tutorial::Authentication> |
d442cc9f |
33 | |
34 | =item 6 |
35 | |
3533daff |
36 | B<Authorization> |
d442cc9f |
37 | |
38 | =item 7 |
39 | |
3533daff |
40 | L<Debugging|Catalyst::Manual::Tutorial::Debugging> |
d442cc9f |
41 | |
42 | =item 8 |
43 | |
3533daff |
44 | L<Testing|Catalyst::Manual::Tutorial::Testing> |
d442cc9f |
45 | |
46 | =item 9 |
47 | |
3533daff |
48 | L<Advanced CRUD|Catalyst::Manual::Tutorial::AdvancedCRUD> |
49 | |
50 | =item 10 |
51 | |
d442cc9f |
52 | L<Appendices|Catalyst::Manual::Tutorial::Appendices> |
53 | |
54 | =back |
55 | |
56 | |
d442cc9f |
57 | =head1 DESCRIPTION |
58 | |
acbd7bdd |
59 | This part of the tutorial adds role-based authorization to the |
60 | existing authentication implemented in Part 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. |
d442cc9f |
65 | |
66 | You can checkout the source code for this example from the catalyst |
67 | subversion repository as per the instructions in |
1390ef0e |
68 | L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>. |
69 | |
d442cc9f |
70 | |
71 | =head1 BASIC AUTHORIZATION |
72 | |
acbd7bdd |
73 | In this section you learn the basics of how authorization works under |
74 | Catalyst. |
d442cc9f |
75 | |
1390ef0e |
76 | |
d442cc9f |
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 | |
acbd7bdd |
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 | /; |
d442cc9f |
95 | |
94d8da41 |
96 | B<Note:> As discussed in MoreCatalystBasics, different versions of |
97 | C<Catalyst::Devel> have used a variety of methods to load the plugins. |
acbd7bdd |
98 | You can put the plugins in the C<use Catalyst> statement if you |
99 | prefer. |
94d8da41 |
100 | |
d442cc9f |
101 | |
102 | =head2 Add Config Information for Authorization |
103 | |
905a3a26 |
104 | Edit C<myapp.conf> and update it to match the following (the |
3533daff |
105 | C<role_relation> and C<role_field> definitions are new): |
d442cc9f |
106 | |
1390ef0e |
107 | # rename this file to MyApp.yml and put a : in front of "name" if |
108 | # you want to use yaml like in old versions of Catalyst |
c010ae0d |
109 | name MyApp |
110 | <authentication> |
111 | default_realm dbic |
112 | <realms> |
113 | <dbic> |
114 | <credential> |
3533daff |
115 | # Note this first definition would be the same as setting |
116 | # __PACKAGE__->config->{authentication}->{realms}->{dbic} |
905a3a26 |
117 | # ->{credential} = 'Password' in lib/MyApp.pm |
3533daff |
118 | # |
119 | # Specify that we are going to do password-based auth |
c010ae0d |
120 | class Password |
3533daff |
121 | # This is the name of the field in the users table with the |
122 | # password stored in it |
c010ae0d |
123 | password_field password |
d0496197 |
124 | # Switch to more secure hashed passwords |
125 | password_type hashed |
126 | # Use the SHA-1 hashing algorithm |
127 | password_hash_type SHA-1 |
128 | </credential> |
c010ae0d |
129 | <store> |
3533daff |
130 | # Use DBIC to retrieve username, password & role information |
c010ae0d |
131 | class DBIx::Class |
905a3a26 |
132 | # This is the model object created by Catalyst::Model::DBIC |
acbd7bdd |
133 | # from your schema (you created 'MyApp::Schema::Result::User' |
134 | # but as the Catalyst startup debug messages show, it was |
135 | # loaded as 'MyApp::Model::DB::Users'). |
905a3a26 |
136 | # NOTE: Omit 'MyApp::Model' here just as you would when using |
d0496197 |
137 | # '$c->model("DB::Users)' |
138 | user_class DB::Users |
3533daff |
139 | # This is the name of a many_to_many relation in the users |
140 | # object that points to the roles for that user |
c010ae0d |
141 | role_relation roles |
3533daff |
142 | # This is the name of field in the roles table that contains |
143 | # the role information |
c010ae0d |
144 | role_field role |
d0496197 |
145 | </store> |
146 | </dbic> |
147 | </realms> |
148 | </authentication> |
d442cc9f |
149 | |
150 | |
151 | =head2 Add Role-Specific Logic to the "Book List" Template |
152 | |
153 | Open C<root/src/books/list.tt2> in your editor and add the following |
154 | lines to the bottom of the file: |
155 | |
acbd7bdd |
156 | ... |
8a7c5151 |
157 | <p>Hello [% c.user.username %], you have the following roles:</p> |
1390ef0e |
158 | |
d442cc9f |
159 | <ul> |
160 | [% # Dump list of roles -%] |
8a7c5151 |
161 | [% FOR role = c.user.roles %]<li>[% role %]</li>[% END %] |
d442cc9f |
162 | </ul> |
1390ef0e |
163 | |
d442cc9f |
164 | <p> |
165 | [% # Add some simple role-specific logic to template %] |
166 | [% # Use $c->check_user_roles() to check authz -%] |
8a7c5151 |
167 | [% IF c.check_user_roles('user') %] |
d442cc9f |
168 | [% # Give normal users a link for 'logout' %] |
e075db0c |
169 | <a href="[% c.uri_for('/logout') %]">User Logout</a> |
d442cc9f |
170 | [% END %] |
1390ef0e |
171 | |
d442cc9f |
172 | [% # Can also use $c->user->check_roles() to check authz -%] |
8a7c5151 |
173 | [% IF c.check_user_roles('admin') %] |
d442cc9f |
174 | [% # Give admin users a link for 'create' %] |
0416017e |
175 | <a href="[% c.uri_for(c.controller.action_for('form_create')) %]">Admin Create</a> |
d442cc9f |
176 | [% END %] |
177 | </p> |
178 | |
179 | This code displays a different combination of links depending on the |
180 | roles assigned to the user. |
181 | |
1390ef0e |
182 | |
d442cc9f |
183 | =head2 Limit C<Books::add> to C<admin> Users |
184 | |
185 | C<IF> statements in TT templates simply control the output that is sent |
186 | to the user's browser; it provides no real enforcement (if users know or |
187 | guess the appropriate URLs, they are still perfectly free to hit any |
188 | action within your application). We need to enhance the controller |
189 | logic to wrap restricted actions with role-validation logic. |
190 | |
191 | For example, we might want to restrict the "formless create" action to |
192 | admin-level users by editing C<lib/MyApp/Controller/Books.pm> and |
193 | updating C<url_create> to match the following code: |
194 | |
195 | =head2 url_create |
1390ef0e |
196 | |
d442cc9f |
197 | Create a book with the supplied title and rating, |
198 | with manual authorization |
1390ef0e |
199 | |
d442cc9f |
200 | =cut |
1390ef0e |
201 | |
e075db0c |
202 | sub url_create :Chained('base') :PathPart('url_create') :Args(3) { |
d442cc9f |
203 | # In addition to self & context, get the title, rating & author_id args |
204 | # from the URL. Note that Catalyst automatically puts extra information |
205 | # after the "/<controller_name>/<action_name/" into @_ |
206 | my ($self, $c, $title, $rating, $author_id) = @_; |
1390ef0e |
207 | |
d442cc9f |
208 | # Check the user's roles |
209 | if ($c->check_user_roles('admin')) { |
905a3a26 |
210 | # Call create() on the book model object. Pass the table |
d442cc9f |
211 | # columns/field values we want to set as hash values |
d0496197 |
212 | my $book = $c->model('DB::Books')->create({ |
d442cc9f |
213 | title => $title, |
214 | rating => $rating |
215 | }); |
1390ef0e |
216 | |
905a3a26 |
217 | # Add a record to the join table for this book, mapping to |
d442cc9f |
218 | # appropriate author |
219 | $book->add_to_book_authors({author_id => $author_id}); |
220 | # Note: Above is a shortcut for this: |
221 | # $book->create_related('book_authors', {author_id => $author_id}); |
1390ef0e |
222 | |
d442cc9f |
223 | # Assign the Book object to the stash for display in the view |
224 | $c->stash->{book} = $book; |
1390ef0e |
225 | |
d442cc9f |
226 | # This is a hack to disable XSUB processing in Data::Dumper |
227 | # (it's used in the view). This is a work-around for a bug in |
228 | # the interaction of some versions or Perl, Data::Dumper & DBIC. |
229 | # You won't need this if you aren't using Data::Dumper (or if |
905a3a26 |
230 | # you are running DBIC 0.06001 or greater), but adding it doesn't |
d442cc9f |
231 | # hurt anything either. |
232 | $Data::Dumper::Useperl = 1; |
1390ef0e |
233 | |
d442cc9f |
234 | # Set the TT template to use |
235 | $c->stash->{template} = 'books/create_done.tt2'; |
236 | } else { |
e075db0c |
237 | # Provide very simple feedback to the user. |
d442cc9f |
238 | $c->response->body('Unauthorized!'); |
239 | } |
240 | } |
241 | |
242 | |
243 | To add authorization, we simply wrap the main code of this method in an |
244 | C<if> statement that calls C<check_user_roles>. If the user does not |
245 | have the appropriate permissions, they receive an "Unauthorized!" |
246 | message. Note that we intentionally chose to display the message this |
247 | way to demonstrate that TT templates will not be used if the response |
248 | body has already been set. In reality you would probably want to use a |
249 | technique that maintains the visual continuity of your template layout |
250 | (for example, using the "status" or "error" message feature added in |
e075db0c |
251 | Part 3 or C<detach> to an action that shows an "unauthorized" page). |
d442cc9f |
252 | |
253 | B<TIP>: If you want to keep your existing C<url_create> method, you can |
254 | create a new copy and comment out the original by making it look like a |
e075db0c |
255 | Pod comment. For example, put something like C<=begin> before |
256 | C<sub add : Local {> and C<=end> after the closing C<}>. |
d442cc9f |
257 | |
1390ef0e |
258 | |
d442cc9f |
259 | =head2 Try Out Authentication And Authorization |
260 | |
261 | Press C<Ctrl-C> to kill the previous server instance (if it's still |
262 | running) and restart it: |
263 | |
264 | $ script/myapp_server.pl |
265 | |
1390ef0e |
266 | Now trying going to L<http://localhost:3000/books/list> and you should |
267 | be taken to the login page (you might have to C<Shift+Reload> or |
fbbb9084 |
268 | C<Ctrl+Reload> your browser and/or click the "User Logout" link on the book |
1390ef0e |
269 | list page). Try logging in with both C<test01> and C<test02> (both |
270 | use a password of C<mypass>) and notice how the roles information |
fbbb9084 |
271 | updates at the bottom of the "Book List" page. Also try the "User Logout" |
1390ef0e |
272 | link on the book list page. |
d442cc9f |
273 | |
274 | Now the "url_create" URL will work if you are already logged in as user |
275 | C<test01>, but receive an authorization failure if you are logged in as |
276 | C<test02>. Try: |
277 | |
278 | http://localhost:3000/books/url_create/test/1/6 |
279 | |
fbbb9084 |
280 | while logged in as each user. Use one of the "logout" links (or go to |
1390ef0e |
281 | L<http://localhost:3000/logout> in your browser directly) when you are |
d442cc9f |
282 | done. |
283 | |
284 | |
acbd7bdd |
285 | =head1 ENABLE MODEL-BASED AUTHORIZATION |
d442cc9f |
286 | |
acbd7bdd |
287 | Hopefully it's fairly obvious that adding detailed permission checking |
288 | logic to our controllers and view templates isn't a very clean or |
289 | scalable way to build role-based permissions into out application. As |
290 | with many other aspects of MVC web development, the goal is to have |
291 | your controllers and views be an "thin" as possible, with all of the |
292 | "fancy business logic" built into your model. |
d442cc9f |
293 | |
acbd7bdd |
294 | For example, let's add a method to our C<Books.pm> Result Class to |
295 | check if a user is allowed to delete a book. Open |
296 | C<lib/MyApp/Schema/Result/Books.pm> and add the following method |
297 | (be sure to add it below the "C<DO NOT MODIFY ...>" line): |
d442cc9f |
298 | |
acbd7bdd |
299 | =head2 delete_allowed_by |
300 | |
301 | Can the specified user delete the current book? |
302 | |
303 | =cut |
304 | |
305 | sub delete_allowed_by { |
306 | my ($self, $user) = @_; |
307 | |
308 | # Only allow delete if user has 'admin' role |
309 | return $user->has_role('admin'); |
310 | } |
d442cc9f |
311 | |
acbd7bdd |
312 | Here we call a C<has_role> method on our user object, so we should add |
313 | this method to our Result Class. Open |
314 | C<lib/MyApp/Schema/Result/Users.pm> and add this near the top: |
d442cc9f |
315 | |
acbd7bdd |
316 | use Perl6::Junction qw/any/; |
1390ef0e |
317 | |
acbd7bdd |
318 | And then add the following method below the "C<DO NOT MODIFY ...>" |
319 | line: |
d442cc9f |
320 | |
acbd7bdd |
321 | =head 2 has_role |
322 | |
323 | Check if a user has the specified role |
324 | |
325 | =cut |
326 | |
327 | sub has_role { |
328 | my ($self, $role) = @_; |
329 | |
330 | # Does this user posses the required role? |
331 | return any(map { $_->role } $self->roles) eq $role; |
332 | } |
d442cc9f |
333 | |
acbd7bdd |
334 | Now we need to add some enforcement inside our controller. Open |
335 | C<lib/MyApp/Controller/Books.pm> and update the C<delete> method to |
336 | match the following code: |
d442cc9f |
337 | |
acbd7bdd |
338 | =head2 delete |
1390ef0e |
339 | |
acbd7bdd |
340 | Delete a book |
1390ef0e |
341 | |
d442cc9f |
342 | =cut |
1390ef0e |
343 | |
acbd7bdd |
344 | sub delete :Chained('object') :PathPart('delete') :Args(0) { |
d442cc9f |
345 | my ($self, $c) = @_; |
1390ef0e |
346 | |
acbd7bdd |
347 | # Check permissions |
348 | $c->detach('/error_noperms') |
349 | unless $c->stash->{object}->delete_allowed_by($c->user->get_object); |
350 | |
351 | # Use the book object saved by 'object' and delete it along |
352 | # with related 'book_authors' entries |
353 | $c->stash->{object}->delete; |
1390ef0e |
354 | |
acbd7bdd |
355 | # Use 'flash' to save information across requests until it's read |
356 | $c->flash->{status_msg} = "Book deleted"; |
357 | |
358 | # Redirect the user back to the list page |
359 | $c->response->redirect($c->uri_for($self->action_for('list'))); |
360 | } |
361 | |
362 | Here, we C<detach> to an error page if the user is lacking the |
363 | appropriate permissions. For this to work, we need to make |
364 | arrangements for the '/error_noperms' action to work. Open |
365 | C<lib/MyApp/Controller/Root.pm> and add this method: |
366 | |
367 | =head2 error_noperms |
368 | |
369 | Permissions error screen |
370 | |
371 | =cut |
372 | |
373 | sub error_noperms :Chained('/') :PathPath('error_noperms') :Args(0) { |
374 | my ($self, $c) = @_; |
375 | |
376 | $c->stash->{template} = 'error_noperms.tt2'; |
d442cc9f |
377 | } |
378 | |
acbd7bdd |
379 | And also add the template file by putting the following text into |
380 | C<root/src/error_noperms.tt2>: |
381 | |
382 | <span class="error">Permission Denied</span> |
383 | |
905a3a26 |
384 | Then run the Catalyst development server script: |
d442cc9f |
385 | |
386 | $ script/myapp_server.pl |
387 | |
acbd7bdd |
388 | Log in as C<test01> and create several new books using the C<url_create> |
389 | feature: |
390 | |
391 | http://localhost:3000/books/url_create/Test/1/4 |
392 | |
393 | Then, while still logged in as C<test01>, click the "Delete" link next |
394 | to one of these books. The book should be removed and you should see |
395 | the usual green "Book deleted" message. Next, click the "User Logout" |
396 | link and log back in as C<test02>. Now try deleting one of the books. |
397 | You should be taken to the red "Permission Denied" message on our |
398 | error page. |
d442cc9f |
399 | |
4feda61c |
400 | Use one of the 'Logout' links (or go to the |
d442cc9f |
401 | L<http://localhost:3000/logout> URL directly) when you are done. |
402 | |
403 | |
404 | =head1 AUTHOR |
405 | |
406 | Kennedy Clark, C<hkclark@gmail.com> |
407 | |
408 | Please report any errors, issues or suggestions to the author. The |
409 | most recent version of the Catalyst Tutorial can be found at |
82ab4bbf |
410 | L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>. |
d442cc9f |
411 | |
45c7830f |
412 | Copyright 2006-2008, Kennedy Clark, under Creative Commons License |
95674086 |
413 | (L<http://creativecommons.org/licenses/by-sa/3.0/us/>). |
d442cc9f |
414 | |