# $Id: AuthzLDAP.pm,v 1.2 2000/06/01 16:18:54 cgilmore Exp cgilmore $
#
# Author          : Jason Bodnar, Christian Gilmore
# Created On      : Apr 04 12:04:00 CDT 2000
# Status          : Functional
# 
# PURPOSE
#    LDAP Group Authentication
#
###############################################################################


# Package name
package Tivoli::Apache::AuthzLDAP;


# Required libraries
use strict;
use Apache::Constants ':common';
use Apache::Log ();
use Net::LDAP qw(:all);


# Global variables
$Tivoli::Apache::AuthLDAP::VERSION = '0.23';
my $Ld;


###############################################################################
###############################################################################
# check_group: check user membership in group
###############################################################################
###############################################################################
sub check_group {
  my ($r, $elog, $ld, $basedn, $memberattr, $username, $userdn,
      $groups, $nested_groups) = @_;
  my @groups = ();
  my ($group, $member) = undef;
  my $inquote = 0;
  my $length = length($groups);
  my $nextquote = 0;
  my $nextspace = 0;
  my $pos = 0;

  # shrink whitespace sets to just a single space
  $groups =~ s/\s+/ /g;

  # Extract groups from list
  while ($pos < $length) {
    $nextquote = index($groups, '"', $pos);
    $nextspace = index($groups, ' ', $pos);
    $nextspace = $length if $nextspace < 0;
    $nextquote = $length if $nextquote < 0;
    if ($inquote) {
      push(@groups, substr($groups, $pos, $nextquote - $pos));
      $pos = $nextquote + 2;
      $inquote = 0;
    } elsif ($nextspace < $nextquote) {
      push(@groups, substr($groups, $pos, $nextspace - $pos));
      $pos = $nextspace + 1;
    } else {
      $inquote = 1;
      $pos = $nextquote + 1;
    }
  }

  foreach $group (@groups) {
    # Look up the group
    my $filter = qq((cn=$group));
    $elog->debug("Iterating over group $group");
    $elog->debug("Using filter: $filter");
    my $msg = $ld->search(base => $basedn, filter => $filter);
    unless ($msg->code == LDAP_SUCCESS) {
      $r->note_basic_auth_failure;
      $r->log_reason("user $username: Could not search for $group: " . 
		     $msg->code . " " . $msg->error, $r->uri);
      return SERVER_ERROR;
    }
    
    # Did we get any entries?
    unless ($msg->count) {
      $elog->debug("user $username: could not find group $group");
      next;
    }
    
    # Check the group
    my $entry = $msg->first_entry; # Only want one
    my $dn = $entry->dn;
    next unless ($dn =~ /ou=group/);
    $elog->debug("Checking group $dn for $userdn");
    my $msg = $ld->compare($dn, attr => $memberattr, value => $userdn);
    return (OK, $group) if $msg->code == LDAP_COMPARE_TRUE;
    $elog->debug("Checking group $dn for $username");
    my $msg = $ld->compare($dn, attr => $memberattr, value => $username);
    return (OK, $group) if $msg->code == LDAP_COMPARE_TRUE;
  
    # Return undef if nested groups is not set
    $r->log_reason("Could not find $username in $group", $r->uri);
    next unless $nested_groups =~ /on/i;
    
    # If we did not find the person in the group let's check the groups members
    foreach $member ($entry->get($memberattr)) {
      if ($member =~ /ou=group/ || !($member =~ /ou=/)) {
	my ($result, $group) = check_group($r, $elog, $ld, $basedn, 
					   $memberattr, $username, 
					   $userdn, "\"$member\"",
					   $nested_groups);
	return (OK, $group) if $result == OK;
      }
    }
  }
  
  # We've fallen through without finding the user in the group
  $r->log_reason("Could not find $username in $groups", $r->uri);
  return AUTH_REQUIRED;
}


###############################################################################
###############################################################################
# handler: hook into Apache/mod_perl API
###############################################################################
###############################################################################
sub handler {
  my $r = shift;
  my $elog = $r->log;
  my $requires = $r->requires;
  return OK unless $requires;
  
  my $username = $r->connection->user;
  my $basedn = $r->dir_config('AuthzBaseDN') || "";
  my $ldapserver = $r->dir_config('LDAPServer') || "localhost";
  my $ldapport = $r->dir_config('LDAPPort') || 389;
  my $memberattr = $r->dir_config('MemberAttr') || 'member';
  my $setremoteuser = $r->dir_config('SetRemoteUser') || 'cn';
  my $resetremoteuser = $r->dir_config('ResetRemoteUser');
  my $nested_groups = $r->dir_config('NestedGroups');
  $elog->debug(join ", ", "AuthzBaseDN=$basedn", "LDAPServer=$ldapserver", 
	       "MemberAttr=$memberattr", "ResetRemoteUser=$resetremoteuser", 
	       "NestedGroups=$nested_groups");

  for my $req (@$requires) {
    my ($require, $rest) = split /\s+/, $req->{requirement}, 2;
    
    if ($require eq "user") { return OK 
				if grep $username eq $_, split /\s+/, $rest} 
    elsif ($require eq "valid-user") { return OK }
    elsif ($require eq 'group') {
      # Connect to the server
      unless ($Ld = new Net::LDAP($ldapserver,port => $ldapport)) {
	$r->note_basic_auth_failure;
	$r->log_reason("user $username: LDAP Connection Failed",$r->uri);
	return SERVER_ERROR;
      }
      
      # Bind anonymously
      my $msg = $Ld->bind;
      unless ($msg->code == LDAP_SUCCESS) {
	$r->note_basic_auth_failure;
	$r->log_reason("user $username: LDAP Initial Bind Failed: " . 
		       $msg->code . " " . $msg->error, $r->uri);
	return SERVER_ERROR;
      }

      # Get user DN
      $msg = $Ld->search(base   => $basedn, 
			 filter => qq($setremoteuser=$username));
      unless ($msg->code == LDAP_SUCCESS) {
	$r->note_basic_auth_failure;
	$r->log_reason("user $username doesn't exist in LDAP " . 
		       $msg->code . " " . $msg->error, $r->uri);
	return SERVER_ERROR;
      }
      my $userdn = $msg->first_entry->dn;
      
      # Compare the username
      my ($result, $group) = check_group($r, $elog, $Ld, $basedn, 
					 $memberattr, $username, $userdn, 
					 $rest, $nested_groups);
      return $result unless $result == OK;
      
      # Reset username if necessary
      $r->connection->user($r->notes("AuthLDAP")) if $resetremoteuser;
      
      # Everything's A-OK
      $r->subprocess_env(REMOTE_GROUP => $group);
      return OK;
    }
  }
}

1;

__END__

# $Log: AuthzLDAP.pm,v $
# Revision 1.2  2000/06/01 16:18:54  cgilmore
# added Log message
#


=head1 NAME

Tivoli::Apache::AuthzLDAP - mod_perl LDAP Authorization Module

=head1 SYNOPSIS

    <Directory /foo/bar>
    # Authorization Realm and Type (only Basic supported)
    AuthName "Foo Bar"
    AuthType Basic

    # Any of the following variables can be set.  Defaults are listed
    # to the right.
    PerlSetVar AuthzBaseDN     o=Tivoli Systems   # Default: none
    PerlSetVar LDAPServer      ldap.foo.com       # Default: localhost
    PerlSetVar LDAPPort        389                # Default: 389
    PerlSetVar MemberAttr      uid                # Default: member
    PerlSetVar ResetRemoteUser On                 # Default: none
    PerlSetVar NestedGroups    On                 # Default: none

    PerlAuthzHandler Tivoli::Apache::AuthzLDAP

    require group "My Group" GroupA "Group B"
    </Directory>

    These directives can also be used in a .htaccess file.

= head1 DESCRIPTION

This perl module is designed to work with mod_perl and
Net::LDAP. It can be used with any authentication module but
works best with Tivoli::Apache::AuthenLDAP.

The parameters are pretty much self-explanatory except for
ResetRemoteUser and NestedGroups.

When ResetRemoteUser is turned on Tivoli::Apache::AuthzLDAP will
look for a note containing the original uid entered in the
password dialog box. You may want to use this when SetRemoteUser
is used in Tivoli::Apache::AuthenLDAP.

When NestedGroups is on Tivoli::Apache::AuthzLDAP will do a
recursive group search until the user is found in a group or the
deepest groups member list does not contain any groups.

=head1 AUTHORS

Jason Bodnar <jbodnar@tivoli.com>
Christian Gilmore <cgilmore@tivoli.com>

=cut


