Making Realm a bonafide object. No change to docs yet, but passes all
[catagits/Catalyst-Plugin-Authentication.git] / lib / Catalyst / Plugin / Authentication.pm
index 73f19ba..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->_set_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;
 
@@ -73,8 +56,8 @@ sub user {
         return $c->_user(@_);
     }
 
-    if ( defined(my $user = $c->_user) ) {
-        return $user;
+    if ( defined($c->_user) ) {
+        return $c->_user;
     } else {
         return $c->auth_restore_user;
     }
@@ -87,15 +70,28 @@ sub user_exists {
        return defined($c->_user) || defined($c->_user_in_session);
 }
 
+# works like user_exists - except only returns true if user 
+# exists AND is in the realm requested.
+sub user_in_realm {
+    my ($c, $realmname) = @_;
+
+    if (defined($c->_user)) {
+        return ($c->_user->auth_realm eq $realmname);
+    } elsif (defined($c->_user_in_session)) {
+        return ($c->session->{__user_realm} eq $realmname);  
+    } else {
+        return undef;
+    }
+}
 
-sub save_user_in_session {
+sub __old_save_user_in_session {
     my ( $c, $user, $realmname ) = @_;
 
     $c->session->{__user_realm} = $realmname;
     
-    # we want to ask the backend for a user prepared for the session.
+    # we want to ask the store for a user prepared for the session.
     # but older modules split this functionality between the user and the
-    # backend.  We try the store first.  If not, we use the old method.
+    # store.  We try the store first.  If not, we use the old method.
     my $realm = $c->get_auth_realm($realmname);
     if ($realm->{'store'}->can('for_session')) {
         $c->session->{__user} = $realm->{'store'}->for_session($c, $user);
@@ -125,30 +121,26 @@ 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};
 }
 
-sub _store_in_session {
-    my $c = shift;
-    
-    # we don't need verification, it's only called if _user_in_session returned something useful
-
-    return $c->session->{__user_store};
-}
-
 sub auth_restore_user {
     my ( $c, $frozen_user, $realmname ) = @_;
 
@@ -159,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->_set_auth_realm($realmname);
+    $user->auth_realm($realmname);
+        
     return $user;
 
 }
@@ -170,114 +163,73 @@ 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);
         }
     }
     
 }
 
-
 # set up realmname.
 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)::Backend
-    if ($storeclass !~ /^\+(.*)$/ ) {
-        $storeclass = "Catalyst::Plugin::Authentication::Store::${storeclass}::Backend";
-    } 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}";
-    } 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->auth_realms->{$realmname}{'store'} = $storeclass->new($config->{'store'}, $app);
-    if ($credentialclass->can('new')) {
-        $app->auth_realms->{$realmname}{'credential'} = $credentialclass->new($config->{'credential'}, $app);
+    my $realm = $realmclass->new($realmname, $config, $app);
+    if ($realm) {
+        $app->auth_realms->{$realmname} = $realm;
     } else {
-        # if the credential class is not actually a class - has no 'new' operator, we wrap it, 
-        # once again - to allow our code to be simple at runtime and allow non-OO packages to function.
-        my $wrapperclass = 'Catalyst::Plugin::Authentication::Credential::Wrapper';
-        Catalyst::Utils::ensure_class_loaded( $wrapperclass );
-        $app->auth_realms->{$realmname}{'credential'} = $wrapperclass->new($config->{'credential'}, $app);
+        $app->log->debug("realm initialization for '$realmname' failed.");
     }
+    return $realm;
 }
 
 sub auth_realms {
@@ -314,15 +266,14 @@ sub authenticate {
         
     my $realm = $app->get_auth_realm($realmname);
     
-    if ($realm && exists($realm->{'credential'})) {
-        my $user = $realm->{'credential'}->authenticate($app, $realm->{store}, $userinfo);
-        if (ref($user)) {
-            $app->set_authenticated($user, $realmname);
-            return $user;
-        }
+    ## note to self - make authenticate throw an exception if realm is invalid.
+    
+    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;
 }
@@ -354,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, 
@@ -371,7 +332,7 @@ sub default_auth_store {
         }
     }
 
-    return $self->get_auth_realm('default')->{'store'};
+    return $self->get_auth_realm('default')->store;
 }
 
 ## BACKWARDS COMPATIBILITY
@@ -380,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 {
@@ -402,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__;
@@ -423,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;
 
@@ -478,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.  
 
@@ -509,7 +471,7 @@ they claim to be.
 
 =head3 Storage Backends
 
-The authentication data also identifies a user, and the Storage Backend modules
+The authentication data also identifies a user, and the Storage backend modules
 use this data to locate and return a standardized object-oriented
 representation of a user.
 
@@ -545,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
@@ -598,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
@@ -618,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:
 
@@ -628,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:
@@ -665,7 +634,9 @@ changing your config:
                         realms => {
                             members => {
                                 credential => {
-                                    class => 'Password'
+                                    class => 'Password',
+                                    password_field => 'password',
+                                    password_type => 'clear'
                                 },
                                 store => {
                                     class => 'DBIx::Class',
@@ -691,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',
@@ -701,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',
@@ -741,7 +716,7 @@ class name. Otherwise it is considered to be a portion of the class name. For
 credentials, the classname 'B<Password>', for example, is expanded to
 Catalyst::Plugin::Authentication::Credential::B<Password>. For stores, the
 classname 'B<storename>' is expanded to:
-Catalyst::Plugin::Authentication::Store::B<storename>::Backend.
+Catalyst::Plugin::Authentication::Store::B<storename>.
 
 
 =back
@@ -765,10 +740,15 @@ Returns the currently logged in user or undef if there is none.
 
 Returns true if a user is logged in right now. The difference between
 user_exists and user is that user_exists will return true if a user is logged
-in, even if it has not been retrieved from the storage backend. If you only
+in, even if it has not been yet retrieved from the storage backend. If you only
 need to know if the user is logged in, depending on the storage mechanism this
 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 was retrieved from the realm provided.  
+
 =item logout
 
 Logs the user out, Deletes the currently logged in user from $c->user and the session.
@@ -886,7 +866,7 @@ The items below are still present in the plugin, though using them is
 deprecated. They remain only as a transition tool, for those sites which can
 not yet be upgraded to use the new system due to local customizations or use
 of Credential / Store modules that have not yet been updated to work with the 
-new backend API.
+new API.
 
 These routines should not be used in any application using realms
 functionality or any of the methods described above. These are for reference
@@ -912,7 +892,7 @@ or by using a Store plugin:
        use Catalyst qw/Authentication Authentication::Store::Minimal/;
 
 Sets the default store to
-L<Catalyst::Plugin::Authentication::Store::Minimal::Backend>.
+L<Catalyst::Plugin::Authentication::Store::Minimal>.
 
 =item get_auth_store $name