added role self_check and self_check_any to User store role_self_check
Mark Ellis [Mon, 17 Mar 2014 14:17:46 +0000 (14:17 +0000)]
The code to do this was already there, just missing a few
parts on the C::A::S::D::C::User class

also removed unnecessary checks for DBIx::Class from tests as it's a
Makefile.PL requirement

16 files changed:
Changes
lib/Catalyst/Authentication/Store/DBIx/Class.pm
lib/Catalyst/Authentication/Store/DBIx/Class/User.pm
t/03-authtest.t
t/04-authsessions.t
t/05-auth-roles-relationship.t
t/06-auth-roles-column.t
t/07-authsessions-cached.t
t/08-simpledb-auth-roles-relationship.t
t/09-simpledb-auth-roles-column.t
t/11-authsessions-load-app-context.t
t/12-auth-roles-relationship-self_check.t [new file with mode: 0644]
t/13-auth-roles-column-self_check.t [new file with mode: 0644]
t/lib/TestApp/Controller/Root.pm
t/lib/TestApp/Model/TestApp.pm
t/lib/TestApp/Schema/User.pm

diff --git a/Changes b/Changes
index c8babb3..cc1bed5 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,5 +1,6 @@
 Revision history for Catalyst-Plugin-Authentication-Store-DBIx-Class
 
+       * Added support for self checking roles
        * Fix doc bugs. RT#87372
        * Fix calling User->can() as a class method. RT#90715
 
index 1262d71..6b68e89 100644 (file)
@@ -114,6 +114,8 @@ This documentation refers to version 0.1503.
                     user_model => 'MyApp::User',
                     role_relation => 'roles',
                     role_field => 'rolename',
+                    check_roles   => 'check_roles',
+                    check_roles_any => 'check_roles_any',
                 }
             }
         }
@@ -167,6 +169,8 @@ The DBIx::Class storage module has several configuration options
                     role_field => 'rolename',
                     ignore_fields_in_find => [ 'remote_name' ],
                     use_userdata_from_session => 1,
+                    check_roles   => 'check_roles',
+                    check_roles_any => 'check_roles_any',
                 }
             }
         }
@@ -259,6 +263,67 @@ has no bearing whatsoever in the initial authentication process.  Note also
 that if use_userdata_from_session is enabled, this config parameter
 is not used at all.
 
+=item check_roles
+
+If this option of set, checking the user has all the roles will be delegated to the
+specified method on the user row. This allows for you to override the role
+check, if you want to check virtual roles, or make super roles etc.
+
+You should set the value to the name of the method on the user row to call
+
+    __PACKAGE__->config('Plugin::Authentication' => {
+        realms => {
+            members => {
+                store => {
+                    check_roles   => 'custom_check_roles',
+                    check_roles_any => 'custom_check_roles_any',
+                }
+            }
+        }
+    });
+
+
+\@roles, and \@wanted_roles will be passed, where \@roles is the list of user roles
+and \@wanted_roles is the list of wanted roles.
+
+Should return true if user has the role.
+
+You will have to check the whole set yourself, eg this is the default behaviour
+when not setting 'check_roles'
+
+    use Set::Object;
+
+    sub custom_check_roles {
+        my ( $self, $roles, $wanted_roles ) = @_;
+
+        my $have = Set::Object->new(@$roles);
+        my $need = Set::Object->new(@$wanted_roles);
+
+        if ( $have->superset($need) ) {
+            return 1;
+        }
+    }
+
+=item check_roles_any
+
+Same as check_roles, except it's for checking that the user has at least one of
+the roles
+
+This is the default when check_roles_any is not set
+
+    use Set::Object;
+
+    sub custom_check_roles_any {
+        my ( $self, $roles, $wanted_roles ) = @_;
+
+        my $have = Set::Object->new(@$roles);
+        my $need = Set::Object->new(@$wanted_roles);
+
+        if ( $have->intersection($need)->size > 0 ) {
+            return 1;
+        }
+    }
+
 =back
 
 =head1 USAGE
index 18282a4..675e537 100644 (file)
@@ -116,10 +116,32 @@ sub supported_features {
 
     return {
         session         => 1,
-        roles           => 1,
+        roles           => {
+            self_check      => $self->config->{check_roles} || 0,,
+            self_check_any  => $self->config->{check_roles_any} || 0,
+        },
     };
 }
 
+#will only be used if $config->{check_roles} is set
+sub check_roles {
+    my ( $self, @wanted_roles ) = @_;
+
+    my @roles = $self->roles;
+    my $name = $self->config->{check_roles};
+
+    return $self->_user->$name( \@roles, \@wanted_roles );
+}
+
+#will only be used if $config->{check_roles_any} is set
+sub check_roles_any {
+    my ( $self, @wanted_roles ) = @_;
+
+    my @roles = $self->roles;
+    my $name = $self->config->{check_roles_any};
+
+    return $self->_user->$name( \@roles, \@wanted_roles );
+}
 
 sub roles {
     my ( $self ) = shift;
@@ -394,6 +416,20 @@ Delegates method calls to the underlying user row.
 
 Delegates handling of the C<< can >> method to the underlying user row.
 
+=head2 check_roles
+
+Calls the specified check_roles method on the underlying user row.
+
+Passes \@roles, \@wanted_roles, where @roles is the list of roles,
+and @wanted_roles is the list of wanted roles
+
+=head2 check_roles_any
+
+Calls the specified check_roles_any method on the underlying user row.
+
+Passes \@roles, \@wanted_roles, where @roles is the list of roles,
+and @wanted_roles is the list of wanted roles
+
 =head1 BUGS AND LIMITATIONS
 
 None known currently, please email the author if you find any.
index 0ecdc8f..bd0a149 100644 (file)
@@ -13,10 +13,6 @@ BEGIN {
         or plan skip_all =>
         "DBD::SQLite is required for this test";
 
-    eval { require DBIx::Class }
-        or plan skip_all =>
-        "DBIx::Class is required for this test";
-
     plan tests => 19;
 
     use TestApp;
index 4952ed4..eea0f9e 100644 (file)
@@ -17,10 +17,6 @@ BEGIN {
         or plan skip_all =>
         "DBD::SQLite is required for this test";
 
-    eval { require DBIx::Class }
-        or plan skip_all =>
-        "DBIx::Class is required for this test";
-
     eval { require Catalyst::Plugin::Session;
            die unless $Catalyst::Plugin::Session::VERSION >= 0.02 }
         or plan skip_all =>
index 7b5f1c8..2d3ebf7 100644 (file)
@@ -13,15 +13,11 @@ BEGIN {
         or plan skip_all =>
         "DBD::SQLite is required for this test";
 
-    eval { require DBIx::Class }
-        or plan skip_all =>
-        "DBIx::Class is required for this test";
-
     eval { require Catalyst::Plugin::Authorization::Roles }
         or plan skip_all =>
         "Catalyst::Plugin::Authorization::Roles is required for this test";
 
-    plan tests => 8;
+    plan tests => 10;
 
     use TestApp;
     TestApp->config( {
@@ -78,3 +74,9 @@ use Catalyst::Test 'TestApp';
     ok( my $res = request('http://localhost/user_login?username=nuffin&password=much&detach=is_admin_user'), 'request ok' );
     is( $res->content, 'failed', 'user is not an admin and a user' );
 }
+
+# test superuser role override fails (not enabled)
+{
+    ok( my $res = request('http://localhost/user_login?username=mark&password=secret&detach=is_admin'), 'request ok' );
+    is( $res->content, 'failed', 'user is not an admin' );
+}
index a021fca..f03922b 100644 (file)
@@ -13,15 +13,11 @@ BEGIN {
         or plan skip_all =>
         "DBD::SQLite is required for this test";
 
-    eval { require DBIx::Class }
-        or plan skip_all =>
-        "DBIx::Class is required for this test";
-
     eval { require Catalyst::Plugin::Authorization::Roles }
         or plan skip_all =>
         "Catalyst::Plugin::Authorization::Roles is required for this test";
 
-    plan tests => 8;
+    plan tests => 10;
 
     use TestApp;
     TestApp->config( {
@@ -77,3 +73,9 @@ use Catalyst::Test 'TestApp';
     ok( my $res = request('http://localhost/user_login?username=joeuser&password=hackme&detach=is_admin_user'), 'request ok' );
     is( $res->content, 'failed', 'user is not an admin and a user' );
 }
+
+# test superuser role override fails (not enabled)
+{
+    ok( my $res = request('http://localhost/user_login?username=graeme&password=supersecret&detach=is_admin'), 'request ok' );
+    is( $res->content, 'failed', 'user is not an admin' );
+}
index 2c8b1d3..a8ab01f 100644 (file)
@@ -17,10 +17,6 @@ BEGIN {
         or plan skip_all =>
         "DBD::SQLite is required for this test";
 
-    eval { require DBIx::Class }
-        or plan skip_all =>
-        "DBIx::Class is required for this test";
-
     eval { require Catalyst::Plugin::Session;
            die unless $Catalyst::Plugin::Session::VERSION >= 0.02 }
         or plan skip_all =>
index 25b85e7..2ecb38c 100644 (file)
@@ -13,10 +13,6 @@ BEGIN {
         or plan skip_all =>
         "DBD::SQLite is required for this test";
 
-    eval { require DBIx::Class }
-        or plan skip_all =>
-        "DBIx::Class is required for this test";
-
     eval { require Catalyst::Plugin::Authorization::Roles }
         or plan skip_all =>
         "Catalyst::Plugin::Authorization::Roles is required for this test";
index b13494f..f31a946 100644 (file)
@@ -13,10 +13,6 @@ BEGIN {
         or plan skip_all =>
         "DBD::SQLite is required for this test";
 
-    eval { require DBIx::Class }
-        or plan skip_all =>
-        "DBIx::Class is required for this test";
-
     eval { require Catalyst::Plugin::Authorization::Roles }
         or plan skip_all =>
         "Catalyst::Plugin::Authorization::Roles is required for this test";
index 7cc2e77..911aa7d 100644 (file)
@@ -17,10 +17,6 @@ BEGIN {
         or plan skip_all =>
         "DBD::SQLite is required for this test";
 
-    eval { require DBIx::Class }
-        or plan skip_all =>
-        "DBIx::Class is required for this test";
-
     eval { require Catalyst::Plugin::Session;
            die unless $Catalyst::Plugin::Session::VERSION >= 0.02 }
         or plan skip_all =>
diff --git a/t/12-auth-roles-relationship-self_check.t b/t/12-auth-roles-relationship-self_check.t
new file mode 100644 (file)
index 0000000..bc9e54d
--- /dev/null
@@ -0,0 +1,119 @@
+#!perl
+
+use strict;
+use warnings;
+use DBI;
+use File::Path;
+use FindBin;
+use Test::More;
+use lib "$FindBin::Bin/lib";
+
+BEGIN {
+    eval { require DBD::SQLite }
+        or plan skip_all =>
+        "DBD::SQLite is required for this test";
+
+    eval { require Catalyst::Plugin::Authorization::Roles }
+        or plan skip_all =>
+        "Catalyst::Plugin::Authorization::Roles is required for this test";
+
+    plan tests => 29;
+
+    use TestApp;
+    TestApp->config( {
+        name => 'TestApp',
+        authentication => {
+            default_realm => "users",
+            realms => {
+                users => {
+                    credential => {
+                        'class' => "Password",
+                        'password_field' => 'password',
+                        'password_type' => 'clear'
+                    },
+                    store => {
+                        'class' => 'DBIx::Class',
+                        'user_model' => 'TestApp::User',
+                        'role_relation' => 'roles',
+                        'role_field' => 'role',
+                        'check_roles'   => 't_check_roles',
+                        'check_roles_any' => 't_check_roles_any'
+                    },
+                },
+            },
+        },
+    } );
+
+    TestApp->setup(
+        qw/Authentication
+           Authorization::Roles
+           /
+    );
+}
+
+use Catalyst::Test 'TestApp';
+
+# test user's admin access
+{
+    ok( my $res = request('http://localhost/user_login?username=jayk&password=letmein&detach=is_admin'), 'request ok' );
+    is( $res->content, 'ok', 'user is an admin' );
+}
+
+# test unauthorized user's admin access
+{
+    ok( my $res = request('http://localhost/user_login?username=nuffin&password=much&detach=is_admin'), 'request ok' );
+    is( $res->content, 'failed', 'user is not an admin' );
+}
+
+# test multiple auth roles
+{
+    ok( my $res = request('http://localhost/user_login?username=jayk&password=letmein&detach=is_admin_user'), 'request ok' );
+    is( $res->content, 'ok', 'user is an admin and a user' );
+}
+
+# test multiple unauth roles
+{
+    ok( my $res = request('http://localhost/user_login?username=nuffin&password=much&detach=is_admin_user'), 'request ok' );
+    is( $res->content, 'failed', 'user is not an admin and a user' );
+}
+
+# test assert_any_user_role
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=nuffin&password=much&detach=is_any_admin_user'), 'request ok' );
+    is( $res->content, 'ok', 'user is user' );
+    is ( my @roles = $c->user->roles, 1, 'only 1 role' );
+    is ( $roles[0], 'user', 'role is user' );
+}
+
+# test assert_any_user_role
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=jayk&password=letmein&detach=is_any_admin_user'), 'request ok' );
+    is( $res->content, 'ok', 'user is user and an admin' );
+    is ( my @roles = $c->user->roles, 2, '2 roles' );
+    is ( $roles[0], 'admin', 'role is user' );
+    is ( $roles[1], 'user', 'role is admin' );
+}
+
+# test superuser role override
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=mark&password=secret&detach=is_admin_user'), 'request ok' );
+    is( $res->content, 'ok', 'superuser role is all roles' );
+    is ( my @roles = $c->user->roles, 1, 'only 1 role' );
+    is ( $roles[0], 'superadmin', 'role is user' );
+}
+
+# test superuser role override none existant roles
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=mark&password=secret&detach=is_nonexistant_roles'), 'request ok' );
+    is( $res->content, 'ok', 'superuser role is all roles' );
+    is ( my @roles = $c->user->roles, 1, 'only 1 role' );
+    is ( $roles[0], 'superadmin', 'role is user' );
+}
+
+# test superuser role override any none existant roles
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=mark&password=secret&detach=is_any_nonexistant_role'), 'request ok' );
+    is( $res->content, 'ok', 'superuser role is all roles' );
+    is ( my @roles = $c->user->roles, 1, 'only 1 role' );
+    is ( $roles[0], 'superadmin', 'role is user' );
+}
diff --git a/t/13-auth-roles-column-self_check.t b/t/13-auth-roles-column-self_check.t
new file mode 100644 (file)
index 0000000..8a663b0
--- /dev/null
@@ -0,0 +1,118 @@
+#!perl
+
+use strict;
+use warnings;
+use DBI;
+use File::Path;
+use FindBin;
+use Test::More;
+use lib "$FindBin::Bin/lib";
+
+BEGIN {
+    eval { require DBD::SQLite }
+        or plan skip_all =>
+        "DBD::SQLite is required for this test";
+
+    eval { require Catalyst::Plugin::Authorization::Roles }
+        or plan skip_all =>
+        "Catalyst::Plugin::Authorization::Roles is required for this test";
+
+    plan tests => 29;
+
+    use TestApp;
+    TestApp->config( {
+        name => 'TestApp',
+        authentication => {
+            default_realm => "users",
+            realms => {
+                users => {
+                    credential => {
+                        'class' => "Password",
+                        'password_field' => 'password',
+                        'password_type' => 'clear'
+                    },
+                    store => {
+                        'class' => 'DBIx::Class',
+                        'user_model' => 'TestApp::User',
+                        'role_column' => 'role_text',
+                        'check_roles'   => 't_check_roles',
+                        'check_roles_any' => 't_check_roles_any'
+                    },
+                },
+            },
+        },
+    } );
+
+    TestApp->setup(
+        qw/Authentication
+           Authorization::Roles
+           /
+    );
+}
+
+use Catalyst::Test 'TestApp';
+
+# test user's admin access
+{
+    ok( my $res = request('http://localhost/user_login?username=joeuser&password=hackme&detach=is_admin'), 'request ok' );
+    is( $res->content, 'ok', 'user is an admin' );
+}
+
+# test unauthorized user's admin access
+{
+    ok( my $res = request('http://localhost/user_login?username=jayk&password=letmein&detach=is_admin'), 'request ok' );
+    is( $res->content, 'failed', 'user is not an admin' );
+}
+
+# test multiple auth roles
+{
+    ok( my $res = request('http://localhost/user_login?username=nuffin&password=much&detach=is_admin_user'), 'request ok' );
+    is( $res->content, 'ok', 'user is an admin and a user' );
+}
+
+# test multiple unauth roles
+{
+    ok( my $res = request('http://localhost/user_login?username=joeuser&password=hackme&detach=is_admin_user'), 'request ok' );
+    is( $res->content, 'failed', 'user is not an admin and a user' );
+}
+
+# test assert_any_user_role
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=joeuser&password=hackme&detach=is_any_admin_user'), 'request ok' );
+    is( $res->content, 'ok', 'user is user' );
+    is ( my @roles = $c->user->roles, 1, 'only 1 role' );
+    is ( $roles[0], 'admin', 'role is admin' );
+}
+
+# test assert_any_user_role
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=nuffin&password=much&detach=is_any_admin_user'), 'request ok' );
+    is( $res->content, 'ok', 'user is user and an admin' );
+    is ( my @roles = $c->user->roles, 2, '2 roles' );
+    is ( $roles[0], 'user', 'role is user' );
+    is ( $roles[1], 'admin', 'role is admin' );
+}
+
+# test superuser role override
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=graeme&password=supersecret&detach=is_admin_user'), 'request ok' );
+    is( $res->content, 'ok', 'superuser role is all roles' );
+    is ( my @roles = $c->user->roles, 1, 'only 1 role' );
+    is ( $roles[0], 'superadmin', 'role is user' );
+}
+
+# test superuser role override none existant roles
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=graeme&password=supersecret&detach=is_nonexistant_roles'), 'request ok' );
+    is( $res->content, 'ok', 'superuser role is all roles' );
+    is ( my @roles = $c->user->roles, 1, 'only 1 role' );
+    is ( $roles[0], 'superadmin', 'role is user' );
+}
+
+# test superuser role override any none existant roles
+{
+    ok( my ( $res, $c )= ctx_request('http://localhost/user_login?username=graeme&password=supersecret&detach=is_any_nonexistant_role'), 'request ok' );
+    is( $res->content, 'ok', 'superuser role is all roles' );
+    is ( my @roles = $c->user->roles, 1, 'only 1 role' );
+    is ( $roles[0], 'superadmin', 'role is user' );
+}
index 3c1ba32..f2998de 100644 (file)
@@ -202,6 +202,46 @@ sub is_admin_user : Global {
     }
 }
 
+sub is_any_admin_user : Global {
+    my ( $self, $c ) = @_;
+
+    eval {
+        if ( $c->assert_any_user_role( qw/admin user/ ) ) {
+            $c->res->body( 'ok' );
+        }
+    };
+    if ($@) {
+        $c->res->body( 'failed' );
+    }
+}
+
+sub is_nonexistant_roles: Global {
+    my ( $self, $c ) = @_;
+
+    eval {
+        if ( $c->assert_user_roles( qw/madeUProle baconHater/ ) ) {
+            $c->res->body( 'ok' );
+        }
+    };
+    if ($@) {
+        $c->res->body( 'failed' );
+    }
+}
+
+sub is_any_nonexistant_role: Global {
+    my ( $self, $c ) = @_;
+
+    eval {
+        if ( $c->assert_any_user_role( qw/madeUProle baconHater/ ) ) {
+            $c->res->body( 'ok' );
+        }
+    };
+    if ($@) {
+        $c->res->body( 'failed' );
+    }
+}
+
+
 sub set_usersession : Global {
     my ( $self, $c, $value ) = @_;
     $c->user_session->{foo} = $value;
index 7abe403..3cba547 100644 (file)
@@ -27,11 +27,15 @@ my @deployment_statements = split /;/, q{
     INSERT INTO user VALUES (2, 'spammer', 'bob@spamhaus.com', 'broken', 'disabled', NULL, NULL);
     INSERT INTO user VALUES (3, 'jayk', 'j@cpants.org', 'letmein', 'active', NULL, NULL);
     INSERT INTO user VALUES (4, 'nuffin', 'nada@mucho.net', 'much', 'registered', 'user admin', NULL);
+    INSERT INTO user VALUES (5, 'mark', 'b@con.com', 'secret', 'active', NULL, NULL);
+    INSERT INTO user VALUES (6, 'graeme', 'gr@e.me', 'supersecret', 'active', 'superadmin', NULL);
     INSERT INTO role VALUES (1, 'admin');
     INSERT INTO role VALUES (2, 'user');
+    INSERT INTO role VALUES (3, 'superadmin');
     INSERT INTO user_role VALUES (1, 3, 1);
     INSERT INTO user_role VALUES (2, 3, 2);
-    INSERT INTO user_role VALUES (3, 4, 2)
+    INSERT INTO user_role VALUES (3, 4, 2);
+    INSERT INTO user_role VALUES (4, 5, 3)
 };
 
 __PACKAGE__->config(
index 7ad6ef8..7020de9 100644 (file)
@@ -18,4 +18,36 @@ __PACKAGE__->has_many( 'map_user_role' => 'TestApp::Schema::UserRole' => 'user'
 
 __PACKAGE__->many_to_many( roles => 'map_user_role', 'role');
 
+use Set::Object;
+
+sub t_check_roles {
+    my ( $self, $roles, $wanted_roles ) = @_;
+
+    if ( grep { $_ eq 'superadmin' } @$roles ) {
+        return 1;
+    }
+
+    my $have = Set::Object->new(@$roles);
+    my $need = Set::Object->new(@$wanted_roles);
+
+    if ( $have->superset($need) ) {
+        return 1;
+    }
+}
+
+sub t_check_roles_any {
+    my ( $self, $roles, $wanted_roles ) = @_;
+
+    if ( grep { $_ eq 'superadmin' } @$roles ) {
+        return 1;
+    }
+
+    my $have = Set::Object->new(@$roles);
+    my $need = Set::Object->new(@$wanted_roles);
+
+    if ( $have->intersection($need)->size > 0 ) {
+        return 1;
+    }
+}
+
 1;