Show Contents Previous Page Next Page
Chapter 6 - Authentication and Authorization / Authorization Handlers Authorizing Against a Database In most real applications you'll be authorizing users against a database of
some sort. This section will show you a simple scheme for doing this that works
hand-in-glove with the Apache::AuthTieDBI database authentication system
that we set up in the "Authenticating Against a Database"
section earlier in this chapter. To avoid making you page backward, we repeat
the contents of the test database here:
+-----------+---------------+-------+---------------------+
| user_name | passwd | level | groups |
+-----------+---------------+-------+---------------------+
| fred | 8uUnFnRlW18qQ | 2 | users,devel |
| andrew | No9eULpnXZAjY | 2 | users |
| george | V8R6zaQuOAWQU | 3 | users |
| winnie | L1PKv.rN0UmsQ | 3 | users,authors,devel |
| root | UOY3rvTFXJAh2 | 5 | users,authors,admin |
| morgana | 93EhPjGSTjjqY | 1 | users |
+-----------+---------------+-------+---------------------+
The module is called Apache::AuthzTieDBI, and the idea is to allow for require statements like these:
require $user_name eq 'fred'
require $level >=2 && $groups =~ /\bauthors\b/;
require $groups =~/\b(users|admin)\b/
Each require directive consists of an arbitrary Perl expression. During evaluation, variable names are replaced by the name of the corresponding column in the database. In the first example above, we require the username to be exactly fred. In the second case, we allow access by any user whose level is greater than or equal to 2 and who belongs to the authors group. In the third case, anyone whose groups field contains either of the strings users or admin is allowed in. As in the previous examples, the require statements are ORed with each other. If multiple require statements are present, the user has to satisfy only one of them in order to be granted access. The directive require valid-user is treated as a special case and not evaluated as a Perl expression.
Example 6-11 shows the code to accomplish
this. Much of it is stolen directly out of Apache::AuthTieDBI, so we
won't review how the database is opened and tied to the %DB hash.
The interesting part begins about midway down the handler() method:
if ($DB{$user}) { # evaluate each requirement
for my $entry (@$requires) {
my $op = $entry->{requirement};
return OK if $op eq 'valid-user';
$op =~ s/\$\{?(\w+)\}?/\$DB{'$user'}{$1}/g;
return OK if eval $op;
$r->log_error($@) if $@;
}
}
After making sure that the user actually exists in the database, we loop through each of the require statements and recover its raw text. We then construct a short string to evaluate, replacing anything that looks like a variable with the appropriate reference to the tied database hash. We next call eval() and return OK if a true value is returned. If none of the require statements evaluates to true, we log the problem, note the authentication failure, and return AUTH_REQUIRED . That's all there is to it!
Although this scheme works well and is actually quite flexible in practice, you should be aware of one small problem before you rush off and implement it on your server. Because the module is calling eval() on Perl code read in from the configuration file, anyone who has write access to the file or to any of the per-directory .htaccess files can make this module execute Perl instructions with the server's privileges. If you have any authors at your site whom you don't fully trust, you might think twice about making this facility available to them.
A good precaution would be to modify this module to use the Safe module. Add the following to the top of the module:
use Safe ();
sub safe_eval {
package main;
my($db, $code) = @_;
my $cpt = Safe->new;
local *DB = $db;
$cpt->share('%DB', '%Tie::DBI::', '%DBI::', '%DBD::');
return $cpt->reval($code);
}
The safe_eval() subroutine creates a safe compartment and shares the %DB , %Tie::DBI:: , %DBI:: , and %DBD:: namespaces with it (the list of namespaces to share was identified by trial and error). It then evaluates the require code in the safe compartment using Safe::reval().
To use this routine, modify the call to eval() in the inner loop to call save_eval():
return OK if safe_eval(\%DB, $op);
The code will now be executed in a compartment in which dangerous calls like system() and unlink() have been disabled. With suitable modifications to the shared namespaces, this routine can also be used in other places where you might be tempted to run eval().
Example 6-11. Authorization Against
a Database with Apache::AuthzTieDBI
package Apache::AuthzTieDBI;
# file: Apache/AuthTieDBI.pm
use strict;
use Apache::Constants qw(:common);
use Tie::DBI ();
sub handler {
my $r = shift;
my $requires = $r->requires;
return DECLINED unless $requires;
my $user = $r->connection->user;
# get configuration information
my $dsn = $r->dir_config('TieDatabase') || 'mysql:test_www'; my $table_data = $r->dir_config('TieTable') || 'users:user:passwd';
my($table, $userfield, $passfield) = split ':', $table_data;
tie my %DB, 'Tie::DBI', {
db => $dsn, table => $table, key => $userfield,
} or die "couldn't open database";
if ($DB{$user}) { # evaluate each requirement
for my $entry (@$requires) {
my $op = $entry->{requirement};
return OK if $op eq 'valid-user';
$op =~ s/\$\{?(\w+)\}?/\$DB{'$user'}{$1}/g;
return OK if eval $op;
$r->log_error($@) if $@;
}
}
$r->note_basic_auth_failure;
$r->log_reason("user $user: not authorized", $r->filename);
return AUTH_REQUIRED;
}
1;
__END__
An access.conf entry to go along with this module might look like this:
<Location /registered_users>
AuthName Enlightenment
AuthType Basic
PerlAuthenHandler Apache::AuthTieDBI
PerlSetVar TieDatabase mysql:test_www
PerlSetVar TieTable user_info:user_name:passwd
PerlAuthzHandler Apache::AuthzTieDBI
require $user_name eq 'fred'
require $level >=2 && $groups =~ /authors/;
</Location>
Before going off and building a 500,000 member authentication database around this module, please realize that it was developed to show the flexibility of using Perl expressions for authentication rather than as an example of the best way to design group membership databases. If you are going to use group membership as your primary authorization criterion, you would want to normalize the schema so that the user's groups occupied their own table:
+-----------+------------+
| user_name | user_group |
+-----------+------------+
| fred | users |
| fred | devel |
| andrew | users |
| george | users |
| winnie | users |
| winnie | authors |
| winnie | devel |
+-----------+------------+
You could then test for group membership using a SQL query and the full DBI API.
Authentication and Authorization's Relationship with Subrequests Show Contents Go to Top Previous Page Next Page If you have been trying out the examples so far, you may notice that the authentication
and authorization handlers are called more than once for certain requests. Chances
are, these requests have been for a / directory, where the actual
file sent back is one configured with the DirectoryIndex directive,
such as index.html or index.cgi. For each file listed in the
DirectoryIndex configuration, Apache will run a subrequest to determine
if the file exists and has sufficent permissions to use in the response. As
we learned in Chapter 3, The Apache Module
Architecture and API, a subrequest will trigger the various
request phase handlers, including authentication and authorization. Depending
on the resources required to provide these services, it may not be desirable
for the handlers to run more than once for a given HTTP request. Auth handlers
can avoid being called more than once by using the is_initial_req()
method, for example:
sub handler {
my $r = shift;
return OK unless $r->is_initial_req;
...
With this test in place, the main body of the handler will only be run once per HTTP request, during the very first internal request. Note that this approach should be used with caution, taking your server access configuration into consideration.
Binding Authentication to Authorization Show Contents Go to Top Previous Page Next Page
Authorization and authentication work together. Often, as we saw in the previous example, you find PerlAuthenHandler and PerlAuthzHandlers side by side in the same access control section. If you have a pair of handlers that were designed to work together, and only together, you simplify the directory configuration somewhat by binding the two together so that you need only specify the authentication handler.
To accomplish this trick, have the authentication handler call push_handlers() with a reference to the authorization handler code before it exits. Because the authentication handler is always called before the authorization handler, this will temporarily place your code on the handler list. After processing the transaction, the authorization handler is set back to its default.
In the case of Apache::AuthTieDBI and Apache::AuthzTieDBI, the only change we need to make is to place the following line of code in Apache::AuthTieDBI somewhere toward the top of the handler subroutine:
$r->push_handlers(PerlAuthzHandler => \&Apache::AuthzTieDBI::handler);
We now need to bring in Apache::AuthTieDBI only. The authorization handler will automatically come along for the ride:
<Location /registered_users>
AuthName Enlightenment
AuthType Basic
PerlAuthenHandler Apache::AuthTieDBI
PerlSetVar TieDatabase mysql:test_www
PerlSetVar TieTable user_info:user_name:passwd
require $user_name eq 'fred'
require $level >=2 && $groups =~ /authors/;
</Location>
Since the authentication and authorization modules usually share common code, it might make sense to merge the authorization and authentication handlers into the same .pm file. This scheme allows you to do that. Just rename the authorization subroutine to something like authorize() and keep handler() as the entry point for the authentication code. Then at the top of handler() include a line like this:
$r->push_handlers(PerlAuthzHandler => \&authorize);
We can now remove redundant code from the two handlers. For example, in the
Apache::AuthTieDBI modules, there is common code that retrieves the
per-directory configuration variables and opens the database. This can now be
merged into a single initialization subroutine. Footnotes 8 Because there are only two genders, looping
through all the require directive's arguments is overkill, but
we do it anyway to guard against radical future changes in biology. Show Contents Go to Top Previous Page Next Page Copyright © 1999 by O'Reilly & Associates, Inc. |