Merge from depluralization branch
[catagits/Catalyst-Manual.git] / lib / Catalyst / Manual / Tutorial / Authorization.pod
index 02d7358..fce6161 100644 (file)
@@ -1,11 +1,11 @@
 =head1 NAME
 
-Catalyst::Manual::Tutorial::Authorization - Catalyst Tutorial - Part 6: Authorization
+Catalyst::Manual::Tutorial::Authorization - Catalyst Tutorial - Chapter 6: Authorization
 
 
 =head1 OVERVIEW
 
-This is B<Part 6 of 10> for the Catalyst tutorial.
+This is B<Chapter 6 of 10> for the Catalyst tutorial.
 
 L<Tutorial Overview|Catalyst::Manual::Tutorial>
 
@@ -56,121 +56,90 @@ L<Appendices|Catalyst::Manual::Tutorial::Appendices>
 
 =head1 DESCRIPTION
 
-This part of the tutorial adds role-based authorization to the existing
-authentication implemented in Part 4.  It provides simple examples of
-how to use roles in both TT templates and controller actions.  The first
-half looks at manually configured authorization.  The second half looks
-at how the ACL authorization plugin can simplify your code.
+This chapter of the tutorial adds role-based authorization to the 
+existing authentication implemented in Chapter 5.  It provides simple 
+examples of how to use roles in both TT templates and controller 
+actions.  The first half looks at basic authorization concepts. The 
+second half looks at how moving your authorization code to your model 
+can simplify your code and make things easier to maintain.
 
 You can checkout the source code for this example from the catalyst
 subversion repository as per the instructions in
-L<Catalyst::Manual::Tutorial::Intro>
+L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>.
+
 
 =head1 BASIC AUTHORIZATION
 
-In this section you learn how to manually configure authorization.
+In this section you learn the basics of how authorization works under 
+Catalyst.
+
 
 =head2 Update Plugins to Include Support for Authorization
 
 Edit C<lib/MyApp.pm> and add C<Authorization::Roles> to the list:
 
-    use Catalyst qw/
-            -Debug
-            ConfigLoader
-            Static::Simple
-            
-            StackTrace
-            
-            Authentication
-            Authorization::Roles
-            
-            Session
-            Session::Store::FastMmap
-            Session::State::Cookie
-            /;
-
-
-=head2 Add Config Information for Authorization
-
-Edit C<myapp.conf> and update it to match the following (the 
-C<role_relation> and C<role_field> definitions are new):
-
-    name MyApp
-    <authentication>
-        default_realm dbic
-        <realms>
-            <dbic>
-                <credential>
-                    # Note this first definition would be the same as setting
-                    # __PACKAGE__->config->{authentication}->{realms}->{dbic}
-                    #     ->{credential} = 'Password' in lib/MyApp.pm 
-                    #
-                    # Specify that we are going to do password-based auth
-                    class Password
-                    # This is the name of the field in the users table with the
-                    # password stored in it
-                    password_field password
-                    # Switch to more secure hashed passwords
-                    password_type  hashed
-                    # Use the SHA-1 hashing algorithm
-                    password_hash_type SHA-1
-                </credential>
-                <store>
-                    # Use DBIC to retrieve username, password & role information
-                    class DBIx::Class
-                    # This is the model object created by Catalyst::Model::DBIC 
-                    # from your schema (you created 'MyApp::Schema::User' but as
-                    # the Catalyst startup debug messages show, it was loaded as 
-                    # 'MyApp::Model::DB::Users').
-                    # NOTE: Omit 'MyApp::Model' here just as you would when using 
-                    # '$c->model("DB::Users)'
-                    user_class DB::Users
-                    # This is the name of the field in your 'users' table that 
-                    # contains the user's name
-                    id_field username
-                    # This is the name of a many_to_many relation in the users
-                    # object that points to the roles for that user
-                    role_relation  roles
-                    # This is the name of field in the roles table that contains
-                    # the role information
-                    role_field role
-                </store>
-            </dbic>
-        </realms>
-    </authentication>
+    # Load plugins
+    use Catalyst qw/-Debug
+                    ConfigLoader
+                    Static::Simple
+                
+                    StackTrace
+                
+                    Authentication
+                    Authorization::Roles
+        
+                    Session
+                    Session::Store::FastMmap
+                    Session::State::Cookie
+                    /;
+
+B<Note:> As discussed in MoreCatalystBasics, different versions of 
+C<Catalyst::Devel> have used a variety of methods to load the plugins. 
+You can put the plugins in the C<use Catalyst> statement if you 
+prefer.
 
+Once again (remain sharp, by now you should be getting the hang of things)
+include this additional plugin as a new dependency in the Makefile.PL file
+like this:
+
+    requires (
+        ...
+        'Catalyst::Plugin::Authorization::Roles' => '0',
+    );
 
 =head2 Add Role-Specific Logic to the "Book List" Template
 
 Open C<root/src/books/list.tt2> in your editor and add the following
 lines to the bottom of the file:
 
-    <p>Hello [% Catalyst.user.username %], you have the following roles:</p>
+    ...
+    <p>Hello [% c.user.username %], you have the following roles:</p>
     
     <ul>
       [% # Dump list of roles -%]
-      [% FOR role = Catalyst.user.roles %]<li>[% role %]</li>[% END %]
+      [% FOR role = c.user.role %]<li>[% role %]</li>[% END %]
     </ul>
     
     <p>
     [% # Add some simple role-specific logic to template %]
     [% # Use $c->check_user_roles() to check authz -%]
-    [% IF Catalyst.check_user_roles('user') %]
+    [% IF c.check_user_roles('user') %]
       [% # Give normal users a link for 'logout' %]
-      <a href="[% Catalyst.uri_for('/logout') %]">Logout</a>
+      <a href="[% c.uri_for('/logout') %]">User Logout</a>
     [% END %]
     
     [% # Can also use $c->user->check_roles() to check authz -%]
-    [% IF Catalyst.check_user_roles('admin') %]
+    [% IF c.check_user_roles('admin') %]
       [% # Give admin users a link for 'create' %]
-      <a href="[% Catalyst.uri_for('form_create') %]">Create</a>
+      <a href="[% c.uri_for(c.controller.action_for('form_create')) %]">Admin Create</a>
     [% END %]
     </p>
 
 This code displays a different combination of links depending on the
 roles assigned to the user.
 
-=head2 Limit C<Books::add> to C<admin> Users
+
+=head2 Limit Books::add to 'admin' Users
 
 C<IF> statements in TT templates simply control the output that is sent
 to the user's browser; it provides no real enforcement (if users know or
@@ -183,13 +152,13 @@ admin-level users by editing C<lib/MyApp/Controller/Books.pm> and
 updating C<url_create> to match the following code:
 
     =head2 url_create
-
+    
     Create a book with the supplied title and rating,
     with manual authorization
     
     =cut
     
-    sub url_create : Local {
+    sub url_create :Chained('base') :PathPart('url_create') :Args(3) {
         # In addition to self & context, get the title, rating & author_id args
         # from the URL.  Note that Catalyst automatically puts extra information
         # after the "/<controller_name>/<action_name/" into @_
@@ -197,34 +166,26 @@ updating C<url_create> to match the following code:
     
         # Check the user's roles
         if ($c->check_user_roles('admin')) {
-            # Call create() on the book model object. Pass the table 
+            # Call create() on the book model object. Pass the table
             # columns/field values we want to set as hash values
-            my $book = $c->model('DB::Books')->create({
+            my $book = $c->model('DB::Book')->create({
                     title   => $title,
                     rating  => $rating
                 });
-            
-            # Add a record to the join table for this book, mapping to 
+    
+            # Add a record to the join table for this book, mapping to
             # appropriate author
-            $book->add_to_book_authors({author_id => $author_id});
+            $book->add_to_book_author({author_id => $author_id});
             # Note: Above is a shortcut for this:
-            # $book->create_related('book_authors', {author_id => $author_id});
-            
+            # $book->create_related('book_author', {author_id => $author_id});
+    
             # Assign the Book object to the stash for display in the view
             $c->stash->{book} = $book;
-        
-            # This is a hack to disable XSUB processing in Data::Dumper
-            # (it's used in the view).  This is a work-around for a bug in
-            # the interaction of some versions or Perl, Data::Dumper & DBIC.
-            # You won't need this if you aren't using Data::Dumper (or if
-            # you are running DBIC 0.06001 or greater), but adding it doesn't 
-            # hurt anything either.
-            $Data::Dumper::Useperl = 1;
-        
+    
             # Set the TT template to use
             $c->stash->{template} = 'books/create_done.tt2';
         } else {
-            # Provide very simple feedback to the user
+            # Provide very simple feedback to the user.
             $c->response->body('Unauthorized!');
         }
     }
@@ -238,12 +199,13 @@ way to demonstrate that TT templates will not be used if the response
 body has already been set.  In reality you would probably want to use a
 technique that maintains the visual continuity of your template layout
 (for example, using the "status" or "error" message feature added in
-Part 2).
+Chapter 3 or C<detach> to an action that shows an "unauthorized" page).
 
 B<TIP>: If you want to keep your existing C<url_create> method, you can
 create a new copy and comment out the original by making it look like a
-Pod comment.  For example, put something like C<=begin> before C<sub add
-: Local {> and C<=end> after the closing C<}>.
+Pod comment.  For example, put something like C<=begin> before 
+C<sub add : Local {> and C<=end> after the closing C<}>.
+
 
 =head2 Try Out Authentication And Authorization
 
@@ -252,13 +214,13 @@ running) and restart it:
 
     $ script/myapp_server.pl
 
-Now trying going to L<http://localhost:3000/books/list> and you should
-be taken to the login page (you might have to C<Shift+Reload> your
-browser and/or click the "Logout" link on the book list page).  Try 
-logging in with both C<test01> and C<test02> (both use a password 
-of C<mypass>) and notice how the roles information updates at the 
-bottom of the "Book List" page. Also try the C<Logout> link on the
-book list page.
+Now trying going to L<http://localhost:3000/books/list> and you should 
+be taken to the login page (you might have to C<Shift+Reload> or 
+C<Ctrl+Reload> your browser and/or click the "User Logout" link on the book 
+list page).  Try logging in with both C<test01> and C<test02> (both 
+use a password of C<mypass>) and notice how the roles information 
+updates at the bottom of the "Book List" page. Also try the "User Logout"
+link on the book list page.
 
 Now the "url_create" URL will work if you are already logged in as user
 C<test01>, but receive an authorization failure if you are logged in as
@@ -266,143 +228,124 @@ C<test02>.  Try:
 
     http://localhost:3000/books/url_create/test/1/6
 
-while logged in as each user.  Use one of the 'Logout' links (or go to
-L<http://localhost:3000/logout> in you browser directly) when you are
+while logged in as each user.  Use one of the "logout" links (or go to 
+L<http://localhost:3000/logout> in your browser directly) when you are 
 done.
 
 
-=head1 ENABLE ACL-BASED AUTHORIZATION
-
-This section takes a brief look at how the
-L<Catalyst::Plugin::Authorization::ACL|Catalyst::Plugin::Authorization::ACL>
-plugin can automate much of the work required to perform role-based 
-authorization in a Catalyst application.
-
-=head2 Add the C<Catalyst::Plugin::Authorization::ACL> Plugin
-
-Open C<lib/MyApp.pm> in your editor and add the following plugin to the
-C<use Catalyst> statement:
-
-    Authorization::ACL
-
-Note that the remaining C<use Catalyst> plugins from earlier sections
-are not shown here, but they should still be included.
-
-=head2 Add ACL Rules to the Application Class
-
-Open C<lib/MyApp.pm> in your editor and add the following B<BELOW> the
-C<__PACKAGE__-E<gt>setup;> statement:
-
-    # Authorization::ACL Rules
-    __PACKAGE__->deny_access_unless(
-            "/books/form_create",
-            [qw/admin/],
-        );
-    __PACKAGE__->deny_access_unless(
-            "/books/form_create_do",
-            [qw/admin/],
-        );
-    __PACKAGE__->deny_access_unless(
-            "/books/delete",
-            [qw/user admin/],
-        );
-
-Each of the three statements above comprises an ACL plugin "rule".  The
-first two rules only allow admin-level users to create new books using
-the form (both the form itself and the data submission logic are
-protected).  The third statement allows both users and admins to delete
-books.  The C</books/url_create> action will continue to be protected by
-the "manually configured" authorization created earlier in this part of
-the tutorial.
-
-The ACL plugin permits you to apply allow/deny logic in a variety of
-ways.  The following provides a basic overview of the capabilities:
-
-=over 4
-
-=item * 
-
-The ACL plugin only operates on the Catalyst "private namespace".  You
-are using the private namespace when you use C<Local> actions.  C<Path>,
-C<Regex>, and C<Global> allow you to specify actions where the path and
-the namespace differ -- the ACL plugin will not work in these cases.
-
-=item * 
-
-Each rule is expressed in a separate
-C<__PACKAGE__-E<gt>deny_access_unless()> or
-C<__PACKAGE__-E<gt>allow_access_if()> line (there are several other
-methods that can be used for more complex policies, see the C<METHODS>
-portion of the
-L<Catalyst::Plugin::Authorization::ACL|Catalyst::Plugin::Authorization::ACL>
-documentation for more details).
-
-=item * 
-
-Each rule can contain multiple roles but only a single path.
+=head1 ENABLE MODEL-BASED AUTHORIZATION
 
-=item * 
+Hopefully it's fairly obvious that adding detailed permission checking 
+logic to our controllers and view templates isn't a very clean or 
+scalable way to build role-based permissions into out application.  As 
+with many other aspects of MVC web development, the goal is to have 
+your controllers and views be an "thin" as possible, with all of the 
+"fancy business logic" built into your model.
 
-The rules are tried in order (with the "most specific" rules tested
-first), and processing stops at the first "match" where an allow or deny
-is specified.  Rules "fall through" if there is not a "match" (where a
-"match" means the user has the specified role).  If a "match" is found,
-then processing stops there and the appropriate allow/deny action is
-taken.
+For example, let's add a method to our C<Books.pm> Result Class to 
+check if a user is allowed to delete a book.  Open 
+C<lib/MyApp/Schema/Result/Book.pm> and add the following method 
+(be sure to add it below the "C<DO NOT MODIFY ...>" line):
 
-=item * 
-
-If none of the rules match, then access is allowed.
-
-=item * 
-
-The rules currently need to be specific in the application class
-C<lib\MyApp.pm> B<after> the C<__PACKAGE__-E<gt>setup;> line.
-
-=back
+    =head2 delete_allowed_by
+    
+    Can the specified user delete the current book?
+    
+    =cut
+    
+    sub delete_allowed_by {
+        my ($self, $user) = @_;
+        
+        # Only allow delete if user has 'admin' role
+        return $user->has_role('admin');
+    }
 
-=head2 Add a Method to Handle Access Violations
+Here we call a C<has_role> method on our user object, so we should add 
+this method to our Result Class.  Open 
+C<lib/MyApp/Schema/Result/User.pm> and add the following method below 
+the "C<DO NOT MODIFY ...>" line:
 
-By default,
-L<Catalyst::Plugin::Authorization::ACL|Catalyst::Plugin::Authorization::ACL>
-throws an exception when authorization fails.  This will take the user
-to the Catalyst debug screen, or a "Please come back later" message if
-you are not using the C<-Debug> flag. This step uses the
-C<access_denied> method in order to provide more appropriate feedback to
-the user.
+    =head 2 has_role
+    
+    Check if a user has the specified role
+    
+    =cut
+    
+    use Perl6::Junction qw/any/;
+    sub has_role {
+        my ($self, $role) = @_;
+    
+        # Does this user posses the required role?
+        return any(map { $_->role } $self->roles) eq $role;
+    }
 
-Open C<lib/MyApp/Controller/Books.pm> in your editor and add the
-following method:
+Now we need to add some enforcement inside our controller.  Open
+C<lib/MyApp/Controller/Books.pm> and update the C<delete> method to
+match the following code:
 
-    =head2 access_denied
+    =head2 delete
     
-    Handle Catalyst::Plugin::Authorization::ACL access denied exceptions
+    Delete a book
     
     =cut
     
-    sub access_denied : Private {
+    sub delete :Chained('object') :PathPart('delete') :Args(0) {
         my ($self, $c) = @_;
     
-        # Set the error message
-        $c->stash->{error_msg} = 'Unauthorized!';
+        # Check permissions
+        $c->detach('/error_noperms')
+            unless $c->stash->{object}->delete_allowed_by($c->user->get_object);
+    
+        # Use the book object saved by 'object' and delete it along
+        # with related 'book_authors' entries
+        $c->stash->{object}->delete;
+    
+        # Use 'flash' to save information across requests until it's read
+        $c->flash->{status_msg} = "Book deleted";
+    
+        # Redirect the user back to the list page
+        $c->response->redirect($c->uri_for($self->action_for('list')));
+    }    
+
+Here, we C<detach> to an error page if the user is lacking the 
+appropriate permissions.  For this to work, we need to make 
+arrangements for the '/error_noperms' action to work.  Open 
+C<lib/MyApp/Controller/Root.pm> and add this method:
+
+    =head2 error_noperms
     
-        # Display the list
-        $c->forward('list');
+    Permissions error screen
+    
+    =cut
+        
+    sub error_noperms :Chained('/') :PathPath('error_noperms') :Args(0) {
+        my ($self, $c) = @_;
+    
+        $c->stash->{template} = 'error_noperms.tt2';
     }
 
-Then run the Catalyst development server script:    
+And also add the template file by putting the following text into
+C<root/src/error_noperms.tt2>:
+
+    <span class="error">Permission Denied</span>
+
+Then run the Catalyst development server script:
 
     $ script/myapp_server.pl
 
-Log in as C<test02>.  Once at the book list, click the "Create" link
-to try the C<form_create> action.  You should receive a red
-"Unauthorized!"  error message at the top of the list.  (Note that in
-the example code the "Create" link code in C<root/src/books/list.tt2>
-is inside an C<IF> statement that only displays the list to
-admin-level users.)  If you log in as C<test01> you should be able to
-view the C<form_create> form and add a new book.
+Log in as C<test01> and create several new books using the C<url_create>
+feature:
+
+    http://localhost:3000/books/url_create/Test/1/4
+
+Then, while still logged in as C<test01>, click the "Delete" link next 
+to one of these books.  The book should be removed and you should see 
+the usual green "Book deleted" message.  Next, click the "User Logout" 
+link and log back in as C<test02>.  Now try deleting one of the books. 
+You should be taken to the red "Permission Denied" message on our 
+error page.
 
-When you are done, use one of the 'Logout' links (or go to the
+Use one of the 'Logout' links (or go to the
 L<http://localhost:3000/logout> URL directly) when you are done.
 
 
@@ -412,8 +355,8 @@ Kennedy Clark, C<hkclark@gmail.com>
 
 Please report any errors, issues or suggestions to the author.  The
 most recent version of the Catalyst Tutorial can be found at
-L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>.
+L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>.
 
 Copyright 2006-2008, Kennedy Clark, under Creative Commons License
-(L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).
+(L<http://creativecommons.org/licenses/by-sa/3.0/us/>).