[Catalyst-commits] r10441 - in trunk/Catalyst-Plugin-Authentication:
lib/Catalyst/Authentication/Credential t t/lib
t0m at dev.catalyst.perl.org
t0m at dev.catalyst.perl.org
Sat Jun 6 09:39:15 GMT 2009
Author: t0m
Date: 2009-06-06 09:39:15 +0000 (Sat, 06 Jun 2009)
New Revision: 10441
Added:
trunk/Catalyst-Plugin-Authentication/lib/Catalyst/Authentication/Credential/Remote.pm
trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestApp1.pm
trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestApp2.pm
trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestEngine.pm
trunk/Catalyst-Plugin-Authentication/t/live_app_remote1.t
trunk/Catalyst-Plugin-Authentication/t/live_app_remote2.t
Log:
Merge kmx's remote_credential branch
Added: trunk/Catalyst-Plugin-Authentication/lib/Catalyst/Authentication/Credential/Remote.pm
===================================================================
--- trunk/Catalyst-Plugin-Authentication/lib/Catalyst/Authentication/Credential/Remote.pm (rev 0)
+++ trunk/Catalyst-Plugin-Authentication/lib/Catalyst/Authentication/Credential/Remote.pm 2009-06-06 09:39:15 UTC (rev 10441)
@@ -0,0 +1,293 @@
+package Catalyst::Authentication::Credential::Remote;
+
+use strict;
+use warnings;
+
+use base 'Class::Accessor::Fast';
+
+BEGIN {
+ __PACKAGE__->mk_accessors(qw/allow_re deny_re cutname_re source realm/);
+}
+
+sub new {
+ my ( $class, $config, $app, $realm ) = @_;
+
+ my $self = { };
+ bless $self, $class;
+
+ # we are gonna compile regular expresions defined in config parameters
+ # and explicitly throw an exception saying what parameter was invalid
+ if (defined($config->{allow_regexp}) && ($config->{allow_regexp} ne "")) {
+ eval { $self->allow_re( qr/$config->{allow_regexp}/ ) };
+ Catalyst::Exception->throw( "Invalid regular expression in ".
+ "'allow_regexp' configuration parameter") if $@;
+ }
+ if (defined($config->{deny_regexp}) && ($config->{deny_regexp} ne "")) {
+ eval { $self->deny_re( qr/$config->{deny_regexp}/ ) };
+ Catalyst::Exception->throw( "Invalid regular expression in ".
+ "'deny_regexp' configuration parameter") if $@;
+ }
+ if (defined($config->{cutname_regexp}) && ($config->{cutname_regexp} ne "")) {
+ eval { $self->cutname_re( qr/$config->{cutname_regexp}/ ) };
+ Catalyst::Exception->throw( "Invalid regular expression in ".
+ "'cutname_regexp' configuration parameter") if $@;
+ }
+ $self->source($config->{source} || 'REMOTE_USER');
+ $self->realm($realm);
+ return $self;
+}
+
+sub authenticate {
+ my ( $self, $c, $realm, $authinfo ) = @_;
+
+ my $remuser;
+ if ($self->source eq "REMOTE_USER") {
+ # compatibility hack:
+ if (defined($c->engine->env)) {
+ # BEWARE: $c->engine->env was broken prior 5.80005
+ $remuser = $c->engine->env->{REMOTE_USER};
+ }
+ elsif ($c->req->can('remote_user')) {
+ # $c->req->remote_users was introduced in 5.80005; if not evailable we are
+ # gonna use $c->req->user that is deprecated but more or less works as well
+ $remuser = $c->req->remote_user;
+ }
+ elsif ($c->req->can('user')) {
+ # maybe show warning that we are gonna use DEPRECATED $req->user
+ if (ref($c->req->user)) {
+ # I do not know exactly when this happens but it happens
+ Catalyst::Exception->throw( "Cannot get remote user from ".
+ "\$c->req->user as it seems to be a reference not a string" );
+ }
+ else {
+ $remuser = $c->req->user;
+ }
+ }
+ }
+ elsif ($self->source =~ /^(SSL_CLIENT_.*|CERT_*|AUTH_USER)$/) {
+ # if you are using 'exotic' webserver or if the user is
+ # authenticated e.g via SSL certificate his name could be avaliable
+ # in different variables
+ # BEWARE: $c->engine->env was broken prior 5.80005
+ my $nam=$self->source;
+ if ($c->engine->can('env')) {
+ $remuser = $c->engine->env->{$nam};
+ }
+ else {
+ # this happens on Catalyst 5.80004 and before (when using FastCGI)
+ Catalyst::Exception->throw( "Cannot handle parameter 'source=$nam'".
+ " as runnig Catalyst engine has broken \$c->engine->env" );
+ }
+ }
+ else {
+ Catalyst::Exception->throw( "Invalid value of 'source' parameter");
+ }
+ return unless defined($remuser);
+ return if ($remuser eq "");
+
+ # $authinfo hash can contain item username (it is optional) - if it is so
+ # this username has to be equal to remote_user
+ my $authuser = $authinfo->{username};
+ return if (defined($authuser) && ($authuser ne $remuser));
+
+ # handle deny / allow checks
+ return if (defined($self->deny_re) && ($remuser =~ $self->deny_re));
+ return if (defined($self->allow_re) && ($remuser !~ $self->allow_re));
+
+ # if param cutname_regexp is specified we try to cut the final usename as a
+ # substring from remote_user
+ my $usr = $remuser;
+ if (defined($self->cutname_re)) {
+ if (($remuser =~ $self->cutname_re) && ($1 ne "")) {
+ $usr = $1;
+ }
+ }
+
+ $authinfo->{id} = $authinfo->{username} = $usr;
+ $authinfo->{remote_user} = $remuser; # just to keep the original value
+ my $user_obj = $realm->find_user( $authinfo, $c );
+ return ref($user_obj) ? $user_obj : undef;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+Catalyst::Authentication::Credential::Remote - Let the webserver (e.g. Apache)
+authenticate Catalyst application users
+
+=head1 SYNOPSIS
+
+ # in your MyApp.pm
+ __PACKAGE__->config(
+
+ 'Plugin::Authentication' => {
+ default_realm => 'remoterealm',
+ realms => {
+ remoterealm => {
+ credential => {
+ class => 'Remote',
+ allow_regexp => '^(user.*|admin|guest)$',
+ deny_regexp => 'test',
+ },
+ store => {
+ class => 'Null',
+ # if you want to have some additional user attributes
+ # like user roles, user full name etc. you can specify
+ # here the store where you keep this data
+ }
+ },
+ },
+ },
+
+ );
+
+ # in your Controller/Root.pm you can implement "auto-login" in this way
+ sub begin : Private {
+ my ( $self, $c ) = @_;
+ unless ($c->user_exists) {
+ # authenticate() for this module does not need any user info
+ # as the username is taken from $c->req->remote_user and
+ # password is not needed
+ unless ($c->authenticate( {} )) {
+ # return 403 forbidden or kick out the user in other way
+ };
+ }
+ }
+
+ # or you can implement in any controller an ordinary login action like this
+ sub login : Global {
+ my ( $self, $c ) = @_;
+ $c->authenticate( {} );
+ }
+
+=head1 DESCRIPTION
+
+This module allows you to authenticate the users of your Catalyst application
+on underlaying webserver. The complete list of authentication method available
+via this module depends just on what your webserver (e.g. Apache, IIS, Lighttpd)
+is able to handle.
+
+Besides the common methods like HTTP Basic and Digest authentication you can
+also use sophisticated ones like so called "integrated authentication" via
+NTLM or Kerberos (popular in corporate intranet applications running in Windows
+Active Directory enviroment) or even the SSL authentication when users
+authenticate themself using their client SSL certificates.
+
+The main idea of this module is based on a fact that webserver passes the name
+of authenticated user into Catalyst application as REMOTE_USER variable (or in
+case of SSL client authentication in other variables like SSL_CLIENT_S_DN on
+Apache + mod_ssl) - from this point referenced as WEBUSER.
+This module simply takes this value - perfoms some optional checks (see
+below) - and if everything is OK the WEBUSER is declared as authenticated on
+Catalyst level. In fact this module does not perform any check for password or
+other credential; it simply believes the webserver that user was properly
+authenticated.
+
+=head1 CONFIG
+
+=head2 class
+
+This config item is B<REQUIRED>.
+
+B<class> is part of the core L<Catalyst::Plugin::Authentication> module, it
+contains the class name of the store to be used.
+
+The classname used for Credential. This is part of L<Catalyst::Plugin::Authentication>
+and is the method by which Catalyst::Authentication::Credential::Remote is
+loaded as the credential validator. For this module to be used, this must be set
+to 'Remote'.
+
+=head2 source
+
+This config item is B<OPTIONAL> - default is REMOTE_USER.
+
+B<source> contains a name of a variable passed from webserver that contains the
+user identification.
+
+Supported values: REMOTE_USER, SSL_CLIENT_*, CERT_*, AUTH_USER
+
+B<BEWARE:> Support for using different variables than REMOTE_USER does not work
+properly with Catalyst 5.8004 and before (if you want details see source code).
+
+Note1: Apache + mod_ssl uses SSL_CLIENT_S_DN, SSL_CLIENT_S_DN_* etc. (has to be
+enabled by 'SSLOption +StdEnvVars') or you can also let Apache make a copy of
+this value into REMOTE_USER (Apache option 'SSLUserName SSL_CLIENT_S_DN').
+
+Note2: Microsoft IIS uses CERT_SUBJECT, CERT_SERIALNUMBER etc. for storing info
+about client authenticated via SSL certificate. AUTH_USER on IIS seems to have
+the same value as REMOTE_USER (but there might be some differences I am not
+aware of).
+
+=head2 deny_regexp
+
+This config item is B<OPTIONAL> - no default value.
+
+B<deny_regexp> contains a regular expression used for check against WEBUSER
+(see details below)
+
+=head2 allow_regexp
+
+This config item is B<OPTIONAL> - no default value.
+
+B<deny_regexp> contains a regular expression used for check against WEBUSER.
+
+Allow/deny checking of WEBUSER values goes in this way:
+
+1) If B<deny_regexp> is defined and WEBUSER matches deny_regexp then
+authentication FAILS otherwise continues with next step. If deny_regexp is not
+defined or is an empty string we skip this step.
+
+2) If B<allow_regexp> is defined and WEBUSER matches allow_regexp then
+authentication PASSES otherwise FAILS. If allow_regexp is not
+defined or is an empty string we skip this step.
+
+The order deny-allow is fixed.
+
+=head2 cutname_regexp
+
+This config item is B<OPTIONAL> - no default value.
+
+If param B<cutname_regexp> is specified we try to cut the final usename passed to
+Catalyst application as a substring from WEBUSER. This is usefull for
+example in case of SSL authentication when WEBUSER looks like this
+'CN=john, OU=Unit Name, O=Company, C=CZ' - from this format we can simply cut
+pure usename by cutname_regexp set to 'CN=(.*), OU=Unit Name, O=Company, C=CZ'.
+
+Substring is always taken as '$1' regexp substring. If WEBUSER does not
+match cutname_regexp at all or if '$1' regexp substring is empty we pass the
+original WEBUSER value (without cutting) to Catalyst application.
+
+=head1 METHODS
+
+=head2 new ( $config, $app, $realm )
+
+Instantiate a new Catalyst::Authentication::Credential::Remote object using the
+configuration hash provided in $config. In case of invalid value of any
+configuration parameter (e.g. invalid regular expression) throws an exception.
+
+=cut
+
+=head2 authenticate ( $realm, $authinfo )
+
+Takes the username form WEBUSER set by webserver, performs additional
+checks using optional allow_regexp/deny_regexp configuration params, optionaly
+takes substring from WEBUSER and the sets the resulting value as
+a Catalyst username.
+
+=cut
+
+=head1 COMPATIBILITY
+
+It is B<strongly recommended> to use this module with Catalyst 5.80005 and above
+as previous versions have some bugs related to $c->engine->env and do not
+support $c->req->remote_user.
+
+This module tries some workarounds when it detects an older version and should
+work as well.
+
+=cut
Added: trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestApp1.pm
===================================================================
--- trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestApp1.pm (rev 0)
+++ trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestApp1.pm 2009-06-06 09:39:15 UTC (rev 10441)
@@ -0,0 +1,45 @@
+package RemoteTestApp1;
+
+use Catalyst qw/
+ Authentication
+/;
+
+use base qw/Catalyst/;
+__PACKAGE__->engine_class('RemoteTestEngine');
+__PACKAGE__->config(
+ 'Plugin::Authentication' => {
+ default_realm => 'remote',
+ realms => {
+ remote => {
+ credential => {
+ class => 'Remote',
+ allow_regexp => '^(bob|john|CN=.*)$',
+ deny_regexp=> 'denied',
+ cutname_regexp=> 'CN=(.*)/OU=Test',
+ },
+ store => {
+ class => 'Null',
+ },
+ },
+ },
+ },
+);
+
+sub default : Local {
+ my ( $self, $c ) = @_;
+ if ($c->authenticate()) {
+ $c->res->body('User:' . $c->user->{id});
+ }
+ else {
+ $c->res->body('FAIL');
+ $c->res->status(403);
+ }
+}
+
+sub public : Local {
+ my ( $self, $c ) = @_;
+ $c->res->body('OK');
+}
+
+__PACKAGE__->setup;
+
Added: trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestApp2.pm
===================================================================
--- trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestApp2.pm (rev 0)
+++ trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestApp2.pm 2009-06-06 09:39:15 UTC (rev 10441)
@@ -0,0 +1,46 @@
+package RemoteTestApp2;
+
+use Catalyst qw/
+ Authentication
+/;
+
+use base qw/Catalyst/;
+__PACKAGE__->engine_class('RemoteTestEngine');
+__PACKAGE__->config(
+ 'Plugin::Authentication' => {
+ default_realm => 'remote',
+ realms => {
+ remote => {
+ credential => {
+ class => 'Remote',
+ allow_regexp => '^(bob|john|CN=.*)$',
+ deny_regexp=> 'denied',
+ cutname_regexp=> 'CN=(.*)/OU=Test',
+ source => 'SSL_CLIENT_S_DN',
+ },
+ store => {
+ class => 'Null',
+ },
+ },
+ },
+ },
+);
+
+sub default : Local {
+ my ( $self, $c ) = @_;
+ if ($c->authenticate()) {
+ $c->res->body('User:' . $c->user->{id});
+ }
+ else {
+ $c->res->body('FAIL');
+ $c->res->status(403);
+ }
+}
+
+sub public : Local {
+ my ( $self, $c ) = @_;
+ $c->res->body('OK');
+}
+
+__PACKAGE__->setup;
+
Added: trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestEngine.pm
===================================================================
--- trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestEngine.pm (rev 0)
+++ trunk/Catalyst-Plugin-Authentication/t/lib/RemoteTestEngine.pm 2009-06-06 09:39:15 UTC (rev 10441)
@@ -0,0 +1,15 @@
+package RemoteTestEngine;
+use base 'Catalyst::Engine::CGI';
+
+our $REMOTE_USER;
+our $SSL_CLIENT_S_DN;
+
+sub env {
+ my $self = shift;
+ my %e = %ENV;
+ $e{REMOTE_USER} = $REMOTE_USER;
+ $e{SSL_CLIENT_S_DN} = $SSL_CLIENT_S_DN;
+ return \%e;
+};
+
+1;
Added: trunk/Catalyst-Plugin-Authentication/t/live_app_remote1.t
===================================================================
--- trunk/Catalyst-Plugin-Authentication/t/live_app_remote1.t (rev 0)
+++ trunk/Catalyst-Plugin-Authentication/t/live_app_remote1.t 2009-06-06 09:39:15 UTC (rev 10441)
@@ -0,0 +1,30 @@
+use strict;
+use warnings;
+use Test::More tests => 10;
+
+use lib 't/lib';
+use Catalyst::Test qw/RemoteTestApp1/;
+
+$RemoteTestEngine::REMOTE_USER = undef;
+ok( request('/public')->is_success, 'anonymous user (undef) - /public' );
+ok( request('/')->is_error, 'anonymous user (undef) - /' );
+
+$RemoteTestEngine::REMOTE_USER = '';
+ok( request('/public')->is_success, 'anonymous user (empty) - /public' );
+ok( request('/')->is_error, 'anonymous user (empty) - /' );
+
+$RemoteTestEngine::REMOTE_USER = 'john';
+ok( request('/')->is_success, 'valid user' );
+
+$RemoteTestEngine::REMOTE_USER = 'nonexisting';
+ok( request('/')->is_error, 'non-existing user' );
+
+$RemoteTestEngine::REMOTE_USER = 'denieduser';
+ok( request('/')->is_error, 'explicitly denied user' );
+
+$RemoteTestEngine::REMOTE_USER = 'CN=namexyz/OU=Test/C=Company';
+ok( request('/')->is_success, 'testing "cutname" option 1' );
+is( request('/')->content, 'User:namexyz', 'testing "cutname" option 2' );
+
+$RemoteTestEngine::REMOTE_USER = 'CN=/OU=Test/C=Company';
+is( request('/')->content, 'User:CN=/OU=Test/C=Company', 'testing "cutname" option - empty $1 match' );
Added: trunk/Catalyst-Plugin-Authentication/t/live_app_remote2.t
===================================================================
--- trunk/Catalyst-Plugin-Authentication/t/live_app_remote2.t (rev 0)
+++ trunk/Catalyst-Plugin-Authentication/t/live_app_remote2.t 2009-06-06 09:39:15 UTC (rev 10441)
@@ -0,0 +1,19 @@
+use strict;
+use warnings;
+use Test::More tests => 3;
+
+use lib 't/lib';
+use Catalyst::Test qw/RemoteTestApp2/;
+
+$RemoteTestEngine::REMOTE_USER = undef;
+
+# WARNING: this requires $c->engine->env to work properly
+# $c->engine->env was slightly broken in 5.8004 but this test should pass
+# as it uses Engine::CGI that works fine even in 5.80004
+
+$RemoteTestEngine::SSL_CLIENT_S_DN = 'CN=anyuser/OU=Test/C=Company';
+ok( request('/')->is_success, 'testing "source" option' );
+
+$RemoteTestEngine::SSL_CLIENT_S_DN = 'CN=namexyz/OU=Test/C=Company';
+ok( request('/')->is_success, 'testing "source" + "cutname" 1' );
+is( request('/')->content, 'User:namexyz', 'testing "source" + "cutname" 2' );
More information about the Catalyst-commits
mailing list