Elements of Access Control
by Vladi Belperchinov-Shabanski
|
Pages: 1, 2, 3
The login procedure must match the LOGIN_WEEKENDS policy before allowing user to continue with other operations. Thus, you need a procedure for reading policy configuration files:
our %ACCESS_POLICY;
sub read_access_config
{
my $fn = shift; # config file name
open( my $f, $fn );
while( <$f> )
{
chomp;
next unless /\S/; # skip whitespace
next if /^[;#]/; # skip comments
die "Syntax error: $_\n" unless /^\s*(\S+?):\s*(.+)$/;
my $n = uc $1; # policy name: LOGIN_WEEKENDS
my $v = $2; # groups lsit: 1+3, 4, 1+5+9
# return list of lists:
# outer list uses comma separator, inner lists use plus sign separator
$ACCESS_POLICY{ $n } = access_policy_parse( $v );
}
close( $f );
}
sub access_policy_parse
{
my $policy = shift;
return [ map { [ split /[\s\+]+/ ] } split /[\s,]+/, $policy ];
}
For the LOGIN_WEEKENDS policy, the resulting value in %ACCESS_POLICY will be:
$ACCESS_POLICY{ 'LOGIN_WEEKENDS' } =>
[
[ '1', '3' ],
[ '4' ],
[ '1', '5', '9' ]
];
To match this policy, a user must be in every groups listed in any of the inner lists:
sub check_policy
{
my $policy = shift;
my $out_arr = $ACCESS_POLICY{ $policy };
die "Invalid policy name; $policy\n" unless $out_arr;
return check_policy_tree( $out_arr );
}
sub check_policy_tree
{
my $out_arr = shift;
for my $in_arr ( @$out_arr )
{
my $c = 0; # matching groups count
for my $group ( @$in_arr )
{
$c++ if $USER_GROUPS{ $group };
}
# matching groups is equal to all groups count in this list
# policy match!
return 1 if $c == @$in_arr;
}
# if this code is reached then policy didn't match
return 0;
}
The example cases will become:
sub user_login
{
# login checks here
...
# login ok, check weekday policy
my $wday = (localtime())[6];
my $policy;
if( $wday == 0 or $wday == 6 )
{
$policy = 'LOGIN_WEEKEND';
}
else
{
$policy = 'LOGIN_WEEKDAY';
}
die "Login denied" unless check_policy( $policy );
}
sub edit_data
{
# require user to be in group 1 (admin) to edit data...
die "Access denied" unless check_policy( 'EDIT' );
# user allowed, 'EDIT' policy match
...
}
Now you have all the parts of a working access control scheme:
- Policy configuration syntax
- Policy parser
- User group storage and mapping
- User group loading
- Policy match function
This scheme may seem complete, but it lacks one thing.
Data Fences
In a multiuser system there is always some kind of ownership on the data stored in the database. This means that each user must see only those parts of the data that his user groups own.
This ownership problem solution is separate from the policy scheme. Each row must have one or more fields filled with groups that have access to the data. Any SQL statements for reading data must also check for this field:
my $rg = join ',', grep { $USER_GROUPS{ $_ } } keys %USER_GROUPS;
my $ug = join ',', grep { $USER_GROUPS{ $_ } } keys %USER_GROUPS;
my $sql = "SELECT * FROM TABLE_NAME
WHERE READ_GROUP IN ( $rg ) AND UPDATE_GROUP IN ( $ug )";
The result set will contain only rows with read and update groups inside the current user's group set. Sometimes you may need all of rows with the same read group for display, even though some of those rows have update restrictions the user does not meet. This case will use only the READ_GROUP field for select and will cut off users when they try to update the record without permission:
my $rg = join ',', grep { $USER_GROUPS{ $_ } } keys %USER_GROUPS;
my $sql = "SELECT * FROM TABLE_NAME WHERE READ_GROUP IN ( $rg )";
$sth = $dbh->prepare( $sql );
$sth->execute();
$hr = $sth->fetchrow_hashref();
die "Edit access denied" unless check_access( $hr->{ 'UPDATE_GROUP' } );
When access checks are explicitly after SELECT statements it is possible to store full policy strings inside CHAR fields:
$hr = $sth->fetchrow_hashref();
die "Edit access denied" unless check_policy_record( $hr, 'UPDATE_GROUP' );
sub check_policy_record
{
my $hr = shift; # hash with record data
my $field = shift; # field containing policy string
my $policy = $hr->{ $field };
my $tree = access_policy_parse( $policy );
return check_policy_tree( $tree );
}
In the Middle of Nowhere
This access control scheme is simple and usable as described. It does not cover all possible cases of access control, but every application has its own unique needs. In certain cases, you can push some of these access controls to lower levels -- your database, for example -- depending on your needs. Good luck with building your own great wall!
You must be logged in to the O'Reilly Network to post a talkback.

