Show Contents Previous Page Next Page Chapter 6 - Authentication and Authorization / Authorization Handlers 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 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 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 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'; 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 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 PageAuthorization 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 PageCopyright © 1999 by O'Reilly & Associates, Inc. |
HIVE: All information for read only. Please respect copyright! |