Usando LDAP com Perl

Alexei Znamensky
Publicado em 01/01/2010

Usando LDAP com Perl

O uso do LDAP (Lightweight Directory Access Protocol) tem se difundido muito no ambiente corporativo como repositório de vários recursos utilizados no cotidiano de uma grande rede. O protocolo tem o suporte de grandes players do mercado, e vários produtos já provêem algum tipo de integração com LDAP. Mesmo redes menores podem utilizar o LDAP, embutido em outros produtos, como o caso clássico de ambientes de rede Windows, que utilizam o Microsoft Active Directory - um servidor LDAP.

Dada a ubiqüidade do Perl, e a crescente adoção do LDAP, é natural que se deseje integrar, em algum momento, os programas em Perl ao diretório de informações da empresa.

Este artigo não pretende ser um guia completo sobre LDAP, ou mesmo uma documentação completa sobre como acessar diretórios LDAP a partir do Perl, mas apenas fornecer um ponto de partida para quem quer trabalhar com o assunto.

LDAP

O LDAP é um protocolo utilizado para consultar e modificar dados em diretórios hierárquicos de informação, através de redes "TCP/IP". O protocolo está em sua versão 3 (Version 3), especificado em várias RFCs listadas na RFC 4510.

Perl

Apesar de haver vários módulos relacionados a LDAP no CPAN, o módulo mais "padrão" e utilizado de fato para acessar diretórios LDAP a partir do Perl é o Net::LDAP, de Graham Barr. Vários dos outros módulos, inclusive, utilizam o Net::LDAP como base.

Show me the code

A receita de bolo mais simples é:

    my $ldap = Net::LDAP->new( 'meu.servidor.ldap.com' ) or die "$@";
    my $mesg = $ldap->bind();
    $mesg = $ldap->search(
        filter =>
            '(&(objectClass=*)(dn="cn=zeca,ou=informatica,o=empresa,c=br"))'
    );
    foreach my $e ( $mesg->entries() ) {
        print "dn: " . $e->dn() . "\n";
        foreach my $a ($e->attributes() ) {
            print $a . ": " . $e->get_value($a) . "\n";
        }
        print "\n";
    }

Agora vamos analisar por partes.

Conexão

No trecho:

    my $ldap = Net::LDAP->new( 'meu.servidor.ldap.com' ) or die "$@";

Nessa linha, obviamente criamos uma instância do objeto, mas é importante entender que isso por si só estabelece uma conexão TCP/IP com o host especificado. Se não especificarmos nada além do hostname, como no caso acima, é utilizada a porta padrão do LDAP, 389.

Após estabelecida a conexão TCP/IP, é preciso fazer o bind:

    my $mesg = $ldap->bind();

Neste caso, como não foi passado nenhum parâmetro, ocorre aquilo que chamamos de anonymous bind, isto é, não houve uma autenticação de fato. O anonymous bind é utilizado para efetuar acessos somente de leitura nos dados do diretório, no entanto, o administrador consciente irá proibir o bind anônimo em seu servidor, por segurança. Para realizar um bind com autenticação, usamos:

    my $mesg = $ldap->bind(
        'cn=root',
        password => 'senhahipersecreta'
    );

Interlúdio Teórico

O LDAP é uma árvore de entradas, definidas por um Distinguished Name ou DN ("Nome Distinto" seria uma tradução literal, e "Nome Único" seria uma tradução mais apropriada). O DN, como o nome indica, identifica de forma única uma entrada - poderíamos dizer que o DN está para um diretório LDAP assim como a chave-primária está para um banco de dados relacional. Para mais detalhes sobre o DN veja a seção 2.3 da RFC 4512.

No nosso exemplo acima, poderíamos ter uma árvore (aqui toscamente desenhada) como:

    c=br
    |
    +---- o=outraempresa
    |
    +---- o=empresa
          |
          +---- ou=financeiro
          |     |
          |     +---- cn=zeca
          |
          +---- ou=marketing
          |     |
          |     +---- cn=tato
          |
          +---- ou=rh
          |     |
          |     +---- cn=duda
          |
          +---- ou=informatica
                |
                +---- cn=zeca
                |
                +---- cn=juca
                |
                +---- cn=foca

Assim, o DN completo do funcionário zeca do departamento de informática é:

    DN: CN=ZECA,OU=INFORMATICA,O=EMPRESA,C=BR

E o DN do zeca do financeiro é:

    dn: cn=zeca,ou=financeiro,o=empresa,c=br

Onde, nessa terminologia, temos os nomes de atributos:

    CN: Common Name
    OU: Organizational Unit Name
    O:  Organization Name
    C:  Country Name

Notem que o DN completo é montado da "folha" para a "raiz" da árvore de dados, de forma bem semelhante ao DNS, e também que ele é case-insensitive. Existem vários nomes de atributos diferentes que podem ser utilizados para essa hierarquia, e não há restrições quanto à ordem ou a sequência em que eles devem aparecer na árvore. Para maiores detalhes sobre os nomes de atributos em um DN, ver a seção 3 da RFC 4514.

Além do DN, que é único para uma entrada LDAP, podemos ter uma infinidade de outros atributos associados com a entrada. Esses outros atributos não precisam ser únicos, podemos ter várias ocorrências do mesmo atributo em uma entrada, cada um com um valor diferente. Quando buscarmos o valor desse atributo com múltiplas ocorrências, teremos como resposta uma lista com todos os valores definidos.

Mas o LDAP é mais sofisticado que isso, ele possui um mecanismo de estrutura de dados similar ao utilizado em programação orientada a objetos, com classes (sem código), e herança (múltipla). Para isso, temos o atributo objectClass: toda entrada no LDAP DEVE possuir pelo menos um atributo com o nome objectClass. Um objectClass determina, como o nome sugere, uma classe de um objeto, e é definido em um tipo de arquivo chamado LDAP Schema. Um objectClass determina quais são os atributos obrigatórios e opcionais para uma entrada que "implementar" aquela classe. Por "implementar", entendam uma entrada que possui um atributo do tipo objectClass cujo valor é o nome da classe específica a que nos referimos. Para mais detalhes leiam a RFC 4512.

As classes podem definir virtualmente qualquer tipo de informação: dados relacionados a uma pessoa (nome, sobrenome, foto - codificada em base64, etc), a um usuário de rede (id, senha, diretório index.t, etc), informações de um cliente B2B, grupos de usuários para permissões, etc...

Busca

Após o bind, podemos realizar uma busca:

    $mesg = $ldap->search(
        filter =>
            '(&(objectClass=*)(dn="cn=zeca,ou=informatica,o=empresa,c=br"))'
    );

Obviamente, no filtro é definido o que iremos buscar. Filtros têm uma gama de variações em sua sintaxe, mas temos duas construções principais. A primeira é o filtro simples:

    ()   # atributo, operador, valor

    (dn="cn=zeca,ou=informatica,o=empresa,c=br")
        # atributo: DN
        # operador: IGUAL A (=)
        # valor: "cn=zeca,ou=informatica,o=empresa,c=br"

    (logonCount>=1000)
        # entradas cujo atributo logonCount é maior ou igual a 1000

E a outra é o filtro composto com operadores lógicos:

    ((filtro1)(filtro2) ... )

    (&(objectClass=inetOrgPerson)(logonCount>=1000))
        # &: operador lógico AND
        # tradução: trazer as entradas que
        #           SÂO DA CLASSE inetOrgPerson E têm logonCount MAIOR QUE 1000

    (|(filtro1)(filtro2))   # operador lógico OR
    (!(filtro))             # operador lógico NOT

    (&(!(cn=zeca))(|(dn=*,ou=financeiro)(dn=*,ou=marketing)))

Para maiores detalhes sobre a sintaxe dos filtros, vejam a RFC 4515.

Exercício para o Leitor

O que esse último filtro traz, quando aplicado ao diretório utilizado como exemplo acima? (o desenho tosco).

Consumo

Para consumir as entradas resultantes de uma busca:

    $mesg = $ldap->search(
        filter =>
            '(&(objectClass=*)(dn="cn=zeca,ou=informatica,o=empresa,c=br"))'
    );
    foreach my $e ( $mesg->entries() ) {
        print "dn: " . $e->dn() . "\n";
        foreach my $a ($e->attributes() ) {
            print $a . ": " . $e->get_value($a) . "\n";
        }
        print "\n";
    }

Primeiro obtemos o array fornecido pelo método entries() do resultado. O escalar $mesg é do tipo Net::LDAP::Search, e os elementos da lista entries() são do tipo Net::LDAP::Entry.

Basta iterar sobre a lista para acessar o DN e atributos de cada elemento do resultado da pesquisa.

LDIF

O protocolo LDAP define um formato padrão de arquivos de dados a serem utilizados pelos servidores. Esse formato é o LDAP Data Interchange Format, ou LDIF.

Segue um exemplo de um trecho de um arquivo LDIF:

    version: 1

    dn: CN=Alexei Znamensky,OU=SnakeOil,OU=Extranet,DC=sa,DC=mynet,DC=net
    objectClass: top
    objectClass: person
    objectClass: organizationalPerson
    objectClass: user
    cn: Alexei Znamensky
    accountExpires: 129290832000000000
    badPasswordTime: 129253228359870323
    badPwdCount: 0
    codePage: 0
    countryCode: 0
    description: Consultant - SnakeOil Brazil on Extranet
    displayName: Alexei Znamensky
    distinguishedName: CN=Alexei Znamensky,OU=SnakeOil,OU=Extranet,DC=sa,DC=mynet,DC=net
    givenName: Alexei
    lastLogoff: 0
    logonCount: 684
    manager: CN=John Doe,OU=SnakeOil,OU=Extranet,DC=na,DC=mynet,DC=net
    memberOf: CN=g_SPO_ADM_WAS,OU=AD_Based_Apps,OU=GROUPS,OU=SPO,DC=sa,DC=mynet,DC=net
    memberOf: CN=u_SPO_SRV_Internals,OU=GROUPS,OU=SPO,DC=sa,DC=mynet,DC=net
    memberOf: CN=g_SPO_ITReleaseManagement_StagingProd,OU=ReleaseMan,OU=IT,OU=GROUPS,OU=SPO,DC=sa,DC=mynet,DC=net
    memberOf: CN=g_SPO_Radius_BR_Associates,OU=SYSTEM,OU=SPO,DC=sa,DC=mynet,DC=net
    memberOf: CN=u_SPO_FS1_IT all users,OU=IT,OU=GROUPS,OU=SPO,DC=sa,DC=mynet,DC=net
    name: Alexei Znamensky
    sAMAccountName: ZnamensA
    sAMAccountType: 805306368
    sn: Znamensky
    userPrincipalName: ZnamensA@mynet.net
    uSNChanged: 160358095
    uSNCreated: 144230343
    whenChanged: 20100906002534.0Z
    whenCreated: 20100318194403.0Z

Dicas

Para modificar entradas no LDAP, use os métodos add(), delete() e modify() da classe Net::LDAP, ou suas variações.

Servidores LDAP são otimizados, por padrão, para leitura, e não para escrita. Em alguns momentos pode parecer tentador utilizar um servidor LDAP como se fosse um banco de dados da aplicação, mas resista! Entenda que: um servidor LDAP NÃO FOI FEITO PARA SER UM BANCO DE DADOS.

Ao fazer buscas, passe os parâmetros base e scope, para delimitar o mais precisamente possível a busca que deseja realizar. Teste suas buscas antes com o comando ldapsearch, disponível em praticamente qualquer Linux.

Alguns servidores e administradores de LDAP não gostam de conexões que duram muito tempo. Adote como política padrão abrir-consultar-sair.

Produtos

Veja uma lista mais abrangente no artigo da Wikipedia.

Servidores

OpenLDAP: Servidor open-source de LDAP, padrão de fato para uso de LDAP no Linux. Escrito em C. (http://www.openldap.org/)

ApacheDS: Servidor open-source da "marca" Apache. Escrito em Java. (http://directory.apache.org/)

IBM Tivoli DS (ITDS): Servidor comercial da IBM. Escrito em C, utilizado um servidor DB2 como back-end para os dados. (http://www-01.ibm.com/software/tivoli/products/directory-server/)

Clientes

Net::LDAP: Faça o seu usando sua linguagem favorita! :-) (Net::LDAP)

ldapsearch: Cliente leve, de linha de comando. No Linux é facilmente instalável com o OpenLDAP.

Apache Directory Studio: Execelente aplicação gráfica para acessar diretórios LDAP. Infelizmente é bem pesada, não é aconselhável para computadores velhos e/ou com recursos limitados.

Conclusão

Servidores LDAP tendem a ser cada vez mais utilizados como padrão de diretório de informações corporativas. O Perl possui uma ferramenta madura, na forma do módulo Net::LDAP, para acessar e manipular diretórios. Certamente há a possibilidade de que o Net::LDAP não atenda a 100% dos casos, mas atende a grande maioria, o que já é um excelente ponto de partida.

Não deixem de ler o fantástico manual ;-).

Referências

Net::LDAP

Link direto no CPAN. Info adicional em http://ldap.perl.org/.

ldapsearch(1)

http://www.openldap.org/software/man.cgi?query=ldapsearch&apropos=0&sektion=0&manpath=OpenLDAP+2.0-Release&format=html

Wikipedia (English) LDAP entry

http://en.wikipedia.org/wiki/LDAP

Wikipedia (Português) verbete LDAP

http://pt.wikipedia.org/wiki/LDAP

"The 10-Minute LDAP Tutorial - Automating System Administration with Perl"

http://oreilly.com/perl/excerpts/system-admin-with-perl/ten-minute-ldap-utorial.html

"Introduction to LDAP: Part 1, Installation and simple Java LDAP programming"

http://www.ibm.com/developerworks/tivoli/library/t-ldap01/index.html

"An Introduction to perl-ldap"

http://www.linuxjournal.com/article/7086

Agradecimentos

Daniel de Oliveira Mantovani

O nosso mascote-emo na São Paulo PM, por ter me atirado em direção a este artigo.

Luis "Fields" Campos

Velho amigo que me trouxe de volta para o Perl.

AUTHOR

Alexei Znamensky "Russo" < russoz no cpan org >

Blog: http://russoz.wordpress.com/

LinkedIn: http://www.linkedin.com/profile?viewProfile=&key=754668&trk=tab_pro

Resposta

A resposta para o exercício deixado a cargo do leitor:

O filtro (&(!(cn=zeca))(|(dn=*,ou=financeiro)(dn=*,ou=marketing))), aplicado à arvore toscamente desenhada, trará como resultado a entrada:

    cn=tato,ou=marketing,o=empresa,c=br

Licença

Este texto está licenciado sob os termos da Creative Commons by-sa, http://creativecommons.org/licenses/by-sa/3.0/br/

Licença Creative Commons
This work is licensed under a Creative Commons Attribution-ShareAlike License.
blog comments powered by Disqus