NAME

SPOPS::Secure - Implement security across one or more classes of SPOPS objects

SYNOPSIS

package MySPOPS::Class;

use SPOPS::Secure qw( :all ); # import the security constants

@MySPOPS::Class::ISA = qw( SPOPS::Secure SPOPS::DBI );

DESCRIPTION

By adding this module into the @ISA variable for your SPOPS class, you implement a mostly transparent per-object security system. This security system relies on a few things being implemented:

  • A SPOPS class implementing users

  • A SPOPS class implementing groups

  • A SPOPS class implementing security objects

Easy, eh? Fortunately, SPOPS comes with all three, although you are free to modify them as you see fit.

Overview of Security

Security is implemented with a number of methods that are called within the SPOPS implementation module. For instance, every time you call fetch() on an object, the system first determines whether you have rights to do so. Similar callbacks are located in save() and remove(). If you do not either define the method in your SPOPS implementation or use this module, the action will always be allowed.

We use the Unix-style of permission scheme, separating the scope into: USER, GROUP and WORLD from most- to least-specific. (This is abbreviated as U/G/W.) When we check permissions, we check whether a security level is defined for the most-specific item first, then work our way up to the least_specific. (We use the term 'scope' frequently in the module and documentation -- a 'specific scope' is a particular user or group, or the world.)

Even though we use the U/G/W scheme from Unix, we are not constrained by its history. There is no strict 'ownership' assigned to an object as there is to a Unix file. Instead, an object can have assigned to it permissions from any number of users, and any number of groups.

There are three settings for any object combined with a specific scope:

NONE:  The scope is barred from even seeing the object.
READ:  The scope can read the object but not save it.
WRITE: The scope can read, write and delete the object.

(To be explicit: WRITE permission implies READ permission as well; if a scope has WRITE permission for an object, it can do anything with it, including remove it.)

Security Rules

With security, there are some important assumptions. These rules are laid out here.

  • The most specific security wins. This means that you might have set permissions on an object to be SEC_LEVEL_WRITE for SEC_LEVEL_WORLD, but if the user who is logged in has SEC_LEVEL_NONE, permission will be denied.

  • All objects must have a WORLD permission. Configuration for your SPOPS object must include the initial_security hash. The only required field is 'WORLD', which defines the default WORLD permission for newly-created objects. If you do not include this, the system will automatically set the WORLD permission to SEC_LEVEL_NONE, which is probably not what you want.

For instance, look at an object that represents a news notice posted:

Object Class: MyApp::News
Object ID:    1625

------------------------------------------------
| SCOPE | SCOPE_ID |  NONE  |  READ  |  WRITE  |
------------------------------------------------
| USER  | 71827    |        |   X    |         |
| USER  | 6351     |   X    |        |         |
| USER  | 9182     |        |        |    X    |
| GROUP | 762      |        |   X    |         |
| GROUP | 938      |        |        |    X    |
| WORLD |          |        |   X    |         |
------------------------------------------------

>From this, we can say:

  • User 6351 can never view this notice. Even though the user might be a part of a group that can; even though WORLD has READ permission. Since the user is explicitly forbidden from viewing the notice, nothing else matters.

  • If a different User (say, 21092) who belongs to both Group 762 and Group 938 tries to determine permission for this object, that User will have WRITE permission since the system returns the highest permission granted by all Group memberships.

  • Any user who is not specified here and who does not belong to either Group 762 or Group 938 will get READ permission to the object, reverting to the permission for the scope WORLD.

Setting Security for Created Objects

The Unix paradigm of file permissions assumes several things.

User and Group Objects

It is a fundamental tenet of this persistence framework that it should have no idea what your application looks like. However, since we deal with user and group objects, it is necessary to enforce some standards.

  • Must be able to retrieve the ID of the object with the method call 'id'. The ID value can be numeric or it can be a string, but it must have 16 or fewer characters.

  • Must be able to get an arrayref of members. With a group object, you must implement a method that returns users called 'user'. Similarly, your user object must implement a method that returns the groups that user belongs to via the method 'group':

    # Note that 'login_name' is not required as a 
    # parameter; this is just an example
    my $user_members = eval { $group->user };
    foreach my $user ( @{ $user_members } ) {
      print "Username is $user->{login_name}\n";
    }
    
    # Note that 'name' is not required as a 
    # parameter; this is just an example
    my $groups = eval { $user->group };
    foreach my $group ( @{ $groups } ) {
      print "Group name is $group->{name}\n";
    }
  • Must be able to retrieve the logged-in user (and, by the rule stated above, the groups that user belongs to). This is done via the global_user_current method call. The SPOPS object or other class must be able to fulfill this method and return a user object.

METHODS

The methods that this class implements can be used by any SPOPS class. The variable $item below refers to the fact that you can either do an object method call or a class method call. If you do a class method call, you must pass in the ID of the object for which you want to get or set security.

However, you may also implement security on the class level as well. For instance, if your application uses classes to implement modules within an application, you might wish to restrict the module by security very similar to the security implemented for individual objects. In this case, you would have a class name and no object ID (oid) value.

To do so, simply make the class a subclass of SPOPS::Secure. All the methods remain exactly the same.

check_security( [ \%params ] )

The method get_security() returns a code corresponding to the LEVEL constants exported from this package. This code tells you what permissions the logged in user has. You can also pass user and group parameters to check security for other items as well.

Note that you can check security for multiple groups but only one user at a time. Passing an arrayref of user objects for the 'user' parameter will result in the first user object being checked and the remainder discarded. This is unlikely to be what you need.

Examples:

# Find the permission for the currently logged-in user for $item
$item->check_security();

# Get the security for this $item for a particuar
# user; note that this *does* find the groups this
# user belongs to and checks those as well
$item->check_security( user => $user );

# Find the security for this item for either of the
# groups specified
$item->check_security( group => [ $group, $group ] );

get_security( [ \%params ] )

Returns a hashref of security information about the particular class or object. The keys of the hashref are the constants, SEC_SCOPE_WORLD, SEC_SCOPE_GROUP and SEC_SCOPE_USER. The value corresponding to the SEC_SCOPE_WORLD key is simply the WORLD permission for the object or class. Similarly, the value of SEC_SCOPE_USER is the permission for the user specified. The SEC_SCOPE_GROUP key has as its value a hashref with the IDs of the group as keys. (Examples below)

Note that if the user specified does not have permissions for the class/object, then its entry is blank.

The parameters correspond to check_security. The default is to retrieve the security for the currently logged-in user and groups (plus WORLD), but you can restrict the output if necessary.

Note that the WORLD key is always set, no matter how much you restrict the user/groups.

Finally: this will not be on the test, since you will probably not need to use this very often. The check_security() and set_security() methods are likely the only interfaces you need with security whether it be object or class-based. The get_security() method is used primarily for internal purposes, but you might also need it if you are writing security administration tools.

Examples:

# Return a hashref using the currently logged-in
# user and the groups the user belongs to
# 
# Sample of what $perm looks like:
# $perm = { 'u' => 4, 'w' => 1, 'g' => { 5162 => 4, 7182 => 8 } };
# 
# Which means that the user has a permission of SEC_LEVEL_READ,
# the user belongs to two groups with IDs 5162 and 7182 which have
# permissions of READ and WRITE, respectively, and the WORLD
# permission is NONE.
my $perm = $item->get_security(); 

# Find the security for a particular user object and its groups
my $perm = $item->get_security( user => $that_user );

# Find the security for two groups, no user objects.
my $perm = $item->get_security( group => [ $group1, $group2 ] );

get_security_scopes( \%params )

Called by get_security() to determine which scopes to use to check security on an object.

set_security( \%params )

The method set_security() returns a status as to whether the permission has been set to what you requested.

The default is to operate on one item at a time, but you can specify many items at once with the 'multiple' parameter.

Examples:

# Set $item security for WORLD to READ

my $wrv =  $item->set_security( scope => SEC_SCOPE_WORLD, 
                                level => SEC_LEVEL_READ );
unless ( $wrv ) {
  # error! security not set properly
}

# Set $item security for GROUP $group to WRITE

my $grv =  $item->set_security( scope => SEC_SCOPE_GROUP,
                                scope_id => $group->id,
                                level => SEC_LEVEL_WRITE );
unless ( $grv ) {
  # error! security not set properly
}

# Set $item security for USER objects whose IDs are the keys in the
# hash %multiple and whose values are the levels corresponding to the
# ID.
#
# (Note that this is a contrived example for setting up the %multiple
# hash - you should always do some sort of validation/checking before
# passing user-specified information to a method.)

my %multiple = (
 $user1->id => $cgi->param( 'level_' . $user1->id ),
 $user2->id => $cgi->param( 'level_' . $user2->id ) 
);
my $rv = $item->set_security( scope => SEC_SCOPE_USER,
                              level => \%multiple );
if ( $rv != scalar( keys %multiple ) ) {
  # error! security not set properly for all items
}

# Set $item security for multiple scopes whose values
# are in the hash %multiple; note that the hash %multiple
# has a separate layer now since we're specifying multiple
# scopes within it.

my %multiple = (
 SEC_SCOPE_USER() => {
    $user1->id => $cgi->param( 'level_' . $user1->id ),
    $user2->id => $cgi->param( 'level_' . $user2->id ),
 },
 SEC_SCOPE_GROUP() => {
    $group1->id  => $cgi->param( 'level_group_' . $group1->id ),
 },
);
my $rv = $item->set_security( scope => [ SEC_SCOPE_USER, SEC_SCOPE_GROUP ],
                              level => \%multiple );

create_initial_security( \%params )

Creates the initial security for an object. This can be simple, or this can be complicated :) It is designed to be flexible enough for us to easily plug-in security policy modules whenever we write them, but simple enough to be used just from the object configuration.

Object security configuration information is specified in the 'creation_security' hashref in the object configuration. A typical setup might look like:

creation_security => {
   u   => undef,
   g   => { level => { 3 => 'WRITE' } },
   w   => { level => 'READ'},
},

Each of the keys maps to a (hopefully intuitive) scope:

u = SEC_SCOPE_USER
g = SEC_SCOPE_GROUP
w = SEC_SCOPE_WORLD

For each scope you can either name security specifically or you can defer the decision-making process to a subroutine. The former is called 'exact specification' and the latter 'code specification'. Both are described below.

Note that the 'level' values used ('WRITE' or 'READ' above) do not match up to the SEC_LEVEL_* values exported from this module. Instead they are just handy mnemonics to use -- just lop off the 'SEC_LEVEL_' from the exported variable:

SEC_LEVEL_NONE  = 'NONE'
SEC_LEVEL_READ  = 'READ'
SEC_LEVEL_WRITE = 'WRITE'

Exact specification

'Exact specification' does exactly that -- you specify the ID and security level of the users and/or groups, along with one for the 'world' scope if you like. This is handy for smaller sites where you might have a small number of groups.

The exact format is:

SCOPE => { level => { ID => LEVEL,
                      ID => LEVEL, ... } }

Where 'SCOPE' is 'u' or 'g', 'ID' is the ID of the group/user and 'LEVEL' is the level you want to assign to that group/user. So using our example above:

g   => { level => { 3 => 'WRITE' } },

We assign the security level 'SEC_LEVEL_WRITE' to the group with ID 3.

You can also use shortcuts.

For the SEC_SCOPE_USER scope, if you specify a level:

u    => { level => 'READ' }

Then that security level is assigned for the user who created the object.

For the SEC_SCOPE_GROUP scope, if you specify a level:

g    => { level => 'READ' }

Then that security level is assigned for all of the groups to which the user who created the object belongs.

If you specify anything other than a level for the SEC_SCOPE_WORLD scope, the system will discard the entry.

Code specificiation

You can also assign the entire process off to a separate routine:

creation_security => {
   code => [ 'My::Package' => 'security_set' ]
},

This code should return a hashref formatted like this

{ 
  u => SEC_LEVEL_*,
  g => { gid => SEC_LEVEL_* },
  w => SEC_LEVEL_*
}

If you do not include a scope in the hashref, no security information for that scope will be entered.

Parameters:

class
  Specify the class you want to use to create the initial security.

oid
  Specify the object ID you want to use to create the initial
  security.

TAGS FOR SCOPE/LEVEL

This module exports nothing by default. You can import specific tags that refer to the scope and level, or you can import groups of them.

Note that you should always use these tags. They may seem unwieldly, but they make your program easier to read and allow us to modify the values for these behind the scenes without you modifying any of your code. If you use the values directly, you will get what is coming to you.

You can import individual tags like this:

use SPOPS::Secure qw( SEC_SCOPE_WORLD );

Or you can import the tags in groups like this:

use SPOPS::Secure qw( :scope );

Scope Tags

  • SEC_SCOPE_WORLD

  • SEC_SCOPE_GROUP

  • SEC_SCOPE_USER

Level Tags

  • SEC_LEVEL_NONE

  • SEC_LEVEL_READ

  • SEC_LEVEL_WRITE

Verbose Level Tags

These tags return a text value for the different security levels.

  • SEC_LEVEL_VERBOSE_NONE (returns 'NONE')

  • SEC_LEVEL_VERBOSE_READ (returns 'READ')

  • SEC_LEVEL_VERBOSE_WRITE (returns 'WRITE')

Groups of Tags

  • scope: brings in all SEC_SCOPE tags

  • level: brings in all SEC_LEVEL tags

  • verbose: brings in all SEC_LEVEL_*_VERBOSE tags

  • all: brings in all tags

TO DO

Sort out the different set_* methods

The different set_* methods are currently quite confusing.

Add SUMMARY level

Think about adding a SUMMARY level of security. This would allow, for instance, search results to bring up an object and display a title and perhaps more (controlled by the object), but forbid actually viewing the entire object.

Add caching

Gotta gotta gotta get a caching interface done, where we simply say:

$object->cache_security_level( $user );

And cache the security level for that object for that user. **Any** security modifications to that object wipe out the cache for that object.

BUGS

COPYRIGHT

Copyright (c) 2001 intes.net, inc.. All rights reserved.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

AUTHORS

Chris Winters <chris@cwinters.com>