home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 6 - Authentication and Authorization / Authorization Handlers
Advanced Gender-Based Authorization

A dissatisfying feature of Apache::AuthzGender is that when an unauthorized user finally gives up and presses the cancel button, Apache displays the generic "Unauthorized" error page without providing any indication of why the user was refused access. Fortunately this is easy to fix with a custom error response. We can call the request object's custom_response() method to display a custom error message, an HTML page, or the output of a CGI script when the AUTH_REQUIRED error occurs.

Another problem with Apache::AuthzGender is that it uses a nonstandard way to configure the authorization scheme. The standard authorization schemes use a require directive as in:

require group authors

At the cost of making our module slightly more complicated, we can accommodate this too, allowing access to the protected directory to be adjusted by any of the following directives:

require gender F            # allow females
require user Webmaster Jeff # allow Webmaster or Jeff
require valid-user          # allow any valid user

Example 6-10 shows an improved Apache::AuthzGender that implements these changes. The big task is to recover and process the list of require directives. To retrieve the directives, we call the request object's requires() method. This method returns an array reference corresponding to all of the require directives in the current directory and its parents. Rather than being a simple string, however, each member of this array is actually a hash reference containing two keys: method_mask and requirement. The requirement key is easy to understand. It's simply all the text to the right of the require directive (excluding comments). You'll process this text according to your own rules. There's nothing magical about the keywords user, group, or valid-user.

The method_mask key is harder to explain. It consists of a bit mask indicating what methods the require statement should be applied to. This mask is set when there are one or more <LIMIT> sections in the directory's configuration. The GET, PUT, POST, and DELETE methods correspond to the first through fourth bits of the mask (counting from the right). For example, a require directive contained within a <LIMIT GET POST> section will have a method mask equal to binary 0101, or decimal 5. If no <LIMIT> section is present, the method mask will be -1 (all bits set, all methods restricted). You can test for particular bits using the method number constants defined in the :methods section of Apache::Constants. For example, to test whether the current mask applies to POST requests, you could write a piece of code like this one (assuming that the current requires() is in $_):

 if ($_->{method_mask} & (1 << M_POST)) {
  warn "Current requirements apply to POST";
}

In practice, you rarely have to worry about the method mask within your own authorization modules because mod_perl automatically filters out any require statement that wouldn't apply to the current transaction.

In the example given earlier, the array reference returned by requires() would look like this:

[
 {
   requirement => 'gender F',
   method_mask => -1
 },
 {
   requirement => 'user Webmaster Jeff',
   method_mask => -1
 },
 {
   requirement => 'valid-user',
   method_mask => -1
 }
]

The revised module begins by calling the request object's requires() method and storing it in a lexical variable $requires:

    my $r = shift;
   my $requires = $r->requires;
   return DECLINED unless $requires;

If requires() returns undef, it means that no require statements were present, so we decline to handle the transaction. (This shouldn't actually happen, but it doesn't hurt to make sure.) The script then recovers the user's name and guesses his or her gender, as before.

Next we begin our custom error message:

    my $explanation = <<END;
<TITLE>Unauthorized</TITLE>
<H1>You Are Not Authorized to Access This Page</H1>
Access to this page is limited to:
<OL>
END

The message will be in a text/html page, so we're free to use HTML formatting. The error warns that the user is unauthorized, followed by a numbered list of the requirements that the user must meet in order to gain access to the page (Figure 6-2). This will help us confirm that the requirement processing is working correctly.

Figure 6-2. The custom error message generated by Apache::AuthzGender specifically lists the requirements that the user has failed to satisfy.

Now we process the requirements one by one by looping over the array contained in $requires:

    for my $entry (@$requires) {
      my($requirement, @rest) = split /\s+/, $entry->{requirement};

For each requirement, we extract the text of the require directive and split it on whitespace into the requirement type and its arguments. For example, the line require gender M would result in a requirement type of gender and an argument of M. We act on any of three different requirement types. If the requirement equals user, we loop through its arguments seeing if the current user matches any of the indicated usernames. If a match is found, we exit with an OK result code:

       if (lc $requirement eq 'user') {
          foreach (@rest) { return OK if $user eq $_; }
          $explanation .= "<LI>Users @rest.\n";
      }

If the requirement equals gender, we loop through its arguments looking to see whether the user's gender is correct and again return OK if a match is found:8

       elsif (lc $requirement eq 'gender') {
          foreach (@rest) { return OK if $guessed_gender eq uc $_; }
          $explanation .= "<LI>People of the @G{@rest} persuasion.\n";
      }

Otherwise, if the requirement equals valid-user, then we simply return OK because the authentication module has already made sure of this for us:

       elsif (lc $requirement eq 'valid-user') {
          return OK;
      }
   }
   $explanation .= "</OL>";

As we process each require directive, we add a line of explanation to the custom error string. We never use this error string if any of the requirements are satisfied, but if we fall through to the end of the loop, we complete the ordered list and set the explanation as the response for AUTH_REQUIRED errors by passing the explanation string to the request object's custom_response() method:

     $r->custom_response(AUTH_REQUIRED, $explanation);

The module ends by noting and logging the failure, and returning an AUTH_REQUIRED status code as before:

   $r->note_basic_auth_failure;
   $r->log_reason("user $user: not authorized", $r->filename);
   return AUTH_REQUIRED;
}

The logic of this module places a logical OR between the requirements. The user is allowed access to the site if any of the require statements is satisfied, which is consistent with the way Apache handles authorization in its standard modules. However, you can easily modify the logic so that all requirements must be met in order to allow the user access.

Example 6-10. An Improved Apache::AuthzGender

package Apache::AuthzGender2;
use strict;
use Text::GenderFromName qw(gender);
use Apache::Constants qw(:common);
my %G = ('M' => "male", 'F' => "female");
sub handler {
   my $r = shift;
   my $requires = $r->requires;
   return DECLINED unless $requires;
   my $user = ucfirst lc $r->connection->user;
   my $guessed_gender = uc(gender($user)) || 'M';
    my $explanation = <<END;
<TITLE>Unauthorized</TITLE>
<H1>You Are Not Authorized to Access This Page</H1>
Access to this page is limited to:
<OL>
END
    for my $entry (@$requires) {
      my($requirement, @rest) = split /\s+/, $entry->{requirement};
      if (lc $requirement eq 'user') {
          foreach (@rest) { return OK if $user eq $_; }
          $explanation .= "<LI>Users @rest.\n";
      }
      elsif (lc $requirement eq 'gender') {
          foreach (@rest) { return OK if $guessed_gender eq uc $_; }
          $explanation .= "<LI>People of the @G{@rest} persuasion.\n"; 
} elsif (lc $requirement eq 'valid-user') { return OK; } }
    $explanation .= "</OL>";
    $r->custom_response(AUTH_REQUIRED, $explanation);
   $r->note_basic_auth_failure;
   $r->log_reason("user $user: not authorized", $r->filename);
   return AUTH_REQUIRED;
}
1;
__END__

   Show Contents   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.