por Luke A.
Kaines - 11/08/2001
Artigo publicado originalmente
em
http://www.oreillynet.com/pub/a/linux/2001/11/08/ldap.html,
na The O'Reilly
Network.
Escrever este artigo acabou sendo mais difícil que eu esperava. Inicialmente eu comecei com uma explicação profunda do LDAP como protocolo, mas percebi que o objetivo real aqui era poder trabalhar com o LDAP agora mesmo, não após ler 50 páginas de explicações abstratas.
Com este objetivo em mente, começaremos a trabalhar com o LDAP e um ambiente de trabalho semi-real. Especificamente, iremos configurar um diretório LDAP básico para armazenar as contas de usuário Unix, além de um script para passar estas contas para um sistema Unix -- esta é uma das coisas para qual você deve usar o LDAP. Este exemplo também será útil para demonstrar que mesmo que sua versão de Unix não possa realizar autenticação de usuários diretamente a partir do LDAP, você ainda pode armazenar informações de seus usuários no LDAP e obter todos os objetivos que provêm disto.
Conforme mencionado em meu artigo anterior, o LDAP foi desenvolvido como um método para consolidar as informações de acesso, autenticação e autorização (AAA, ou Triplo-A). Por si mesmo, ele é útil, por que você vai estar mantendo todas as informações em um único lugar, ao invés de vários locais. Entretanto, você poderia conseguir isto usando qualquer banco de dados antigo. O que torna o LDAP especialmente apropriado para armazenar as informações AAA é que todas as operações LDAP tem lugar no contexto da informação AAA, ao invés de forçar a aplicação de fornecer ou interpretar o contexto. As operações falham ou terminam com sucesso sem necessidade da aplicação entender as regras envolvidas.
Se você tentou colocar toda a mesma informação AAA em um banco de dados (o que pode ser algo difícil, por que você tem que definir todos os padrões para armazenamento de informações, algo que o LDAP já tem pronto para você usar), então cada uma de suas aplicações devem analisar aquelas informações e levar as mesmas em conta para cada operação AAA. Se você usar o LDAP, entretanto, já existem métodos para armazenar as informações AAA, apesar de ainda não estarem definidas em uma RFC, e o servidor LDAP aplicar as regras para operações AAA ao invés das aplicações. Isto não só torna a vida dos desenvolvedores de aplicações mais fácil, mas também elimina a chance de aplicações ou usuários mal comportados ignorar as regras de AAA e fazer acesso direto ou modificação do conteúdo de diretórios, exceto, obviamente, atravéz de um compromisso tradicional da segurança.
Assim, nosso objetivo aqui é criar uma infraestrutura AAA, com o LDAP no seu núcleo. A maioria de nossas informações de autenticação já existem, na forma de contas de usuários, e para o nosso artigo, vamos assumir que estas contas de usuários estão em máquinas Unix. Nosso objetivo, então, é colocar estas informações de usuários no LDAP, administrar as mesmas inteiramente no LDAP, e usar a mesma como a origem de todas as operações de AAA em outras aplicações. Uma vez que tenhamos a informação de autenticação configurada, devemos acrescentar as informações de acesso e autorização.
Este artigo vai tratar da primeira tarefa, substituindo os métodos padrão de manter contas Unix com métodos que usem o LDAP. Mais tarde, provavelmente iremos dar exemplos de manutenção baseada na Web de nossos dados LDAP e algumas aplicações que podem obter vantagens destes novos dados centralizados.
Conforme mencionado no artigo anterior, quase toda linguagem moderna tem uma API LDAP; assim, há uma disponibilidade quase infinita de ferramentas. Por sorte, devido a simplicidade do protocolo, a maioria das APIs funciona de forma semelhante. Para iniciar imediantamente, iremos começar com as ferramentas de linha de comando (das quais existem múltiplas versões), por que são de uso direto e presentes em praticamente todos os sistemas.
Há somente três tipos básicos de operações LDAP, e cada tipo básico tem poucos subtipos: interrogação (pesquisa, comparação), atualização (acrescentar, modificar, renomear, excluir), e ligação/controle (ligar, desligar, abandonar). Note que não há uma operação de "leitura"; se você quer ler uma entrada, use uma operação de leitura para recuperar a mesma.
Para mover nossas contas Unix para o LDAP, precisamos converter
as mesmas em uma forma que o servidor LDAP possa entender. Devido à
simplicidade da conversão de um arquivo passwd
para o
LDIF (veja abaixo), iremos deixar a conversão como um exercício
para o leitor. Ao invés disto, iremos começar com uma conta de
usuário já convertida e vamos acrescentar a mesma ao LDAP:
$ ldapadd -D "cn=Directory Manager" -h server
password: ********
dn: uid=luke,ou=People,dc=domain,dc=com
objectclass: top
objectclass: posixAccount
uid: luke
cn: Luke A. Kanies
cn: Luke Kanies
cn: Kanies, Luke
uidNumber: 100
gidNumber: 14
homeDirectory: /home/luke
userPassword: {crypt}8SYYCOBH.aIII
gecos: Luke A. Kanies
^D
adding new entry uid=luke,ou=People,dc=domain,dc=com
$
Esta operação usa duas da três operações básicas do LDAP, uma operação de atualização e uma operação de ligação, junto com uma operação implícita de desligamento. Devemos fazer a ligação como usuário privilegiado para poder modificar o diretório (este exemplo utiliza um usuário especial que todos os servidores iPlanet Directory Server possuem).
Fornecemos então os dados para a entrada que pretendemos acrescentar, que está no formato LDAP Date Interchange Format (LDIF), o formato texto padrão para dados LDAP. Note que cada linha possue um nome de atributo, seguido de dois pontos e espaço, e o valor do atributo. Certamente faze sentido uma operação de pesquisa do LDAP retornar este formato, de forma que pode ser imediatamente informada a um servidor LDAP, mas algumas ferramentas -- incluindo as que são distribuídas com o Solaris -- não imprimem no formato padrão LDIF, requerendo estupidamente que façamos uma conversão.
Agora que veremos como um registro LDAP se parece, vamos ver todos seus pedaços para entender o mesmo.
A primeira linha é o que chamamos de "Distinguished Name" ("Nome
Distinto"), ou dn
. Como ele é usado para dizermos para
o servidor qual objeto estamos trabalhando, a linha dn
sempre deve ser definida primeiro.
O dn
é como um registro é referenciado univocamente
em um servidor LDAP, da mesma forma que um path absoluto ou um
nome de domínio completamente qualificado. Note que o
dn
é representado de forma semelhante aos nomes DNS,
com a informação mais específica primeira, em oposição a nomes de
path, que tem a informação menos específica primeiro. Ao contrário
do que se parece, a origem desta árvore LDAP, também chamada de
"naming context" ("contexto de nomes") é dc=domain,
dc=com
, e não dc=com
.
Não existem exigências sobre qual o nome da origem de sua árvore
LDAP deve ser, mas existem dois padrões: ou o padrão que eu
utilizo aqui, que divide um domínio em vários componentes de
domínio, ou um em que a organização é referida no nível mais alto
(por exemplo, o=domain.com
). Qual você deve seguir
será respondido de forma diferente para cada um que você consultar,
e por que você deve seguir este ou aquele padrão também será
respondido de forma diferente. Eu escolhi o seguinte padrão de
componente de domínio por que parece ser mais popular atualmente --
a Sun e a Microsoft estão recomendando/exigindo ele. Uso o que
parece mais fácil de usar e que seja mais apropriado para a forma
que você está planejando usar os dados.
O resto do dn
consiste de uma ramificação na
árvore, ou=People
, e um par atributo-valor que
identifica univocamente este registro, uid=luke
. Este
par atributo-valor, quando separado do dn
, é chamado
de "Relative Distinguished Name" ("Nome Distinto Relativo"), ou
rdn
, e identifica univocamente este objeto neste nível
da árvore.
Como os nomes de path e nomes DNS semper se referem ao nome do
objeto em questão, tudo que se precisa especifica é este valor, mas
o LDAP pode usar qualquer atributo para criar um rdn
,
e em resultado disto tanto o atributo quanto seu valor deve ser
especificado. O dn
para esta entrada poderia ser
cn=Luke A. Kanies,ou=People,dc=domain,dc=com
, já que
não há ninguém mais na ou=People
com "Luke A. Kanies"
como valor de cn
.
Escolhemos uid
aqui, por que sempre iremos garantir
que este é único apra cada entrada -- qualquer outra coisa não irá
funcionar corretamente em nossos sistemas Unix, e o Directory
Server iPlanet pode ser convenientemente configurado para não
permitir valores uid
duplicados (ou qualquer outro
atributo). Aparentemente o Active Directory da Microsoft exige que
o cn
seja usado para criar o rdn
, o que é
inconveniente por que o cn
é quase que garantidamente
não único, e este é o nome completo do usuário. Muito obrigado!
A seguir em nossa entrata existem dois atributos
objectclass
. Estes atributos definem qual o tipo de
objeto que a entrada é. Entretanto, o conceito de objeto no LDAP é
extremamente simples: ele apenas define quais atributos um registro
deve ter e quais atributos um registro pode ter. Todas as classes
de objeto herdam as especificações de suas classes de objeto pai e
acrescentam suas próprias. O atributo objectclass
não
é um atributo especial, entretanto -- em todas operações LDAP ele é
tratado exatamente como outros atributos LDAP, mas a modificação de
classes de objeto determina se o objeto será aceitável ao servidor
após a operação.
A definição acima de objeto LDAP é importante, por que é difícil
convencer-se como esta definição é simples. Novamente, um objeto
somente define quais atributos devem ou podem ser armazenados com
uma entrada. Eu posso criar um ovjeto que tenha tanto a classe de
objeto posixAccount
e um objeto de classe
printer
; esta combinação pode ser bastante
contraditória para você e eu, mas para o LDAP, dados são apenas
dados, e não possuem nenhum significado intrínseco. Uma das coisas
que faz o LDAP legal é que os atributos significam algo para os
humanos, mas os humanos que trabalham com os dados devem reter este
significado nomeandos classes de objetos e atributos de forma
inteligente e criando objetos que realmente façam sentido. Isto é
mais difícil que parece, e decidir como tudo isto será feito é o
primeiro passo para usar o LDAP. Felizmente, a maior parte das
classes de objetos que você precisará fazem parte da especificação
LDAP (apesar de posixAccount
ser parte de uma RFC
posterior), então, pelo menos inicialmente, você não terá de se
preocupar muito com isto.
Note que você não pode simplesmente criar classes de objetos e
acrescentar as mesmas a um objeto, o servidor deve ter cada classe
de objeto definida, no que é chamado de schema
(esquema
). Se você tentar acrescentar um registro com
uma classe de objeto indefinida, você receberá uma mensagem de
violação do esquema, e a operação irá falhar. Como você devine uma
classe de objeto para o servidor vaira de servidor para servidor,
mas a maioria deles tem isto bem documentado.
Neste caso, declaramos que nosso registro será dos tipos
top
e posixAccount
. A classe de objeto
top
somente exige que o atributo
objectclass
esteja presente, e por definição é a
classe objeto pai de todas as outras classes de objeto LDAP. Dado
este fato, e o fato que os objetos sempre listam as classes de
objeto, elas são uma instância junto com todas as outras classes de
objeto pai, todo objeto em um banco de dados LDAP irá listar
top
como uma classe de objeto. A classe de objeto
posixAccount
é definida em uma RFC por Luke Howard,
que realizou um trabalho significante para permitir que o LDAP
armazene contas Unix, e forneça meios de armazenar todas as
informaçãoes de um arquivo passwd
no LDAP.
Como você já deve ter adivinhado, os atributos podem ter múltiplos valores. Se um atributo suporta múltiplos valoers está declarado na definição do atributo, mas a maioria suporta.
Conforme mencionado anteriormente, a classe de objeto
top
exige que a objectclass
esteja
presente, e só. Assim, a presença do top
não tem outro
efeito neste registro. A classe de objeto posixAccount
exige cn
, uid
, uidNumber
,
gidNumber
, e homeDirectory
, e permite os
campos userPassword
, loginShell
,
gecos
, e description
. Como um login
válido deve ter toda esta informação, exceto a descrição, incluímos
toda ela em nosso registro.
Tdos os nossos valores de atributos são auto-explicativos,
exceto o valor de userPassword
. Como a maioria
(todas?) as máquinas Unix armazenam as senhas no formato
crypt
, e por que iremos fazer um dump do registro LDAP
em um arquivo passwd
, também devemos armazenar a senha
LDAP no formato crypt
. Muitos servidores LDAP suportam
múltiplos formatos de senha, e incluímos um exemplo de como
especificar o formato. Os servidores que suprotam múltiplos
formatos devem permitir a especificação do formato default, e é
recomendado que você configure o formato default para
crypt
se você pretende usar seu servidor LDAP para
armazenar contas Unix. A única exceção para isto é se todas as suas
máquinas Unix possam fazer a autenticação diretamente do servidor
LDAP, e podem entender outros formatos que não o
crypt
.
Ok, agora temos o registro no servidor LDAP, e temos um entendimento básico do registro em si. Agora é hora de começar a usar os dados. Como mencionado antes, se você quer ler um registro, tem que fazer uma pesquisa por ele. Então, para verificar se nosso registro está presente, é o que faremos.
Existe um total de oito (sim, oito) diferentes opções para cada pesquisa LDAP, mas a maioria delas tem valores padrão razoáveis e geralmente fazem sentido. Não usaremos muitas destas opções de início, para manter a simplicidade.
Um exemplo de pesquisa:
$ ldapsearch -h server -b "dc=domain,dc=com" "(uid=luke)"
uid=luke,ou=People,dc=madstop,dc=com
objectclass=top
objectclass=posixAccount
uid=luke
cn=Luke A. Kanies
uidnumber=100
gidnumber=14
homedirectory=/home/luke
gecos=Luke A. Kanies
$
Este resultado é em um sistema Solaris; horror dos horrorres --
note que a saída não está no formato LDIF (sinais de igual são
usados ao invés de dois-pontos e espaços). Incrível mas verdadeiro.
Note que a senha não foi apresentada; isto acontece por razões de
segurança, da mesma forma que o /etc/shadow
somente
pode ser lido por usuários privilegiados.
Em nossa pesquisa, incluímos duas flags e um argumento. A
primeira flag é óbvia: especificamos o servidor. A segunda flag
especificou a base da pesquisa, de forma similar a como especificar
o ponto inicial de uma pesquisa usando o comando find
;
poderíamos ter especificado qualquer ramo incluindo o próprio
objeto. O argumento da pesquisa é chamado de filtro, e é
como especificamos o registro ou registros específicos que
queremos. Todos os registros que combinarem com o filtro serão
retornados. Felizmente o formato do filtro é um pouco sofisticado,
de forma que você não terá dificuldades em fazer pesquisas
complexas para encontrar exatamente o que está procurando, mas
estou somente usando os filgros básicos para conseguir o que quero.
Para mais informações, consulte uma referência.
Como conhecemos o dn
do registro, poderíamos fazer
uma pesquisa deste jeito:
$ ldapsearch -s base -h server -b "uid=luke,ou=People,dc=domain,dc=com" "(objectclass=*)"
Como conhecemos o dn
, pdoeriamos especificar aquele
dn
como base
da pesquisa. Se fizermos
isto, entretanto, temos que mudar o escopo da pesquisa. Por padrão,
uma consulta LDAP procura por objetos em qualquer ponto da
hierarquia começando na base
da pesquisa. Você pode
especifica um escopo de "um", que somente pesquisa o próximo nível
abaixo na hierarquia, e base
, que somente pesquisa a
base. Eu percebi algumas discrepâncias quando usando o iPlanet
Dierctory Server 4.x, geralmente a base da pesquisa é
retornada se ela combina com o filtro, mas já vi alguns exemplos em
que não é este o caso. O método acima é o método preferido de
recuperação se você conhece a dn
da entrada, por que é
mais rápida e exige menos trabalho do servidor.
Note também que nosso filtro mudou. Anteriormente procuramos por
um valor específico de uid
, mas como esmos usando o
dn
do registro como base
de nossa
pesquisa, para recuperar o mesmo diretamente usamos o que é chamado
de filtro de existência. Quando é feita uma pesquisa por
um atributo com o valor *
, o servidor LDAP retorna
todos registros que possuem algum valor para aquele atributo. Como
todos os registros possuem valor para objectclass
,
usar um filtro objectclass=*
é o método padrão para
retornar todos os registros de uma dada base
e escopo.
A razão pela preferência de um filtro de existência sobre um filtro
mais específico é que um filtro específico obriga o servidor LDAP
trabalhar mais, e o uso de um filtro de existência aqui dá o mesmo
resultado com menos trabalho para o servidor.
Está tudo bem, temos os dados no servidor LDAP, e podemos examinar o mesmo, mas como iremos transformar o mesmo em uma conta Unix?
Assumindo que você tenha acrescentado este registro e todas as
outras entradas desejadas no seu servidor LDAP, tudo que é
necessário é apenas um script shell simples para converter estes
dados para os arquivos passwd
e
shadow
:
#!/usr/bin/bash
#
# shell script to convert from LDAP to passwd/shadow files
IFS='
' # set the internal file separator to a carriage return, in case
# attributes have spaces or tabs in them
# look for all entries which have the posixAccount objectclass
# we have to authenticate as a privileged user in order to read the
# password
for entry in $(ldapsearch -h server -D \
"uid=sysadmin,ou=People,dc=domain,dc=com" -w password \
-b dc=domain,dc=com "(objectclass=posixAccount)"); do
# this is the logic that prints out the passwd entry for the
# previous entry every time we hit a DN line
echo $line | grep "dc=madstop,dc=com" > /dev/null
if [ $? == 0 -a ! -z "$uid" ]; then
echo $uid:+:$uidnumber:$gidnumber:$gecos:$homedirectory:$usershell \
>> newpasswd
echo $userpassword >> newshadow
fi
# this uses some trickery to cause the shell to interpret
# ldapsearch output as variable assignments; this may need
# to be modified depending on the output of the ldapsearch command,
# but this works with the ldapsearch supplied on Solaris 8
newline=$(echo $line | sed "s/'/'\''/g
s/=/='/
s/$/'/")
eval $newline
done
# this echo line puts our last entry into the files, because our
# method of dumping the entries is triggered by the next entry,
# and there is no next entry for the last entry
echo $uid:+:$uidnumber:$gidnumber:$gecos:$homedirectory:$usershell \
>> newpasswd
echo $userpassword >> newshadow
# backup the passwd and shadow files, and put the new ones in place
for file in passwd shadow; do
cp $file $file.bak
cp new$file $file
done
Não é bonito? Não mesmo. Eu deveria ter feito isto em Perl? Sim,
mas eu não quero entrar nas API(s) do Perl. Em vez disto, este
script irá pegar seus registros LDAP e criar arquivos
passwd
e shadow
válidos para o Solaris.
Eles podem requerer modificações para outros Unices. Ele não é
terrivelmente interessante ou útil se você tem somente um par de
máquinas Unix, mas se você tem 200 máquinas todas rodando
diferentes versões de Unix, você pdoe facilmente fazer uma versão
deste script para cada versão do Unix (se necessário), e assim
consolidar todas as informações de contas em um repositório de
dados.
Uma coisa que ignoramos é a informação de administração de
senha. Toda a informação relacionada a validade e administração de
senhas é armazenada na classe de objeto shadowAccount
.
É direto para usar, e só teria complicado este artigo, por isto é
deixado como exercício para o leitor. Outra coisa que ignoramos é a
segurança, ou seja, estamos fazendo uma autenticação em texto plano
com o servidor LDAP e toda a comunicação é em texto plano. Um
próximo artigo começará explicando como incorporar criptografia ao
seu serviço de diretório. A autenticação SSL não é muito mais
complicada que a autenticação simples.
Provavelmente agora conseguimos acrescentar as informações das
contas Unix ao LDAP, e fornecemos uma forma de passar a informação
de volta e repopular os arquivos passwd
e
shadow
. Sim, ignoramos os arquivos de grupos, que
também são importamos, e assumimos que todas as contas estarão
presentes em todas as máquinas. Felizmente não é um salto muito
grande colocar as informações dos grupos no LDAP, e é relativamente
fácil somente colocar certas classes de usuários em várias classes
de máquinas, por exemplo, somente acrescentando contas de
desenvolvedores e administradores de sistema nos servidores web, e
somente administradores de sistemas e DBA nos servidores de banco
de dados. De fato, conforme você começa a usar o LDAP, voc~e vai
descobrir que, como em muitos outros processos de
automação/centralização, a parte mais difícil é decidir como fazer
as classificações acima. A maior parte das companhias confia no
administrador do sistema para decidir se um usuário terá uma conta
em algum lugar, e a freqüência de troca de senhas, e mesmo se
existem regras claras, estas regras são geralmente obedecidas
somente por administradores de sistemas, e não pelas ferramentas
que eles utilizam.
Para aproveitar todas as vantagens do LDAP, todas estas regras devem ser colocadas no LDAP, de forma que todas estas decisões possam ser feitas por ferramentas automatizadas ao invés de pessoas. É um investimento feito apenas uma vez em direção a menos trabalho. Felizmente, o desenvolvimento e codificação destas regras é muito bom para seus negócios mesmo se você não planeja usar o LDAP, e você provavelmente irá descobrir que é um processo benéfico por si mesmo.
Conforme progredimos nesta série de artigos, iremos desenvolver uma arquitetura LDAP para uma organização simples. Um dos aspectos deste processo será a criação destas regras LDAP que irão determinar quem tem acesso a quais serviços e informações, incluindo quem tem contas em quais máquinas.
Introduzimos alguns aspectos de como a autenticação funciona dentro do LDAP e como você pode migrar seus dados de autenticação existentes para o LDAP. No próximo artigo desta série, iremos explorar a administração destes dados em uma interface web, juntamente com controle básico de AAA, que é necessário para ter uma administração aceitável. Se eu puder colocar isto junto a tempo, poderemos estar prontos para incorporar o SSL na mistura.
Luke A. Kanies é um administrador de sistemas Unix que está mais interessado no sistema operacional do que no que pode ser feito com o mesmo. Ele trabalha atualmente como contratante e pesquisador, tentando fazer um melhor sysadmin.
Original: oreillynet.com
Copyright © 2000 O'Reilly & Associates, Inc.
Tradução: Copyright © 2001 Cesar A. K. Grossmann