#!/usr/bin/perl -w

use strict;
use Pod::Usage;
use Getopt::Long;
use File::Basename;
use Date::Format;

=pod

=head1 NAME

 ext_kerberos_sid_group_acl - external ACL helper for Squid to verify AD Domain group membership using sid.

=head1 SYNOPSIS

 ext_kerberos_sid_group_acl [-d] [-h] -p Principal Name -D Domain Controller -b Base DN -G Group1:Group2

=head1 DESCRIPTION

B<ext_kerberos_sid_group_acl> is an installed executable script.
It uses B<ldapsearch> from Openldap to lookup the name of a AD group sid.

This helper must be used in with the negotiate_kerberos_auth helper in a
Microsoft AD or Samba environment.

It reads from the standard input the domain username and a list of group sids
and tries to match the group SIDs to the AD group sids.

=head1 OPTIONS

=over 12

=item B<-d>

Write debug info to stderr.

=item B<-h>

Print the help.

=item B<-p principal name>

Principal name in squid keytab to use for ldap authentication to AD

=item B<-D domain controller>

Domain controller to contact to lookup group SID

=item B<-b base DN>

Base DN for ldap search

=item B<-G AD group name>

AD group name to be used for SID lookup. List separated by a colon (:)

=back

=head1 CONFIGURATION

  auth_param negotiate program /path/to/negotiate_wrapper_auth -d \
       --ntlm /path/to/ntlm_auth --helper-protocol=squid-2.5-ntlmssp --domain example.com \
       --kerberos /path/to/negotiate_kerberos_auth -d -s GSS_C_NO_NAME -k /path/to/squid.keytab -t none
  external_acl_type sid_check %LOGIN %note{group} /path/to/kerberos_sid_group_acl -p principal -D dc1.example.com -b "DC=example,DC=com" -G Group1:Group2
  acl squid_allow external sid_check
  acl allowed_group external sid_check
  http_access allow allowed_group

If the local perl interpreter is in a unusual location it may need to be added:

  external_acl_type sid_check %LOGIN %note{group} /path/to/perl /path/to/kerberos_sid_group_acl -p principal -D dc1.example.com -b "DC=example,DC=com" -G Group1:Group2

=head1 AUTHOR

This program was written by Markus Moeller <markus_moeller@compuserve.com>

This manual was written by Markus Moeller <markus_moeller@compuserve.com>

=head1 COPYRIGHT

 * Copyright (C) 1996-2023 The Squid Software Foundation and contributors
 *
 * Squid software is distributed under GPLv2+ license and includes
 * contributions from numerous individuals and organizations.
 * Please see the COPYING and CONTRIBUTORS files for details.

 This program is put in the public domain by Markus Moeller
 <markus_moeller@compuserve.com>. It is distributed in the hope that it will
 be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

=head1 QUESTIONS

Questions on the usage of this program can be sent to the I<Squid Users mailing list <squid-users@lists.squid-cache.org>>

=head1 REPORTING BUGS

Bug reports need to be made in English.
See https://wiki.squid-cache.org/SquidFaq/BugReporting for details of what you need to include with your bug report.

Report bugs or bug fixes using https://bugs.squid-cache.org/

Report serious security bugs to I<Squid Bugs <squid-bugs@lists.squid-cache.org>>

Report ideas for new improvements to the I<Squid Developers mailing list <squid-dev@lists.squid-cache.org>>

=head1 SEE ALSO

negotiate_kerberos_auth(8)

The Squid FAQ wiki https://wiki.squid-cache.org/SquidFaq

The Squid Configuration Manual http://www.squid-cache.org/Doc/config/

=cut

#
# Version history:
#   2018-06-10 Markus Moeller <markus_moeller@compuserve.com>
#               Initial release
#
# Globals
#
use vars qw/ %opt /;

my $name = basename($0);
my $principal;
my $dc;
my $basedn;
my $ccname="/tmp/squid_krb5cc";
my $groupSIDs;
my @ADgroupSIDs;
my $user;
my @groups;
my $ans;

# Disable output buffering
$|=1;

sub debug()
{
    my @lt = localtime;
    print STDERR strftime("%Y/%m/%d %H:%M:%S", @lt)." | $name: @_\n" if $opt{d};
}

sub info()
{
    my @lt = localtime;
    print STDERR strftime("%Y/%m/%d %H:%M:%S", @lt)." | $name: @_\n";
}

sub check()
{
    if ( grep( /^@_$/, @ADgroupSIDs) ) {
        &debug("DEBUG: Found @_ in AD group SID");
        return "OK";
    } else {
        &debug("DEBUG: Did not find @_ in AD group SID");
        return "ERR";
    }
}

#
# Command line options processing
#
sub init()
{
    use Getopt::Std;
    my $errmsg;
    my $opt_string = 'hdD:p:b:G:';
    getopts( "$opt_string", \%opt ) or usage();
    Pod::Usage::pod2usage(1) if $opt{h};
    Pod::Usage::pod2usage(1) if not defined $opt{D};
    Pod::Usage::pod2usage(1) if not defined $opt{b};
    Pod::Usage::pod2usage(1) if not defined $opt{p};
    Pod::Usage::pod2usage(1) if not defined $opt{G};

    $ENV{'KRB5CCNAME'} = $ccname;

    @groups = split(/:/,$opt{G});
    $errmsg=`kinit -k $opt{p} 2>&1`;
    &info("ERROR: $errmsg") if $errmsg;
    exit 99 if $errmsg;

    $errmsg="";
    foreach my $group (@groups) {
        open(LDAP, "ldapsearch -LLL -Ygssapi -H ldap://$opt{D}:389 -s sub -b \"$opt{b}\" \"(CN=$group)\" objectsid 2>&1 |");
        my $sid;
        while (<LDAP>) {
            chomp($_);
            if ( $_ =~ /^object/ && defined $sid ) {
                &info("ERROR: multiple SIDs returned for group $group");
            } elsif ( $_ =~ /^object/ ) {
                $sid=$_;
                $sid=~s/^[^\s]+\s+//;
            } else {
                $errmsg=$errmsg.";".$_;
            }
        }
        close(LDAP);
        if ( ! defined $sid ) {
            $errmsg=~s/^;//;
            &info("ERROR: $errmsg");
            &info("ERROR: no SID returned for group $group");
        } else {
            &info("INFO:ldapsearch result Group=$group, SID=$sid");
            push @ADgroupSIDs, $sid;
        }
    }
    &info("ERROR: Exit as no sid was found for any group") if ! @ADgroupSIDs;
    exit 99 if ! @ADgroupSIDs;
}

init();
&debug("INFO: Debugging mode ON.");

#
# Main loop
#
while (<STDIN>) {
    chop;
    &debug("DEBUG: Got $_ from squid");
    ($user, $groupSIDs) = split(/\s+/);
    if ( defined $user && defined $groupSIDs ) {
        &debug("DEBUG: user=$user");
        &debug("DEBUG: groups=$groupSIDs");
        # test for each group squid send in it's request
        foreach my $group (split(/,/,$groupSIDs)) {
            $ans = &check($group);
            last if $ans eq "OK";
        }
        &debug("DEBUG: Sending $ans to squid");
        print "$ans\n";
    } else {
        &debug("DEBUG: Sending ERR to squid");
        print "ERR\n";
    }
}

