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 | |
59 | This part of the tutorial adds role-based authorization to the existing |
9ad715b3 |
60 | authentication implemented in Part 5. It provides simple examples of |
d442cc9f |
61 | how to use roles in both TT templates and controller actions. The first |
62 | half looks at manually configured authorization. The second half looks |
63 | at how the ACL authorization plugin can simplify your code. |
64 | |
65 | You can checkout the source code for this example from the catalyst |
66 | subversion repository as per the instructions in |
1390ef0e |
67 | L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>. |
68 | |
d442cc9f |
69 | |
70 | =head1 BASIC AUTHORIZATION |
71 | |
72 | In this section you learn how to manually configure authorization. |
73 | |
1390ef0e |
74 | |
d442cc9f |
75 | =head2 Update Plugins to Include Support for Authorization |
76 | |
77 | Edit C<lib/MyApp.pm> and add C<Authorization::Roles> to the list: |
78 | |
1390ef0e |
79 | __PACKAGE__->setup(qw/ |
d442cc9f |
80 | -Debug |
81 | ConfigLoader |
82 | Static::Simple |
1390ef0e |
83 | |
d442cc9f |
84 | StackTrace |
1390ef0e |
85 | |
d442cc9f |
86 | Authentication |
d442cc9f |
87 | Authorization::Roles |
1390ef0e |
88 | |
d442cc9f |
89 | Session |
90 | Session::Store::FastMmap |
91 | Session::State::Cookie |
1390ef0e |
92 | /; |
d442cc9f |
93 | |
94d8da41 |
94 | B<Note:> As discussed in MoreCatalystBasics, different versions of |
95 | C<Catalyst::Devel> have used a variety of methods to load the plugins. |
533fee73 |
96 | You can put the plugins in the C<use Catalyst> statement if you prefer. |
94d8da41 |
97 | |
d442cc9f |
98 | |
99 | =head2 Add Config Information for Authorization |
100 | |
905a3a26 |
101 | Edit C<myapp.conf> and update it to match the following (the |
3533daff |
102 | C<role_relation> and C<role_field> definitions are new): |
d442cc9f |
103 | |
1390ef0e |
104 | # rename this file to MyApp.yml and put a : in front of "name" if |
105 | # you want to use yaml like in old versions of Catalyst |
c010ae0d |
106 | name MyApp |
107 | <authentication> |
108 | default_realm dbic |
109 | <realms> |
110 | <dbic> |
111 | <credential> |
3533daff |
112 | # Note this first definition would be the same as setting |
113 | # __PACKAGE__->config->{authentication}->{realms}->{dbic} |
905a3a26 |
114 | # ->{credential} = 'Password' in lib/MyApp.pm |
3533daff |
115 | # |
116 | # Specify that we are going to do password-based auth |
c010ae0d |
117 | class Password |
3533daff |
118 | # This is the name of the field in the users table with the |
119 | # password stored in it |
c010ae0d |
120 | password_field password |
d0496197 |
121 | # Switch to more secure hashed passwords |
122 | password_type hashed |
123 | # Use the SHA-1 hashing algorithm |
124 | password_hash_type SHA-1 |
125 | </credential> |
c010ae0d |
126 | <store> |
3533daff |
127 | # Use DBIC to retrieve username, password & role information |
c010ae0d |
128 | class DBIx::Class |
905a3a26 |
129 | # This is the model object created by Catalyst::Model::DBIC |
d0496197 |
130 | # from your schema (you created 'MyApp::Schema::User' but as |
905a3a26 |
131 | # the Catalyst startup debug messages show, it was loaded as |
d0496197 |
132 | # 'MyApp::Model::DB::Users'). |
905a3a26 |
133 | # NOTE: Omit 'MyApp::Model' here just as you would when using |
d0496197 |
134 | # '$c->model("DB::Users)' |
135 | user_class DB::Users |
3533daff |
136 | # This is the name of a many_to_many relation in the users |
137 | # object that points to the roles for that user |
c010ae0d |
138 | role_relation roles |
3533daff |
139 | # This is the name of field in the roles table that contains |
140 | # the role information |
c010ae0d |
141 | role_field role |
d0496197 |
142 | </store> |
143 | </dbic> |
144 | </realms> |
145 | </authentication> |
d442cc9f |
146 | |
147 | |
148 | =head2 Add Role-Specific Logic to the "Book List" Template |
149 | |
150 | Open C<root/src/books/list.tt2> in your editor and add the following |
151 | lines to the bottom of the file: |
152 | |
8a7c5151 |
153 | <p>Hello [% c.user.username %], you have the following roles:</p> |
1390ef0e |
154 | |
d442cc9f |
155 | <ul> |
156 | [% # Dump list of roles -%] |
8a7c5151 |
157 | [% FOR role = c.user.roles %]<li>[% role %]</li>[% END %] |
d442cc9f |
158 | </ul> |
1390ef0e |
159 | |
d442cc9f |
160 | <p> |
161 | [% # Add some simple role-specific logic to template %] |
162 | [% # Use $c->check_user_roles() to check authz -%] |
8a7c5151 |
163 | [% IF c.check_user_roles('user') %] |
d442cc9f |
164 | [% # Give normal users a link for 'logout' %] |
8a7c5151 |
165 | <a href="[% c.uri_for('/logout') %]">Logout</a> |
d442cc9f |
166 | [% END %] |
1390ef0e |
167 | |
d442cc9f |
168 | [% # Can also use $c->user->check_roles() to check authz -%] |
8a7c5151 |
169 | [% IF c.check_user_roles('admin') %] |
d442cc9f |
170 | [% # Give admin users a link for 'create' %] |
8a7c5151 |
171 | <a href="[% c.uri_for('form_create') %]">Create</a> |
d442cc9f |
172 | [% END %] |
173 | </p> |
174 | |
175 | This code displays a different combination of links depending on the |
176 | roles assigned to the user. |
177 | |
1390ef0e |
178 | |
d442cc9f |
179 | =head2 Limit C<Books::add> to C<admin> Users |
180 | |
181 | C<IF> statements in TT templates simply control the output that is sent |
182 | to the user's browser; it provides no real enforcement (if users know or |
183 | guess the appropriate URLs, they are still perfectly free to hit any |
184 | action within your application). We need to enhance the controller |
185 | logic to wrap restricted actions with role-validation logic. |
186 | |
187 | For example, we might want to restrict the "formless create" action to |
188 | admin-level users by editing C<lib/MyApp/Controller/Books.pm> and |
189 | updating C<url_create> to match the following code: |
190 | |
191 | =head2 url_create |
1390ef0e |
192 | |
d442cc9f |
193 | Create a book with the supplied title and rating, |
194 | with manual authorization |
1390ef0e |
195 | |
d442cc9f |
196 | =cut |
1390ef0e |
197 | |
d442cc9f |
198 | sub url_create : Local { |
199 | # In addition to self & context, get the title, rating & author_id args |
200 | # from the URL. Note that Catalyst automatically puts extra information |
201 | # after the "/<controller_name>/<action_name/" into @_ |
202 | my ($self, $c, $title, $rating, $author_id) = @_; |
1390ef0e |
203 | |
d442cc9f |
204 | # Check the user's roles |
205 | if ($c->check_user_roles('admin')) { |
905a3a26 |
206 | # Call create() on the book model object. Pass the table |
d442cc9f |
207 | # columns/field values we want to set as hash values |
d0496197 |
208 | my $book = $c->model('DB::Books')->create({ |
d442cc9f |
209 | title => $title, |
210 | rating => $rating |
211 | }); |
1390ef0e |
212 | |
905a3a26 |
213 | # Add a record to the join table for this book, mapping to |
d442cc9f |
214 | # appropriate author |
215 | $book->add_to_book_authors({author_id => $author_id}); |
216 | # Note: Above is a shortcut for this: |
217 | # $book->create_related('book_authors', {author_id => $author_id}); |
1390ef0e |
218 | |
d442cc9f |
219 | # Assign the Book object to the stash for display in the view |
220 | $c->stash->{book} = $book; |
1390ef0e |
221 | |
d442cc9f |
222 | # This is a hack to disable XSUB processing in Data::Dumper |
223 | # (it's used in the view). This is a work-around for a bug in |
224 | # the interaction of some versions or Perl, Data::Dumper & DBIC. |
225 | # You won't need this if you aren't using Data::Dumper (or if |
905a3a26 |
226 | # you are running DBIC 0.06001 or greater), but adding it doesn't |
d442cc9f |
227 | # hurt anything either. |
228 | $Data::Dumper::Useperl = 1; |
1390ef0e |
229 | |
d442cc9f |
230 | # Set the TT template to use |
231 | $c->stash->{template} = 'books/create_done.tt2'; |
232 | } else { |
233 | # Provide very simple feedback to the user |
234 | $c->response->body('Unauthorized!'); |
235 | } |
236 | } |
237 | |
238 | |
239 | To add authorization, we simply wrap the main code of this method in an |
240 | C<if> statement that calls C<check_user_roles>. If the user does not |
241 | have the appropriate permissions, they receive an "Unauthorized!" |
242 | message. Note that we intentionally chose to display the message this |
243 | way to demonstrate that TT templates will not be used if the response |
244 | body has already been set. In reality you would probably want to use a |
245 | technique that maintains the visual continuity of your template layout |
246 | (for example, using the "status" or "error" message feature added in |
9ad715b3 |
247 | Part 3). |
d442cc9f |
248 | |
249 | B<TIP>: If you want to keep your existing C<url_create> method, you can |
250 | create a new copy and comment out the original by making it look like a |
251 | Pod comment. For example, put something like C<=begin> before C<sub add |
252 | : Local {> and C<=end> after the closing C<}>. |
253 | |
1390ef0e |
254 | |
d442cc9f |
255 | =head2 Try Out Authentication And Authorization |
256 | |
257 | Press C<Ctrl-C> to kill the previous server instance (if it's still |
258 | running) and restart it: |
259 | |
260 | $ script/myapp_server.pl |
261 | |
1390ef0e |
262 | Now trying going to L<http://localhost:3000/books/list> and you should |
263 | be taken to the login page (you might have to C<Shift+Reload> or |
264 | C<Ctrl+Reload> your browser and/or click the "Logout" link on the book |
265 | list page). Try logging in with both C<test01> and C<test02> (both |
266 | use a password of C<mypass>) and notice how the roles information |
267 | updates at the bottom of the "Book List" page. Also try the C<Logout> |
268 | link on the book list page. |
d442cc9f |
269 | |
270 | Now the "url_create" URL will work if you are already logged in as user |
271 | C<test01>, but receive an authorization failure if you are logged in as |
272 | C<test02>. Try: |
273 | |
274 | http://localhost:3000/books/url_create/test/1/6 |
275 | |
1390ef0e |
276 | while logged in as each user. Use one of the 'Logout' links (or go to |
277 | L<http://localhost:3000/logout> in your browser directly) when you are |
d442cc9f |
278 | done. |
279 | |
280 | |
281 | =head1 ENABLE ACL-BASED AUTHORIZATION |
282 | |
283 | This section takes a brief look at how the |
284 | L<Catalyst::Plugin::Authorization::ACL|Catalyst::Plugin::Authorization::ACL> |
905a3a26 |
285 | plugin can automate much of the work required to perform role-based |
d442cc9f |
286 | authorization in a Catalyst application. |
287 | |
1390ef0e |
288 | |
d442cc9f |
289 | =head2 Add the C<Catalyst::Plugin::Authorization::ACL> Plugin |
290 | |
291 | Open C<lib/MyApp.pm> in your editor and add the following plugin to the |
1390ef0e |
292 | C<__PACKAGE__-E<gt>setup> statement: |
d442cc9f |
293 | |
294 | Authorization::ACL |
295 | |
296 | Note that the remaining C<use Catalyst> plugins from earlier sections |
297 | are not shown here, but they should still be included. |
298 | |
1390ef0e |
299 | |
d442cc9f |
300 | =head2 Add ACL Rules to the Application Class |
301 | |
302 | Open C<lib/MyApp.pm> in your editor and add the following B<BELOW> the |
1390ef0e |
303 | C<__PACKAGE__-E<gt>setup> statement: |
d442cc9f |
304 | |
305 | # Authorization::ACL Rules |
306 | __PACKAGE__->deny_access_unless( |
307 | "/books/form_create", |
308 | [qw/admin/], |
309 | ); |
310 | __PACKAGE__->deny_access_unless( |
311 | "/books/form_create_do", |
312 | [qw/admin/], |
313 | ); |
314 | __PACKAGE__->deny_access_unless( |
315 | "/books/delete", |
316 | [qw/user admin/], |
317 | ); |
318 | |
319 | Each of the three statements above comprises an ACL plugin "rule". The |
320 | first two rules only allow admin-level users to create new books using |
321 | the form (both the form itself and the data submission logic are |
322 | protected). The third statement allows both users and admins to delete |
323 | books. The C</books/url_create> action will continue to be protected by |
324 | the "manually configured" authorization created earlier in this part of |
325 | the tutorial. |
326 | |
327 | The ACL plugin permits you to apply allow/deny logic in a variety of |
328 | ways. The following provides a basic overview of the capabilities: |
329 | |
330 | =over 4 |
331 | |
905a3a26 |
332 | =item * |
d442cc9f |
333 | |
334 | The ACL plugin only operates on the Catalyst "private namespace". You |
335 | are using the private namespace when you use C<Local> actions. C<Path>, |
336 | C<Regex>, and C<Global> allow you to specify actions where the path and |
337 | the namespace differ -- the ACL plugin will not work in these cases. |
338 | |
905a3a26 |
339 | =item * |
d442cc9f |
340 | |
341 | Each rule is expressed in a separate |
342 | C<__PACKAGE__-E<gt>deny_access_unless()> or |
343 | C<__PACKAGE__-E<gt>allow_access_if()> line (there are several other |
344 | methods that can be used for more complex policies, see the C<METHODS> |
345 | portion of the |
346 | L<Catalyst::Plugin::Authorization::ACL|Catalyst::Plugin::Authorization::ACL> |
347 | documentation for more details). |
348 | |
905a3a26 |
349 | =item * |
d442cc9f |
350 | |
351 | Each rule can contain multiple roles but only a single path. |
352 | |
905a3a26 |
353 | =item * |
d442cc9f |
354 | |
355 | The rules are tried in order (with the "most specific" rules tested |
356 | first), and processing stops at the first "match" where an allow or deny |
357 | is specified. Rules "fall through" if there is not a "match" (where a |
358 | "match" means the user has the specified role). If a "match" is found, |
359 | then processing stops there and the appropriate allow/deny action is |
360 | taken. |
361 | |
905a3a26 |
362 | =item * |
d442cc9f |
363 | |
364 | If none of the rules match, then access is allowed. |
365 | |
905a3a26 |
366 | =item * |
d442cc9f |
367 | |
905a3a26 |
368 | The rules currently need to be specified in the application class |
d442cc9f |
369 | C<lib\MyApp.pm> B<after> the C<__PACKAGE__-E<gt>setup;> line. |
370 | |
371 | =back |
372 | |
1390ef0e |
373 | |
d442cc9f |
374 | =head2 Add a Method to Handle Access Violations |
375 | |
376 | By default, |
377 | L<Catalyst::Plugin::Authorization::ACL|Catalyst::Plugin::Authorization::ACL> |
378 | throws an exception when authorization fails. This will take the user |
379 | to the Catalyst debug screen, or a "Please come back later" message if |
380 | you are not using the C<-Debug> flag. This step uses the |
381 | C<access_denied> method in order to provide more appropriate feedback to |
382 | the user. |
383 | |
384 | Open C<lib/MyApp/Controller/Books.pm> in your editor and add the |
385 | following method: |
386 | |
387 | =head2 access_denied |
1390ef0e |
388 | |
d442cc9f |
389 | Handle Catalyst::Plugin::Authorization::ACL access denied exceptions |
1390ef0e |
390 | |
d442cc9f |
391 | =cut |
1390ef0e |
392 | |
d442cc9f |
393 | sub access_denied : Private { |
394 | my ($self, $c) = @_; |
1390ef0e |
395 | |
d442cc9f |
396 | # Set the error message |
397 | $c->stash->{error_msg} = 'Unauthorized!'; |
1390ef0e |
398 | |
d442cc9f |
399 | # Display the list |
400 | $c->forward('list'); |
401 | } |
402 | |
905a3a26 |
403 | Then run the Catalyst development server script: |
d442cc9f |
404 | |
405 | $ script/myapp_server.pl |
406 | |
3778bcbe |
407 | Log in as C<test02>. Once at the book list, click the "Create" link |
408 | to try the C<form_create> action. You should receive a red |
409 | "Unauthorized!" error message at the top of the list. (Note that in |
410 | the example code the "Create" link code in C<root/src/books/list.tt2> |
411 | is inside an C<IF> statement that only displays the list to |
412 | admin-level users.) If you log in as C<test01> you should be able to |
413 | view the C<form_create> form and add a new book. |
d442cc9f |
414 | |
415 | When you are done, use one of the 'Logout' links (or go to the |
416 | L<http://localhost:3000/logout> URL directly) when you are done. |
417 | |
418 | |
419 | =head1 AUTHOR |
420 | |
421 | Kennedy Clark, C<hkclark@gmail.com> |
422 | |
423 | Please report any errors, issues or suggestions to the author. The |
424 | most recent version of the Catalyst Tutorial can be found at |
82ab4bbf |
425 | L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>. |
d442cc9f |
426 | |
45c7830f |
427 | Copyright 2006-2008, Kennedy Clark, under Creative Commons License |
95674086 |
428 | (L<http://creativecommons.org/licenses/by-sa/3.0/us/>). |
d442cc9f |
429 | |