Автоматический импорт ФИО пользователей из Active Directory в Lightsquid

У многих, кто управляет прокси-сервером Squid на предприятии, есть потребность периодически демонстрировать статистику пользования интернетом сотрудниками руководству. Для представления статистики дополнительно к Squid устанавливается анализатор журналов типа SARG, Lightsquid и т.д. При этом зачастую в организации развёрнута служба каталогов (предполагается, это Active Directory), в которой у всех сотрудников есть учётные записи и авторизация в прокси-сервере идёт на основе учётных записей. Естественно, для руководства, когда оно смотрит отчёт, удобней идентифицировать сотрудника по имени и фамилии. На удивление, во многих форумах и IT-порталах эту задачу предлагают решать вручную, забивая имена и фамилии в конфигурационные файлы анализатора журналов. У этого решения есть недостаток — при зачислении/увольнении какого либо сотрудника конфиги придётся редактировать.
Эта статья описывает метод автоматического извлечения данных о фамилии и имени сотрудников из ActiveDirectory и их вставку в отчёты Lightsquid.

Задача: обеспечить вывод отчётов через Lightsquid по каждому пользователю Active Directory, заходившего в интернет, с указанием его имени и фамилии (забегая вперёд, скажу, что в AD этой информации соответствует поле «выводимое имя», в LDAP-запросах на него ссылается переменная displayName)


Исходные данные:

  • -Настроенный и корректно работающий Squid 3.3.8 с авторизацией ntlm
  • -в конфиге Squid присутствуют следующие строки:
    	logfile_rotate N
    	debug_options rotate=M
    	

    где N и M -максимальное количество файлов журнала, которые Squid может создать
    logfile_rotate — для access.log, debug_options -для cache.log
  • -Настроенный и корректно работающий Apache 2.4.7
  • -Настроенный и корректно работающий Lightsquid 1.8 ()
  • -в конфиге Lightsquid есть переменная $lang = «ru»


Решение:

Сначала я опишу механизм передачи данных о ФИО в Lightsquid из ActiveDirectory, затем приведу его реализацию.

Сведения о ФИО представлены в AD в свойствах доменного пользователя, в поле «Выводимое имя». Для получения сведений от AD необходимо взаимодействовать с ним через LDAP-запросы. При этом взаимодействовать можно только от имени авторизованного пользователя домена. Поскольку Lightsquid написан на Perl, для выполнения этих запросов потребуется модуль Net::LDAP. А для автоматического вывода в отчёте сведений из AD необходимо заменить простое получение логина от squid на выполнение LDAP-запроса.

Вначале необходимо создать в AD учётку с максимально ограниченными правами, которая будет использоваться для выполнения LDAP-запросов. Для этого запустите оснастку «Active Directory- пользователи и компьютеры» и создайте нового пользователя. Дайте ему название, выражающее его предназначение. Например, LightSquidAgent. Затем создайте новый объект групповой политики и войдите в его свойства (или в свойства существующего объекта). Далее Конфигурация компьютера->Конфигурация Windows->Параметры безопасности->Локальные политики->Назначение прав пользователя. В параметре «Отказ в доступе к компьютеру из сети» введите LightSquidAgent. В параметре «Отклонить локальный вход» тоже введите LightSquidAgent.

Теперь установим в Perl модуль Net::LDAP. Запускаем bash или аналогичную командную оболочку и выполняем
perl -MCPAN -e shell
. После входа в интерпретатор cpan выполняем
install Net::LDAP
. Далее программа установки выведет вопрос, хотим ли мы позволить ей выполнить авто-конфигурирование. Просто жмём Enter. В конце вы должны увидеть
LDAP module was installed successfully
.

После этого можно править код, генерирующий отчёты. Заходим в папку с установленным LightSquid, переходим в папку ip2name и открываем файл ip2name.squidauth. Он должен выглядеть так:
#contributor: esl
#specialy for squid with turned on user authentication
#simple version

sub StartIp2Name() {
}

sub Ip2Name($$$) {
  # $Lhost,$user,$Ltimestamp
  my $Lhost=shift;
  my $user =shift;
  $user    =URLDecode($user); #decode user name
  return $user if ($user ne "-");
  return $Lhost;
}

sub StopIp2Name() {
}

#warning !!!
1;

UPD: Учтены рекомендации автора Lightsquid насчёт кэширования имён пользователей.

UPD2: Исправлена ошибка с зависанием скрипта при распознавании неавторизованных пользователей

UPD3: Исправлена ошибка с зависанием скрипта при недоступности контроллёра домена и при невозможности выполнить вход под учётной записью LightSquidAgent

В шапке файла нужно прописать пространства имён, в которых лежат нужные нам функции и объявить 3 новые переменные:
#contributor: esl
#specialy for squid with turned on user authentication
#simple version

use strict;
use warnings;
use Net::LDAP;
use Encode;

my $ldap;
my $message;
my %hDisplayName;


Заменяем пустое определение StartIp2Name на наше, в котором устанавливается подключение к контроллёру домена

sub StartIp2Name() {
my $server = "ldap://ourserver.domain.com";
$ldap = Net::LDAP->new( $server );
return if(!defined $ldap);
$message = $ldap->bind(q(domain\LightSquidAgent), password => "passwd"); 
}


Заменяем определение функции Ip2Name, наш вариант функции будет брать у контроллёра домена ФИО сотрудников

В ветке условного оператора обеспечивается пропуск повторяющихся логинов, для каждого пользователя будет сделан только один LDAP-запрос.

Вместо
sub Ip2Name($$$) {
  # $Lhost,$user,$Ltimestamp
  my $Lhost=shift;
  my $user =shift;
  $user    =URLDecode($user); #decode user name
  return $user if ($user ne "-");
  return $Lhost;
}

вставляем
sub Ip2Name($$$) {
  # $Lhost,$user,$Ltimestamp
  my $Lhost=shift;
  my $user =shift;
  $user    =URLDecode($user); #decode user name  
  return $Lhost if ($user eq "-");
  return $user if (!defined $ldap);
  return $user if ($message->code());
  
  if (!defined $hDisplayName{$user})
  {

    my $result = $ldap->search(
	base	=> "dc=domain,dc=com",
	filter	=> "(&(objectCategory=person)(objectClass=user)(sAMAccountName=" . $user . "))",
    );

my $first_entry = $result->entry(0);
if (!defined $first_entry)
  {
    return $Lhost;
  }

my $pure_displayName = $first_entry->get_value("displayName");
$pure_displayName =~ s/ /_/g;
Encode::from_to($pure_displayName, 'utf-8', 'windows-1251');

  $hDisplayName{$user}=$pure_displayName;
  }

  return $hDisplayName{$user};
}


Последнее: В функции StopIp2Name отключаемся от контроллёра домена

sub StopIp2Name() {
return if (!defined $ldap);
$message = $ldap->unbind;
}


Объективная критика приветствуется
Поделиться публикацией

Комментарии 11

    +1
    Так эта, в lightsquid есть штатные realname.cfg и group.cfg

    Я их скриптом просто генерирую несколько раз в стуки перед запуском парсера лайтсквида и всё, зачем лезть в код?

    #!/usr/bin/perl -wT
    
    use Net::LDAP;
    use Encode qw(from_to);
    
    my $prefix = '/var/www/stats';
    my $users = $prefix.'/realname.cfg';
    my $groups = $prefix.'/group.cfg';
    
    my $ldap = Net::LDAP->new('ldap://dc.domain.ru') or die "$@";
    my $result = $ldap->bind('CN=user,CN=Users,DC=domain,DC=ru', 'password' => 'xxxxx');
    
    if($result->code) {
        die 'Bind failed!\n';
    }
    
    $result = $ldap->search(
        'base'   => 'OU=Пользователи,DC=domain,DC=ru',
        'filter' => '(&(objectClass=user)(!(UserAccountControl=66050)))',
        'scope'  => 'sub',
        'attrs'  => [ 'cn', 'sAMAccountName', 'Department' ]
    );
    
    if($result->entries <= 0) {
        die "Found no users\n";
    }
    
    my %groups = ();
    
    open(OUT, '>' . $users);
    foreach my $entry ($result->entries) {
        my $name = $entry->get_value('cn');
        my $login = lc($entry->get_value('sAMAccountName'));
        my $dept = $entry->get_value('Department');
    
        if(defined $dept) {
            $groups{$dept}{$login} = 1;
        }
    
        print OUT $login . "\t" . $name . "\n";
    }
    close(OUT);
    
    my $i = 1;
    open(OUT, '>' . $groups);
    foreach my $group (sort keys %groups) {
        foreach my $login (sort keys %{$groups{$group}} ) {
            my $num = sprintf("%02d", $i);
            print OUT $login . "\t" . $num . "\t" . $group . "\n";
        }
        $i++;
    }
    close(OUT);
    
    
      0
      В общем-то у меня тоже была мысль через cron генерировать содержимое конфига Lightsquid, но честно, поленился. Подумал, что результат что так что так будет одинаковый.
      А так ваше решение тоже хорошее, плюсую.
      PS Кармы не хватает.
        0
        В моем подходе очевидный плос в том, что он вообще не грузит LDAP сервер т.к. операция поиска происходит один раз.

        А в твоем случае — LDAP запрос (вызов функции ip2name) идет для каждой(!!!) строчки в логах:
        while (<FF>) {
        ...
        $user = Ip2Name($Lhost,$user,$Ltimestamp);
        ...
        }
        


        Вот за вчера у меня в нашей относительно небольшой конторе было:
        # xzcat access.log-20140624.xz | wc -l
        1691991
        

        ~1.7 миллиона строк, и столько раз лайтсквид бы дёргал контроллеры домена.
          0
          Простейшее кеширование рещаеть на раз проблемму 1691991 строк ;)
          там в других ip2name есть примеры.
          0
          Вариант предложенный автором более правильный чем генерация конфигов по крону ;)
          так и задумывалось.
          именно для этой цели ip2name и был введен.

          Это я как автор говорю.
            0
            Ну, это с какой стороны посмотреть :) Если кешировать в StartIp2Name() — то да, разницы в общем и целом никакой.
            Но в том виде, в котором оно в статье — адский ад домен-контроллера :)
              0
              добавить кеширвание — дело минут.

              с другой стороны, запросы то легковесные, пусть микрософт работает ;)

              еще раз повторюсь, путь с ip2name — это путь как раз правильный.

              перегенерация конфигов — костыль.

        0
        как нескромный автор lightsquid — рекомендую

        в ip2name 3 функции

        в StartIp2Name — вынести бинд к LDAP, зачем его каждый раз открывать, на каждую запись
        сюда же инициализацию кеша

        в StopIp2Name — закрытие коннекшена

        в самом ip2name
        проверить есть ли в хеше, если нет, запрашиваем LDAP и складываем в хеш
        возвращаем значение из хеша

          0
          Учтено, теперь в сабжевом листинге есть кеширование.
            0
            выглядит правильно.

            как-то повлияло на скорость?
          0
          Обнаружилась ошибка — если в момент захода в интернет контроллёр домена недоступен, то в журнал Squid попадает не логин AD, а IP-адрес пользователя. В результате в функции Ip2Name LDAP-запрос возвращает пустое значение по IP, и на пустой ссылке вызывается метод get_value, затем скрипт зависает. Исправлено проверкой, определена ли $first_entry (см. листинг).

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое