package RT::Authen::ExternalAuth::LDAP;

use Net::LDAP qw(LDAP_SUCCESS LDAP_PARTIAL_RESULTS);
use Net::LDAP::Util qw(ldap_error_name);
use Net::LDAP::Filter;

use strict;

require Net::SSLeay if $RT::ExternalServiceUsesSSLorTLS;

sub GetAuth {
    
    my ($service, $username, $password) = @_;
    
    my $config = $RT::ExternalSettings->{$service};
    $RT::Logger->debug( "Trying external auth service:",$service);

    my $base            = $config->{'base'};
    my $filter          = $config->{'filter'};
    my $group           = $config->{'group'};
    my $group_attr      = $config->{'group_attr'};
    my $attr_map        = $config->{'attr_map'};
    my @attrs           = ('dn');

    # Empty parentheses as filters cause Net::LDAP to barf.
    # We take care of this by using Net::LDAP::Filter, but
    # there's no harm in fixing this right now.
    if ($filter eq "()") { undef($filter) };

    # Now let's get connected
    my $ldap = _GetBoundLdapObj($config);
    return 0 unless ($ldap);

    $filter = Net::LDAP::Filter->new(   '(&(' . 
                                        $attr_map->{'Name'} . 
                                        '=' . 
                                        $username . 
                                        ')' . 
                                        $filter . 
                                        ')'
                                    );

    $RT::Logger->debug( "LDAP Search === ",
                        "Base:",
                        $base,
                        "== Filter:", 
                        $filter->as_string,
                        "== Attrs:", 
                        join(',',@attrs));

    my $ldap_msg = $ldap->search(   base   => $base,
                                    filter => $filter,
                                    attrs  => \@attrs);

    unless ($ldap_msg->code == LDAP_SUCCESS || $ldap_msg->code == LDAP_PARTIAL_RESULTS) {
        $RT::Logger->debug( "search for", 
                            $filter->as_string, 
                            "failed:", 
                            ldap_error_name($ldap_msg->code), 
                            $ldap_msg->code);
        # Didn't even get a partial result - jump straight to the next external auth service
        return 0;
    }

    unless ($ldap_msg->count == 1) {
        $RT::Logger->info(  $service,
                            "AUTH FAILED:", 
                            $username,
                            "User not found or more than one user found");
        # We got no user, or too many users.. jump straight to the next external auth service
        return 0;
    }

    my $ldap_dn = $ldap_msg->first_entry->dn;
    $RT::Logger->debug( "Found LDAP DN:", 
                        $ldap_dn);

    # THIS bind determines success or failure on the password.
    $ldap_msg = $ldap->bind($ldap_dn, password => $password);

    unless ($ldap_msg->code == LDAP_SUCCESS) {
        $RT::Logger->info(  $service,
                            "AUTH FAILED", 
                            $username, 
                            "(can't bind:", 
                            ldap_error_name($ldap_msg->code), 
                            $ldap_msg->code, 
                            ")");
        # Could not bind to the LDAP server as the user we found with the password
        # we were given, therefore the password must be wrong so we fail and
        # jump straight to the next external auth service
        return 0;
    }

    # The user is authenticated ok, but is there an LDAP Group to check?
    if ($group) {
        # If we've been asked to check a group...
        $filter = Net::LDAP::Filter->new("(${group_attr}=${ldap_dn})");
        
        $RT::Logger->debug( "LDAP Search === ",
                            "Base:",
                            $base,
                            "== Filter:", 
                            $filter->as_string,
                            "== Attrs:", 
                            join(',',@attrs));
        
        $ldap_msg = $ldap->search(  base   => $group,
                                    filter => $filter,
                                    attrs  => \@attrs,
                                    scope  => 'base');

        # And the user isn't a member:
        unless ($ldap_msg->code == LDAP_SUCCESS || 
                $ldap_msg->code == LDAP_PARTIAL_RESULTS) {
            $RT::Logger->critical(  "Search for", 
                                    $filter->as_string, 
                                    "failed:",
                                    ldap_error_name($ldap_msg->code), 
                                    $ldap_msg->code);

            # Fail auth - jump to next external auth service
            return 0;
        }

        unless ($ldap_msg->count == 1) {
            $RT::Logger->info(  $service,
                                "AUTH FAILED:", 
                                $username);
                                
            # Fail auth - jump to next external auth service
            return 0;
        }
    }
    
    # Any other checks you want to add? Add them here.

    # If we've survived to this point, we're good.
    $RT::Logger->info(  (caller(0))[3], 
                        "External Auth OK (",
                        $service,
                        "):", 
                        $username);
    return 1;

}


sub CanonicalizeUserInfo {
    
    my ($service, $key, $value) = @_;

    my $found = 0;
    my %params = (Name         => undef,
                  EmailAddress => undef,
                  RealName     => undef);

    # Load the config
    my $config = $RT::ExternalSettings->{$service};
   
    # Figure out what's what
    my $base            = $config->{'base'};
    my $filter          = $config->{'filter'};

    # Get the list of unique attrs we need
    my @attrs = values(%{$config->{'attr_map'}});

    # This is a bit confusing and probably broken. Something to revisit..
    my $filter_addition = ($key && $value) ? "(". $key . "=$value)" : "";
    if(defined($filter) && ($filter ne "()")) {
        $filter = Net::LDAP::Filter->new(   "(&" . 
                                            $filter . 
                                            $filter_addition . 
                                            ")"
                                        ); 
    } else {
        $RT::Logger->debug( "LDAP Filter invalid or not present.");
    }

    unless (defined($base)) {
        $RT::Logger->critical(  (caller(0))[3],
                                "LDAP baseDN not defined");
        # Drop out to the next external information service
        return ($found, %params);
    }

    # Get a Net::LDAP object based on the config we provide
    my $ldap = _GetBoundLdapObj($config);

    # Jump to the next external information service if we can't get one, 
    # errors should be logged by _GetBoundLdapObj so we don't have to.
    return ($found, %params) unless ($ldap);

    # Do a search for them in LDAP
    $RT::Logger->debug( "LDAP Search === ",
                        "Base:",
                        $base,
                        "== Filter:", 
                        $filter->as_string,
                        "== Attrs:", 
                        join(',',@attrs));

    my $ldap_msg = $ldap->search(base   => $base,
                                 filter => $filter,
                                 attrs  => \@attrs);

    # If we didn't get at LEAST a partial result, just die now.
    if ($ldap_msg->code != LDAP_SUCCESS and 
        $ldap_msg->code != LDAP_PARTIAL_RESULTS) {
        $RT::Logger->critical(  (caller(0))[3],
                                ": Search for ",
                                $filter->as_string,
                                " failed: ",
                                ldap_error_name($ldap_msg->code), 
                                $ldap_msg->code);
        # $found remains as 0
        
        # Drop out to the next external information service
        $ldap_msg = $ldap->unbind();
        if ($ldap_msg->code != LDAP_SUCCESS) {
            $RT::Logger->critical(  (caller(0))[3],
                                    ": Could not unbind: ", 
                                    ldap_error_name($ldap_msg->code), 
                                    $ldap_msg->code);
        }
        undef $ldap;
        undef $ldap_msg;
        return ($found, %params);
      
    } else {
        # If there's only one match, we're good; more than one and
        # we don't know which is the right one so we skip it.
        if ($ldap_msg->count == 1) {
            my $entry = $ldap_msg->first_entry();
            foreach my $key (keys(%{$config->{'attr_map'}})) {
                if ($RT::LdapAttrMap->{$key} eq 'dn') {
                    $params{$key} = $entry->dn();
                } else {
                    $params{$key} = 
                      ($entry->get_value($config->{'attr_map'}->{$key}))[0];
                }
            }
            $found = 1;
        } else {
            # Drop out to the next external information service
            $ldap_msg = $ldap->unbind();
            if ($ldap_msg->code != LDAP_SUCCESS) {
                $RT::Logger->critical(  (caller(0))[3],
                                        ": Could not unbind: ", 
                                        ldap_error_name($ldap_msg->code), 
                                        $ldap_msg->code);
            }
            undef $ldap;
            undef $ldap_msg;
            return ($found, %params);
        }
    }
    $ldap_msg = $ldap->unbind();
    if ($ldap_msg->code != LDAP_SUCCESS) {
        $RT::Logger->critical(  (caller(0))[3],
                                ": Could not unbind: ", 
                                ldap_error_name($ldap_msg->code), 
                                $ldap_msg->code);
    }

    undef $ldap;
    undef $ldap_msg;

    return ($found, %params);
}

sub UserExists {
    my ($username,$service) = @_;
   $RT::Logger->debug("UserExists params:\nusername: $username , service: $service"); 
    my $config              = $RT::ExternalSettings->{$service};
    
    my $base                = $config->{'base'};
    my $filter              = $config->{'filter'};

    # While LDAP filters must be surrounded by parentheses, an empty set
    # of parentheses is an invalid filter and will cause failure
    # This shouldn't matter since we are now using Net::LDAP::Filter below,
    # but there's no harm in doing this to be sure
    if ($filter eq "()") { undef($filter) };

    if (defined($config->{'attr_map'}->{'Name'})) {
        # Construct the complex filter
        $filter = Net::LDAP::Filter->new(           '(&' . 
                                                    $filter . 
                                                    '(' . 
                                                    $config->{'attr_map'}->{'Name'} . 
                                                    '=' . 
                                                    $username . 
                                                    '))'
                                        );
    }

    my $ldap = _GetBoundLdapObj($config);
    return unless $ldap;

    my @attrs = values(%{$config->{'attr_map'}});

    # Check that the user exists in the LDAP service
    $RT::Logger->debug( "LDAP Search === ",
                        "Base:",
                        $base,
                        "== Filter:", 
                        $filter->as_string,
                        "== Attrs:", 
                        join(',',@attrs));
    
    my $user_found = $ldap->search( base    => $base,
                                    filter  => $filter,
                                    attrs   => \@attrs);

    if($user_found->count < 1) {
        # If 0 or negative integer, no user found or major failure
        $RT::Logger->debug( "User Check Failed :: (",
                            $service,
                            ")",
                            $username,
                            "User not found");   
        return 0;  
    } elsif ($user_found->count > 1) {
        # If more than one result returned, die because we the username field should be unique!
        $RT::Logger->debug( "User Check Failed :: (",
                            $service,
                            ")",
                            $username,
                            "More than one user with that username!");
        return 0;
    }
    undef $user_found;
    
    # If we havent returned now, there must be a valid user.
    return 1;
}

sub UserDisabled {

    my ($username,$service) = @_;

    # FIRST, check that the user exists in the LDAP service
    unless(UserExists($username,$service)) {
        $RT::Logger->debug("User (",$username,") doesn't exist! - Assuming not disabled for the purposes of disable checking");
        return 0;
    }
    
    my $config          = $RT::ExternalSettings->{$service};
    my $base            = $config->{'base'};
    my $filter          = $config->{'filter'};
    my $d_filter        = $config->{'d_filter'};
    my $search_filter;

    # While LDAP filters must be surrounded by parentheses, an empty set
    # of parentheses is an invalid filter and will cause failure
    # This shouldn't matter since we are now using Net::LDAP::Filter below,
    # but there's no harm in doing this to be sure
    if ($filter eq "()") { undef($filter) };
    if ($d_filter eq "()") { undef($d_filter) };

    unless ($d_filter) {
        # If we don't know how to check for disabled users, consider them all enabled.
        $RT::Logger->debug("No d_filter specified for this LDAP service (",
                            $service,
                            "), so considering all users enabled");
        return 0;
    }

    if (defined($config->{'attr_map'}->{'Name'})) {
        # Construct the complex filter
        $search_filter = Net::LDAP::Filter->new(   '(&' . 
                                                    $filter . 
                                                    $d_filter . 
                                                    '(' . 
                                                    $config->{'attr_map'}->{'Name'} . 
                                                    '=' . 
                                                    $username . 
                                                    '))'
                                                );
    } else {
        $RT::Logger->debug("You haven't specified an LDAP attribute to match the RT \"Name\" attribute for this service (",
                            $service,
                            "), so it's impossible look up the disabled status of this user (",
                            $username,
                            ") so I'm just going to assume the user is not disabled");
        return 0;
        
    }

    my $ldap = _GetBoundLdapObj($config);
    next unless $ldap;

    # We only need the UID for confirmation now, 
    # the other information would waste time and bandwidth
    my @attrs = ('uid'); 
    
    $RT::Logger->debug( "LDAP Search === ",
                        "Base:",
                        $base,
                        "== Filter:", 
                        $search_filter->as_string,
                        "== Attrs:", 
                        join(',',@attrs));
          
    my $disabled_users = $ldap->search(base   => $base, 
                                       filter => $search_filter, 
                                       attrs  => \@attrs);
    # If ANY results are returned, 
    # we are going to assume the user should be disabled
    if ($disabled_users->count) {
        undef $disabled_users;
        return 1;
    } else {
        undef $disabled_users;
        return 0;
    }
}
# {{{ sub _GetBoundLdapObj

sub _GetBoundLdapObj {

    # Config as hashref
    my $config = shift;

    # Figure out what's what
    my $ldap_server     = $config->{'server'};
    my $ldap_user       = $config->{'user'};
    my $ldap_pass       = $config->{'pass'};
    my $ldap_tls        = $config->{'tls'};
    my $ldap_ssl_ver    = $config->{'ssl_version'};
    my $ldap_args       = $config->{'net_ldap_args'};
    
    my $ldap = new Net::LDAP($ldap_server, @$ldap_args);
    
    unless ($ldap) {
        $RT::Logger->critical(  (caller(0))[3],
                                ": Cannot connect to",
                                $ldap_server);
        return undef;
    }

    if ($ldap_tls) {
        $Net::SSLeay::ssl_version = $ldap_ssl_ver;
        # Thanks to David Narayan for the fault tolerance bits
        eval { $ldap->start_tls; };
        if ($@) {
            $RT::Logger->critical(  (caller(0))[3], 
                                    "Can't start TLS: ",
                                    $@);
            return;
        }

    }

    my $msg = undef;

    if (($ldap_user) and ($ldap_pass)) {
        $msg = $ldap->bind($ldap_user, password => $ldap_pass);
    } elsif (($ldap_user) and ( ! $ldap_pass)) {
        $msg = $ldap->bind($ldap_user);
    } else {
        $msg = $ldap->bind;
    }

    unless ($msg->code == LDAP_SUCCESS) {
        $RT::Logger->critical(  (caller(0))[3], 
                                "Can't bind:", 
                                ldap_error_name($msg->code), 
                                $msg->code);
        return undef;
    } else {
        return $ldap;
    }
}

# }}}

1;
