Making Realm a bonafide object. No change to docs yet, but passes all
[catagits/Catalyst-Plugin-Authentication.git] / lib / Catalyst / Plugin / Authentication.pm
index b2e7cbd..f139712 100644 (file)
@@ -1,12 +1,9 @@
-#!/usr/bin/perl
-
 package Catalyst::Plugin::Authentication;
 
 use base qw/Class::Accessor::Fast Class::Data::Inheritable/;
 
 BEGIN {
     __PACKAGE__->mk_accessors(qw/_user/);
-    __PACKAGE__->mk_classdata($_) for qw/_auth_realms/;
 }
 
 use strict;
@@ -14,6 +11,7 @@ use warnings;
 
 use Tie::RefHash;
 use Class::Inspector;
+use Catalyst::Plugin::Authentication::Realm;
 
 # this optimization breaks under Template::Toolkit
 # use user_exists instead
@@ -22,7 +20,7 @@ use Class::Inspector;
 #      constant->import(have_want => eval { require Want });
 #}
 
-our $VERSION = "0.10";
+our $VERSION = "0.10003";
 
 sub set_authenticated {
     my ( $c, $user, $realmname ) = @_;
@@ -33,39 +31,24 @@ sub set_authenticated {
     if (!$realmname) {
         $realmname = 'default';
     }
+    my $realm = $c->get_auth_realm($realmname);
+    
+    if (!$realm) {
+        Catalyst::Exception->throw(
+                "set_authenticated called with nonexistant realm: '$realmname'.");
+    }
     
     if (    $c->isa("Catalyst::Plugin::Session")
         and $c->config->{authentication}{use_session}
         and $user->supports("session") )
     {
-        $c->save_user_in_session($user, $realmname);
+        $realm->save_user_in_session($c, $user);
     }
-    $user->auth_realm($realmname);
+    $user->auth_realm($realm->name);
     
     $c->NEXT::set_authenticated($user, $realmname);
 }
 
-sub _should_save_user_in_session {
-    my ( $c, $user ) = @_;
-
-    $c->_auth_sessions_supported
-    and $c->config->{authentication}{use_session}
-    and $user->supports("session");
-}
-
-sub _should_load_user_from_session {
-    my ( $c, $user ) = @_;
-
-    $c->_auth_sessions_supported
-    and $c->config->{authentication}{use_session}
-    and $c->session_is_valid;
-}
-
-sub _auth_sessions_supported {
-    my $c = shift;
-    $c->isa("Catalyst::Plugin::Session");
-}
-
 sub user {
     my $c = shift;
 
@@ -101,7 +84,7 @@ sub user_in_realm {
     }
 }
 
-sub save_user_in_session {
+sub __old_save_user_in_session {
     my ( $c, $user, $realmname ) = @_;
 
     $c->session->{__user_realm} = $realmname;
@@ -138,18 +121,22 @@ sub find_user {
     
     $realmname ||= 'default';
     my $realm = $c->get_auth_realm($realmname);
-    if ( $realm->{'store'} ) {
-        return $realm->{'store'}->find_user($userinfo, $c);
-    } else {
-        $c->log->debug('find_user: unable to locate a store matching the requested realm');
+    
+    if (!$realm) {
+        Catalyst::Exception->throw(
+                "find_user called with nonexistant realm: '$realmname'.");
     }
+    return $realm->find_user($userinfo, $c);
 }
 
 
 sub _user_in_session {
     my $c = shift;
 
-    return unless $c->_should_load_user_from_session;
+    return unless
+        $c->isa("Catalyst::Plugin::Session")
+        and $c->config->{authentication}{use_session}
+        and $c->session_is_valid;
 
     return $c->session->{__user};
 }
@@ -164,10 +151,11 @@ sub auth_restore_user {
     return unless $realmname; # FIXME die unless? This is an internal inconsistency
 
     my $realm = $c->get_auth_realm($realmname);
-    $c->_user( my $user = $realm->{'store'}->from_session( $c, $frozen_user ) );
+    $c->_user( my $user = $realm->from_session( $c, $frozen_user ) );
     
     # this sets the realm the user originated in.
     $user->auth_realm($realmname);
+        
     return $user;
 
 }
@@ -175,47 +163,52 @@ sub auth_restore_user {
 # we can't actually do our setup in setup because the model has not yet been loaded.  
 # So we have to trigger off of setup_finished.  :-(
 sub setup {
-    my $c = shift;
+    my $app = shift;
 
-    $c->_authentication_initialize();
-    $c->NEXT::setup(@_);
+    $app->_authentication_initialize();
+    $app->NEXT::setup(@_);
 }
 
 ## the actual initialization routine. whee.
 sub _authentication_initialize {
-    my $c = shift;
+    my $app = shift;
 
-    if ($c->_auth_realms) { return };
-    
-    my $cfg = $c->config->{'authentication'} || {};
+    ## let's avoid recreating / configuring everything if we have already done it, eh?
+    if ($app->can('_auth_realms')) { return };
 
-    %$cfg = (
-        use_session => 1,
-        %$cfg,
-    );
+    ## make classdata where it is used.  
+    $app->mk_classdata( '_auth_realms' => {});
+    
+    my $cfg = $app->config->{'authentication'} ||= {};
 
-    my $realmhash = {};
-    $c->_auth_realms($realmhash);
+    $cfg->{use_session} = 1;
     
-    ## BACKWARDS COMPATIBILITY - if realm is not defined - then we are probably dealing
-    ## with an old-school config.  The only caveat here is that we must add a classname 
     if (exists($cfg->{'realms'})) {
-        
         foreach my $realm (keys %{$cfg->{'realms'}}) {
-            $c->setup_auth_realm($realm, $cfg->{'realms'}{$realm});
+            $app->setup_auth_realm($realm, $cfg->{'realms'}{$realm});
         }
-
-        #  if we have a 'default-realm' in the config hash and we don't already 
+        #  if we have a 'default_realm' in the config hash and we don't already 
         # have a realm called 'default', we point default at the realm specified
-        if (exists($cfg->{'default_realm'}) && !$c->get_auth_realm('default')) {
-            $c->_set_default_auth_realm($cfg->{'default_realm'});
+        if (exists($cfg->{'default_realm'}) && !$app->get_auth_realm('default')) {
+            $app->_set_default_auth_realm($cfg->{'default_realm'});
         }
     } else {
+        
+        ## BACKWARDS COMPATIBILITY - if realms is not defined - then we are probably dealing
+        ## with an old-school config.  The only caveat here is that we must add a classname
+        
+        ## also - we have to treat {store} as {stores}{default} - because 
+        ## while it is not a clear as a valid config in the docs, it 
+        ## is functional with the old api. Whee!
+        if (exists($cfg->{'store'}) && !exists($cfg->{'stores'}{'default'})) {
+            $cfg->{'stores'}{'default'} = $cfg->{'store'};
+        }
+
         foreach my $storename (keys %{$cfg->{'stores'}}) {
             my $realmcfg = {
-                store => $cfg->{'stores'}{$storename},
+                store => { class => $cfg->{'stores'}{$storename} },
             };
-            $c->setup_auth_realm($storename, $realmcfg);
+            $app->setup_auth_realm($storename, $realmcfg);
         }
     }
     
@@ -225,56 +218,18 @@ sub _authentication_initialize {
 sub setup_auth_realm {
     my ($app, $realmname, $config) = @_;
     
-    $app->log->debug("Setting up $realmname");
-    if (!exists($config->{'store'}{'class'})) {
-        Carp::croak "Couldn't setup the authentication realm named '$realmname', no class defined";
-    } 
-        
-    # use the 
-    my $storeclass = $config->{'store'}{'class'};
-    
-    ## follow catalyst class naming - a + prefix means a fully qualified class, otherwise it's
-    ## taken to mean C::P::A::Store::(specifiedclass)
-    if ($storeclass !~ /^\+(.*)$/ ) {
-        $storeclass = "Catalyst::Plugin::Authentication::Store::${storeclass}";
-    } else {
-        $storeclass = $1;
+    my $realmclass = 'Catalyst::Plugin::Authentication::Realm';
+    if (defined($config->{'class'})) {
+        $realmclass = $config->{'class'};
+        Catalyst::Utils::ensure_class_loaded( $realmclass );
     }
-    
-
-    # a little niceness - since most systems seem to use the password credential class, 
-    # if no credential class is specified we use password.
-    $config->{credential}{class} ||= "Catalyst::Plugin::Authentication::Credential::Password";
-
-    my $credentialclass = $config->{'credential'}{'class'};
-    
-    ## follow catalyst class naming - a + prefix means a fully qualified class, otherwise it's
-    ## taken to mean C::P::A::Credential::(specifiedclass)
-    if ($credentialclass !~ /^\+(.*)$/ ) {
-        $credentialclass = "Catalyst::Plugin::Authentication::Credential::${credentialclass}";
+    my $realm = $realmclass->new($realmname, $config, $app);
+    if ($realm) {
+        $app->auth_realms->{$realmname} = $realm;
     } else {
-        $credentialclass = $1;
-    }
-    
-    # if we made it here - we have what we need to load the classes;
-    Catalyst::Utils::ensure_class_loaded( $credentialclass );
-    Catalyst::Utils::ensure_class_loaded( $storeclass );
-    
-    # BACKWARDS COMPATIBILITY - if the store class does not define find_user, we define it in terms 
-    # of get_user and add it to the class.  this is because the auth routines use find_user, 
-    # and rely on it being present. (this avoids per-call checks)
-    if (!$storeclass->can('find_user')) {
-        no strict 'refs';
-        *{"${storeclass}::find_user"} = sub {
-                                                my ($self, $info) = @_;
-                                                my @rest = @{$info->{rest}} if exists($info->{rest});
-                                                $self->get_user($info->{id}, @rest);
-                                            };
+        $app->log->debug("realm initialization for '$realmname' failed.");
     }
-    
-    $app->auth_realms->{$realmname}{'store'} = $storeclass->new($config->{'store'}, $app);
-    $app->auth_realms->{$realmname}{'credential'} = $credentialclass->new($config->{'credential'}, $app);
-   
+    return $realm;
 }
 
 sub auth_realms {
@@ -313,15 +268,12 @@ sub authenticate {
     
     ## note to self - make authenticate throw an exception if realm is invalid.
     
-    if ($realm && exists($realm->{'credential'})) {
-        my $user = $realm->{'credential'}->authenticate($app, $realm->{store}, $userinfo);
-        if (ref($user)) {
-            $app->set_authenticated($user, $realmname);
-            return $user;
-        }
+    if ($realm) {
+        return $realm->authenticate($app, $userinfo);
     } else {
-        $app->log->debug("The realm requested, '$realmname' does not exist," .
-                         " or there is no credential associated with it.")
+        Catalyst::Exception->throw(
+                "authenticate called with nonexistant realm: '$realmname'.");
+
     }
     return undef;
 }
@@ -353,9 +305,19 @@ sub get_user {
 sub default_auth_store {
     my $self = shift;
 
+    my $realm = $self->get_auth_realm('default');
+    if (!$realm) {
+        $realm = $self->setup_auth_realm('default', { class => "Catalyst::Plugin::Authentication::Realm::Compatibility" });
+    }
     if ( my $new = shift ) {
-        $self->auth_realms->{'default'}{'store'} = $new;
-        my $storeclass = ref($new);
+        $realm->store($new);
+        
+        my $storeclass;
+        if (ref($new)) {
+            $storeclass = ref($new);
+        } else {
+            $storeclass = $new;
+        }
         
         # BACKWARDS COMPATIBILITY - if the store class does not define find_user, we define it in terms 
         # of get_user and add it to the class.  this is because the auth routines use find_user, 
@@ -370,7 +332,7 @@ sub default_auth_store {
         }
     }
 
-    return $self->get_auth_realm('default')->{'store'};
+    return $self->get_auth_realm('default')->store;
 }
 
 ## BACKWARDS COMPATIBILITY
@@ -379,7 +341,7 @@ sub default_auth_store {
 sub auth_store_names {
     my $self = shift;
 
-    my %hash = (  $self->get_auth_realm('default')->{'store'} => 'default' );
+    my %hash = (  $self->get_auth_realm('default')->store => 'default' );
 }
 
 sub get_auth_store {
@@ -401,7 +363,7 @@ sub get_auth_store_name {
 sub auth_stores {
     my $self = shift;
 
-    my %hash = ( 'default' => $self->get_auth_realm('default')->{'store'});
+    my %hash = ( 'default' => $self->get_auth_realm('default')->store);
 }
 
 __PACKAGE__;
@@ -422,7 +384,8 @@ authentication framework.
     /;
 
     # later on ...
-    $c->authenticate({ username => 'myusername', password => 'mypassword' });
+    $c->authenticate({ username => 'myusername', 
+                       password => 'mypassword' });
     my $age = $c->user->get('age');
     $c->logout;
 
@@ -477,7 +440,7 @@ The next logical step is B<authorization>, the process of deciding what a user
 is (or isn't) allowed to do. For example, say your users are split into two
 main groups - regular users and administrators. You want to verify that the
 currently logged in user is indeed an administrator before performing the
-actions in an administrative part of your application. These decisionsmay be
+actions in an administrative part of your application. These decisions may be
 made within your application code using just the information available after
 authentication, or it may be facilitated by a number of plugins.  
 
@@ -544,30 +507,32 @@ This means that our application will begin like this:
     /;
 
     __PACKAGE__->config->{authentication} = 
-                    {  
-                        default_realm => 'members',
-                        realms => {
-                            members => {
-                                credential => {
-                                    class => 'Password'
-                                },
-                                store => {
-                                    class => 'Minimal',
-                                       users = {
-                                           bob => {
-                                               password => "s00p3r",                                       
-                                               editor => 'yes',
-                                               roles => [qw/edit delete/],
-                                           },
-                                           william => {
-                                               password => "s3cr3t",
-                                               roles => [qw/comment/],
-                                           }
-                                       }                       
-                                   }
-                               }
-                       }
-                    };
+                {  
+                    default_realm => 'members',
+                    realms => {
+                        members => {
+                            credential => {
+                                class => 'Password',
+                                password_field => 'password',
+                                password_type => 'clear'
+                            },
+                            store => {
+                                class => 'Minimal',
+                               users = {
+                                   bob => {
+                                       password => "s00p3r",                                       
+                                       editor => 'yes',
+                                       roles => [qw/edit delete/],
+                                   },
+                                   william => {
+                                       password => "s3cr3t",
+                                       roles => [qw/comment/],
+                                   }
+                               }                       
+                           }
+                       }
+                       }
+                };
     
 
 This tells the authentication plugin what realms are available, which
@@ -597,8 +562,8 @@ To show an example of this, let's create an authentication controller:
     }
 
 This code should be very readable. If all the necessary fields are supplied,
-call the L<Catalyst::Plugin::Authentication/authenticate> method in the
-controller. If it succeeds the user is logged in.
+call the "authenticate" method from the controller. If it succeeds the 
+user is logged in.
 
 The credential verifier will attempt to retrieve the user whose details match
 the authentication information provided to $c->authenticate(). Once it fetches
@@ -617,8 +582,8 @@ call:
     } ...
 
 
-Now suppose we want to restrict the ability to edit to a user with 'edit'
-in it's roles list.  
+Now suppose we want to restrict the ability to edit to a user with an 
+'editor' value of yes.
 
 The restricted action might look like this:
 
@@ -627,12 +592,17 @@ The restricted action might look like this:
 
         $c->detach("unauthorized")
           unless $c->user_exists
-          and $c->user->get('editor') == 'yes';
+          and $c->user->get('editor') eq 'yes';
 
         # do something restricted here
     }
 
-This is somewhat similar to role based access control.
+(Note that if you have multiple realms, you can use $c->user_in_realm('realmname')
+in place of $c->user_exists(); This will essentially perform the same 
+verification as user_exists, with the added requirement that if there is a 
+user, it must have come from the realm specified.)
+
+The above example is somewhat similar to role based access control.  
 L<Catalyst::Plugin::Authentication::Store::Minimal> treats the roles field as
 an array of role names. Let's leverage this. Add the role authorization
 plugin:
@@ -664,7 +634,9 @@ changing your config:
                         realms => {
                             members => {
                                 credential => {
-                                    class => 'Password'
+                                    class => 'Password',
+                                    password_field => 'password',
+                                    password_type => 'clear'
                                 },
                                 store => {
                                     class => 'DBIx::Class',
@@ -690,7 +662,9 @@ new source. The rest of your application is completely unchanged.
                     realms => {
                         members => {
                             credential => {
-                                class => 'Password'
+                                class => 'Password',
+                                password_field => 'password',
+                                password_type => 'clear'
                             },
                             store => {
                                 class => 'DBIx::Class',
@@ -700,7 +674,9 @@ new source. The rest of your application is completely unchanged.
                        },
                        admins => {
                            credential => {
-                               class => 'Password'
+                               class => 'Password',
+                               password_field => 'password',
+                                password_type => 'clear'
                            },
                            store => {
                                class => '+MyApp::Authentication::Store::NetAuth',
@@ -771,7 +747,7 @@ can be much more efficient.
 =item user_in_realm ( $realm )
 
 Works like user_exists, except that it only returns true if a user is both 
-logged in right now and is from the realm provided.  
+logged in right now and was retrieved from the realm provided.  
 
 =item logout