The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Decision::ACL - Manage and Build Access Control Lists

SYNOPSIS

  use Decision::ACL;
  use Decision::ACL::Rule;
  use Decision::ACL::Constants qw(:rule);

  my $Acl = Decision::ACL->new();

  my $rule = Decision::ACL::Rule({
                                        action => 'allow',
                                        now => 0,
                                        fields =>
                                        {
                                                field1 => 'field1val',
                                                field2 => 'field2val',
                                                ...
                                        }
                                });

   ...


   $Acl->PushRule($rule);

   my $return_status = $Acl->RunACL({
                                        field1 => 'testfield1value',
                                        field2 => 'testfield2value',
                                        ...
                                        });

   if($return_status == ACL_RULE_ALLOW)
   {
        print "testfield1value, testfield2value allowed!\n";
   }


   $Acl->PushRule($rule);               Push a rule onto the Decision::ACL rule list.
   $Acl->PopRule();                     Pop a rule from the Decision::ACL rule list.
   $Acl->ShiftRule();                   Shift a rule from the Decision::ACL rule list.
   $Acl->UnshiftRule($rule);            Unshift a rule onto the Decision::ACL rule list.
   $Acl->Rules();                       Return an arrayref of the rule objects in this rule list.
   $Acl->RunACL({ args });              Run the list, Returns ACL_RULE_ALLOW or ACL_RULE_DENY.
   
   $rule->Fields();                     Returns a has reference of the fields and values for this rule.
   $rule->Now();                        Wether this rule has to be applied now or not. 1 or 0.
   $rule->Action();                     Returns the action for this rule, either ALLOW or DENY.
   $rule->Concerned({});                Is rule concerned by data ? Returns ACL_RULE_CONCERNED or ACL_RULE_UNCONCERNED
   $rule->Control({});                  Test rule against data, returns status of rule.

DESCRIPTION

This module's purpose is to provide an already implemented ACL logic for programmers. Most of the time writing access control list scripts is long and boring. This set of modules has all the convenient logic behind access control lists and provide an easy interface to it. It allows you to build custom ACL's, and provide the mechanisms to run the ACL against data.

INSTALLATION

        perl Makefile.PL
        make
        make test
        make install

IMPLEMENTATION

The Decision::ACL set of modules is implemented in pure perl, with very simple behaviour. The main idea behind it is that a ACL object tests each rule in it's list against the target data. Each rule, if concerned applies it's action.

Decision::ACL Implementation

This class is simply is a list of Decision::ACL::Rule objects, in a particular order. Once the rules have been "pushed" onto the ACL, the RunACL() methods will take the arguments defined in the rule objects and execute each rule in order and get their return values. The final return value of the RunACL() method is determined if there are no denying rule, and that an explicit allowing rule has been encountered. If no rules are concerned by the data, the ACL will deny automatically.

Decision::ACL::Rule Implementation

This module implements a basic rule. It holds a list of fields, (wich will be propagated up to the ACL) and a list of values for each fields. It also contains an "action" (ALLOW | DENY). The logic behind the rule is that when the Control() method is called with arguments, the rule will check first it is CONCERNED by the data. If it is, it applies it's action (ALLOW | DENY). If not, it return UNCONCERNED and the ACL RunACL() method continues to the next rule.

Basic Usage

Here is a description of a basic use of this module set.

Decision::ACL

To create an initial empty rule list, simply use the new() constructor.

        my $Acl = Decision::ACL->new();

This build an empty Decision::ACL rule list ready to receive rule objects. You can also directly pass an anonymous array of rule objects to new() like this:

        my $Acl = Decision::ACL->new([ $rule1, $rule2, $rule3 ]);

The list now needs to be populated with rules so that it can become useful. To do this, you can either write yourself a small parser, get your rules from a database, a DBM file or any way you want. The only thing you need to know, is how to put these in the rule list and manage that rule list. The Decision::ACL module provides multiple methods to deal with the list of rules:

        $Acl->PushRule($rule);                  Push a rule onto the Decision::ACL rule list.
        $Acl->PopRule();                        Pop a rule from the Decision::ACL rule list.
        $Acl->ShiftRule();                      Shift a rule from the Decision::ACL rule list.
        $Acl->UnshiftRule($rule);               Unshift a rule onto the Decision::ACL rule list.

The last step is to run the ACL and ask each rule if they are concerned by the data you pass to them.

        my $return_status = $Acl->RunACL({
                                                field1 => 'testf1val',
                                                field2 => 'testd2val',
                                        });

The arguments passed to RunACL() are checked to see if they are consistent with the fields defined in the rules themselves. When the first rule is "pushed" onto the rule list, Decision::ACL will scan it's Fields() and keep a list of them internally. When RunACL() is ranned, each parameter has to be present, and no unknown parameters can be passed. This is true only if DIE_ON_BAD_PARAMETERS is set to 1 in the class. The same applies for PushRule(), the Fields() of the rules consequent to the first rule are checked for inconsistencies. This behaviour is controlled by the DIE_ON_MALFORMED_RULES, set either to 1 or 0 in the class.

The return status are defined in the Decision::ACL::Constants package under the keyword "rule". They are ACL_RULE_ALLOW or ACL_RULE_DENY, so you can test the return of the run and deal with it accordingly in your application.

I suggest you import the constants in your namespace when dealing with this suite.

        use Decision::ACL::Constants qw(:rule);

Decision::ACL::Rule

Rules objects are created using the new() constructor.

        my $rule = Decision::ACL::Rule->new({
                                                action => 'deny',
                                                now => 1,
                                                fields  =>
                                                {
                                                        fieldname => 'fieldvalue',
                                                        fieldname2 => 'field2value',
                                                },
                                        });

The arguments passed to the new() constructor are as follows:

  • action, Action to be applied when rule is concerned, allow, deny, permit or block.

  • now, Wether rule should act when encountered and concerned.

  • fields, The fields of the rule with their values for this rule. To be tested against data.

Once the rule object has been created, it can be pushed onto an ACL for execution. This object has mainly 4 methods to access and modify the object at runtime.

   $rule->Fields();                     Returns a has reference of the fields and values for this rule.
   $rule->Now();                        Wether this rule has to be applied now or not. 1 or 0.
   $rule->Action();                     Returns the action for this rule, either ALLOW or DENY.
   $rule->Concerned({});                Is rule concerned by data ? Returns ACL_RULE_CONCERNED or ACL_RULE_UNCONCERNED
   $rule->Control({});                  Test rule against data, returns status of rule.

The Fields() method is used to store and retrieve the fields specified for this rule and their associated values. The Now() method return or sets the value of the "now" parameter of the rule. When a rule is set to act "now", the ACL will stop the RunACL() at this level and directly return the Action() of the rule. Concerned({}) will tell wether the rule is concerned by the data that is passed to it. It will test each field data against the values of the same fields in Fields() and return 1 or 0/undef.

The Control({}) method is what is called by the ACL in RunACL() to get the status of the rule on a certain set of the fields passed. Control() will first call Concerned({}) to see if the rule is concerned by the dataset, if not Control() exits with status of ACL_RULE_UNCONCERNED. If it is concerned, the Control() method will return the status matching the Action() of the rule.

Real life example

Ok, this all sounds nice and fun, but let's see how we really can use this in real life. This example is a script I wrote to use as a "precommit" script for CVS. It implements an advanced rule system for CVS repository access. This script gets called everytime someone commits to CVS and gets the username, repository and file in wich the person wants to commit. The script simply parses a rule file that contains a very simple rule language, creates the rule objects, pushes them in the ACL and runs the ACL with the values passed by CVS. It then will permit or deny the commit.

commitcheck script

        #!/usr/bin/perl
        # commitcheck using Decision::ACL.
        #
        #Copyright (c) 2001 Benoit Beausejour <bbeausej@pobox.com> All rights reserved. This program is
        #free software, you can redistribute it and/or modify it under the same terms as Perl itself.
        #
                
        use strict;
        use Data::Dumper;
        
        use Decision::ACL;
        use Decision::ACL::Rule;
        use Decision::ACL::Constants qw(:rule);
        
        
        #The user's CVSROOT.
        my $cvsroot = $ENV{'CVSROOT'};
        $cvsroot .= "/";
        
        #The username of the current user.
        my $username = system('id -un');
        chomp $username;

        #The repository being commited into.
        my $repository = shift;
        $repository =~ s/$cvsroot//g;
        
        #The file being commited.
        my $module = shift;
        
        my $rule_file = $cvsroot."CVSROOT/commit_acl";
        
        open(RULES, $rule_file) || &failed("Cant open $rule_file: $!\n");

        #Create the ACL list object.    
        my $ACL = new Decision::ACL();
        
        my @rules = <RULES>;
        foreach (reverse @rules)
        {
                next if substr($_,0,1) eq '#' || !$_;
        
                chomp $_;
        
                my ($rule_base, $rule_spec) = split(/to/i, $_);
        
                my ($action, $target);
                my $nowflag = 0;
        
                if($rule_base =~ /now/i)
                {
                        ($action, $target) = split(/now/i, $rule_base);
                        $nowflag++;
                }
                else
                {
                        ($action, $target) = split(/ /i, $rule_base);
                }
        
                
                my ($repository, $module) = split(/in/i, $rule_spec);
        
        
        
        
                $module = '' if not defined $module;
                $action = uc $action;
                $action =~ s/ //g;
                $repository =~ s/ //g;
                $module =~ s/ //g if defined $module;
                $target =~ s/ //g;
                $repository = uc $repository if $repository =~ /^all$/i;
                $module = uc $module if $module =~ /^all$/i;
                $target = uc $target if $target =~ /^all$/i;

                #Create a Decision::ACL::Rule object from the data parsed in the rule file.     
                my $rule = new Decision::ACL::Rule({
                                                now => $nowflag, 
                                                action => $action,
                                                fields =>
                                                {
                                                        repository => $repository,      
                                                        component => $module,
                                                        username => $target,
                                                }
                                        });

                #Push that rule onto the ACL.
                $ACL->PushRule($rule);  
        }

        #Run the ACL, get the return and give it back to CVS.   
        my $return_status = $ACL->RunACL({
                        repository => $repository, 
                        component => $module, 
                        username =>  $username,
                });
        
        if($return_status == ACL_RULE_ALLOW)
        {
                exit(0);
        }
        else
        {
                print STDERR "Commit to $repository in module $module DENIED for user $username\n";
                exit(1);
        }
        exit(1);
        
        
        sub failed
        {
                my $message = shift;
                print STDERR $message;
                exit(1);
        }

Sample rule file for commitcheck Here is an example rule file for this script:

        deny all to all in all
        allow root to CVSROOT in all
        allow bbeausej to CVSROOT in commit_acl
        allow fred to Decision-ACL in LICENSE

This script is usable, if you find it useful don't be afraid to use it in your daily CVS uses. To use it, simply put this line in your $CVSROOT/CVSROOT/commitinfo file:

        DEFAULT /path/to/commitcheck

Then create your rulefile named $CVSROOT/CVSROOT/commit_acl.

I hope this example shows you how simple it is to use Decision::ACL in real life examples.

TODO

This module is evolving rapidly. I am already writing version 1.0 of it wich will contain a RecDescent parser for ACL files with a generic dynamic grammar. So, be prepared for the next versions as the module called Decision::ACL::Parser will be released along with the package.

I can see many other things go in this module, generic parsers for specific types of ACL, passing them through the 2 main classes, many ideas on the table. If you have anything idea that you want to implement, please do and let me know about it. I'll be glad to integrate your work here if needed.

AUTHOR

Benoit, "SaKa" Beausejour, <bbeausej@pobox.com>

NOTES

This module was made possible by the help of individuals:

- #perl (ers) for their help and support.

COPYRIGHT

Copyright (c) 2001 Benoit Beausejour <bbeausej@pobox.com> All rights reserved. This program is free software, you can redistribute it and/or modify it under the same terms as Perl itself.

SEE ALSO

perl(1), Solaris::ACL(1),