vitorpinho.com Aquele papo de tecnologia

Aviso prévio

A

Desde o início deste ano, envolvi-me em um projecto que me levou a programar em Python. Já uso o PHP, mas a necessidade de ter sempre um servidor para correr o código não é nada viável. Como a Amazon AWS (e a Google Cloud também) oferece quase de graça a possibilidade de correr código sem servidor (serverless) aproveitei a chance.

Mas aí veio uma questão: já que falam tanto do Python, será que posso torná-lo útil no meu dia-a-dia? Existem alguns vídeos de como usar Python para configurar switches Cisco, mas o meu objectivo é fazê-lo interagir com o Active Directory.

Notificação de utilizadores

Duas coisas sempre me incomodaram na gestão de utilizadores: as notificações de senha a expirar somente aparecem nos computadores adicionados ao domínio. Não aparecem quando o utilizador inicia a sessão no webmail, por exemplo, nem quando o mesmo já está iniciado e nunca terminou. Outra é que para se enviar notificações a todos os utilizadores por email, é necessário “mexer” em configurações (no meu caso, ter uma lista de distribuição que inclui todos os utilizadores.

O problema para o primeiro ponto é óbvio: haverá sempre reclamações que quem “nunca recebeu a notificação” ou “não sabia”, e a segunda é que acabamos por não poder usar analíticas para saber quem lê ou não as notificações para depois confrontar caso digam que “não recebi/vi”.

Senhas a expirar

De formas a podermos alertar os utilizadores cujas senhas estarão quase a expirar(daqui a 6 dias, por exemplo), primeiro necessitamos de usar o módulo ldap3 que nos permita ligar ao AD a partir do Python. Este artigo sempre de um ponto de início para a ligação, mas abaixo irei usar ligação não segura, visto que muitos administradores ainda usam o Active Directory sem certificados. Caso usem, é só alterar port=636 e a use_ssl=True.

import os
import json
from ldap3 import Server, Connection
import datetime
import pytz
from genderize import Genderize

from dotenv import load_dotenv
load_dotenv()

# Ligar ao AD
print('Ligando ao Active Directory. Aguarde por favor...')
AD_USER = os.getenv('AD_USER')
AD_PASSWORD = os.getenv('AD_PASSWORD')
AD_IP = os.getenv('AD_SERVER')
server = Server(
    AD_IP,
    port=389,
    use_ssl=False)
conn = Connection(
    server,
    user=AD_USER,
    password=AD_PASSWORD,
    auto_bind=True)

# Procurar utilizadores com contas a expirar
conn.search(
    search_base='DC=exemplo,DC=co,DC=ao',
    search_filter='(&(objectclass=user)'
                  '(mail=*)'
                  '(!(mail=msExch*))'
                  '(!(mail=HealthMailbox*))'
                  '(!(userAccountControl:1.2.840.113556.1.4.803:=65536))'
                  '(!(userAccountControl:1.2.840.113556.1.4.803:=2)))',
    attributes=['cn', 'mail', 'givenName', 'pwdLastSet'],
    )
response = json.loads(conn.response_to_json())

O que a parte inicial do script faz é buscar as variáveis presentes no .env e trazê-las para o Python para se poder autenticar, usando o módulo dotenv. Isto é feito para que se o código for carregado em um repositório Git remoto, nenhuma informação sensível venha a ser partilhada. Também é importante mencionar que o utilizador a usar necessita de ter acesso administrativo.

Após autenticar, filtramos para obter que dados queremos. Em resumo nós precisamos de:

  • (&(objectclass=user) - Apenas utilizadores.
  • (mail=*) - Utilizadores com conta de correio.
  • (!(mail=msExch*)) - Exclui contas de correio que começam com msExch (contas de sistema do Microsoft Exchange Server).
  • (!(mail=HealthMailbox*)) - Exclui contas de correio que começam com HealthMailbox (contas de sistema do Microsoft Exchange Server).
  • (!(userAccountControl:1.2.840.113556.1.4.803:=65536)) - Exclui contas cujas senhas não expiram.
  • (!(userAccountControl:1.2.840.113556.1.4.803:=2))) - Exclui contas desactivadas.

Para mais exemplos, vê este artigo. No campo attributes ficam mencionados os campos dos dados que queremos:

  • cn - Primeiro e último nome
  • mail - endereço de correio
  • givenName - Primeiro nome
  • pwdLastSet - Data da última alteração de senha

Os campo usados estão disponiveis na tabela Atribute Editor nas propriedades de cada utilizador. attribute-editor-windows

Para sabermos quando as senhas expiram, iremos usar a informação do pwdLastSet e calcular a sua data final. Isso depende da política de expiração de senha de cada domínio, portanto, caso uses o script, altera o valor abaixo de acordo com o mesmo.

Começamos agora por fazer um loop para validar os dados de cada utilizador devolvido

for i, row in enumerate(response['entries']):
    NOME_COMPLETO = row['attributes']['cn']
    PRIMEIRO_NOME = row['attributes']['givenName']
    EMAIL_COLEGA = row['attributes']['mail']

    data_utc = row['attributes']['pwdLastSet']
    if data_utc.startswith('1601-01-01'):
        continue

Existem casos que a data de expiração pode ser devolvida como 1601-01-01. Isto é porque desde que o utilizador foi criado com a opção para alterar a senha no primeiro login, o mesmo nunca o fez e ela permanece activa, porém inutilizada. A opção continue é usada para ignorar e pular para o próximo item no loop, mas pode incluir um print ****para o notificar de contas inutilizadas.

    data_utc = datetime.datetime.strptime(
        data_utc, '%Y-%m-%d %H:%M:%S.%f%z')
    timezone_ao = pytz.timezone("Africa/Luanda")
    data_senha_alterada = data_utc.astimezone(timezone_ao).date()
    data_hoje = datetime.date.today()
    dias_avisar = 6  # Avisar antes de 'x' dias
    dias_senha_valida = 182  # 6 meses
    data_expiracao = data_senha_alterada \
        + datetime.timedelta(dias_senha_valida)

    dias_ate_expirar = (data_expiracao - data_hoje)
    expiracao_proxima = dias_ate_expirar.days <= dias_avisar

		if expiracao_proxima is True:
        if dias_ate_expirar.days > 1:
            dias_extenso = 'em ' + str(dias_ate_expirar.days) + ' dias'
        elif dias_ate_expirar.days == 1:
            dias_extenso = 'em ' + str(dias_ate_expirar.days) + ' dia'
        else:
            print(str(i+1) + '. ' + NOME_COMPLETO + ' ('
                            + EMAIL_COLEGA + ')' + ' com senha expirada.')
            continue  # Senha expirou

Na secção acima, calcula-se a data de expiração para o utilizador a partir da última alteração da senha e avançando a data 6 meses (valores a alterar conforme as tuas políticas). Também aqui podes definir o número de dias de véspera para os utilizadores receberem a notificação.

Caso o utilizador não tenha alterado a senha em tempo útil, também somos notificados de que o mesmo tem a senha expirada (útil caso já não faça parte do quadro da empresa, por exemplo).

        # Verificar genero do nome
        genero = Genderize().get([PRIMEIRO_NOME])
        if genero[0]['gender'] == 'female':
            SAUDACAO = 'Estimada colega,'
        else:
            SAUDACAO = 'Estimado colega,'

Este é apenas um bónus para quem quiser dar ao email um toque pessoal. Usando o genderize.io, o módulo permite identificar o género usando o primeiro nome. Assim, para mulheres o email começa com *Estimada colega *****quando para homens fica ****Estimado colega.

Para enviar emails, utilizo o Postmark, mas muitos quererão enviar via SMTP usando uma conta local. Esse trabalho eu deixo para vocês, porém abaixo mostro o script completo para poderem copiar e alterar conforme quiserem.

"""Notificação da aproximação da expiração de senha do utilizador ."""

import os
import json
from postmarker.core import PostmarkClient
from ldap3 import Server, Connection
import datetime
import pytz
from genderize import Genderize

from dotenv import load_dotenv
load_dotenv()

POSTMARK_TOKEN = os.getenv('POSTMARK_TOKEN')

# Ligar ao AD
print('Ligando ao Active Directory. Aguarde por favor...')
AD_USER = os.getenv('AD_USER')
AD_PASSWORD = os.getenv('AD_PASSWORD')
AD_IP = os.getenv('AD_SERVER')
server = Server(
    AD_IP,
    port=389,
    use_ssl=False)
conn = Connection(
    server,
    user=AD_USER,
    password=AD_PASSWORD,
    auto_bind=True)

# Procurar utilizadores com contas a expirar
conn.search(
    search_base='DC=exemplo,DC=co,DC=ao',
    search_filter='(&(objectclass=user)'
                    '(mail=*)'
                    '(!(mail=msExch*))'
                    '(!(mail=HealthMailbox*))'
                    '(!(userAccountControl:1.2.840.113556.1.4.803:=65536))'
                    '(!(userAccountControl:1.2.840.113556.1.4.803:=2)))',
    attributes=['cn', 'mail', 'givenName', 'pwdLastSet'],
    )
response = json.loads(conn.response_to_json())

for i, row in enumerate(response['entries']):
    NOME_COMPLETO = row['attributes']['cn']
    PRIMEIRO_NOME = row['attributes']['givenName']
    EMAIL_COLEGA = row['attributes']['mail']

    data_utc = row['attributes']['pwdLastSet']
    if data_utc.startswith('1601-01-01'):
        continue
    data_utc = datetime.datetime.strptime(
        data_utc, '%Y-%m-%d %H:%M:%S.%f%z')
    timezone_ao = pytz.timezone("Africa/Luanda")
    data_senha_alterada = data_utc.astimezone(timezone_ao).date()
    data_hoje = datetime.date.today()
    dias_avisar = 6  # Avisar antes de 'x' dias
    dias_senha_valida = 182  # 6 meses
    data_expiracao = data_senha_alterada \
        + datetime.timedelta(dias_senha_valida)

    dias_ate_expirar = (data_expiracao - data_hoje)
    expiracao_proxima = dias_ate_expirar.days <= dias_avisar

    if expiracao_proxima is True:
        if dias_ate_expirar.days > 1:
            dias_extenso = 'em ' + str(dias_ate_expirar.days) + ' dias'
        elif dias_ate_expirar.days == 1:
            dias_extenso = 'em ' + str(dias_ate_expirar.days) + ' dia'
        else:
            print(str(i+1) + '. ' + NOME_COMPLETO + ' ('
                            + EMAIL_COLEGA + ')' + ' com senha expirada.')
            continue  # Senha expirou

        # Verificar genero do nome
        genero = Genderize().get([PRIMEIRO_NOME])
        if genero[0]['gender'] == 'female':
            SAUDACAO = 'Estimada colega,'
        else:
            SAUDACAO = 'Estimado colega,'

        postmark = PostmarkClient(server_token=POSTMARK_TOKEN)
        DIA_EXPIRACAO = dias_extenso \
            + ' (' + data_expiracao.strftime('%d/%m/%Y') + ')'

        # Enviar email
        try:
            email = postmark.emails.send_with_template(
                    TemplateAlias='senha-quase-expirar',
                    TemplateModel={
                        'saudacao': SAUDACAO,
                        'dia_expiracao': DIA_EXPIRACAO
                    },
                    TrackLinks='HtmlOnly',
                    From=os.getenv('FROM_EMAIL'),
                    To=NOME_COMPLETO + ' <' + EMAIL_COLEGA.lower() + '>',
                    MessageStream='outbound'
            )
            print(str(i+1) + '. Envio para '
                            + NOME_COMPLETO + ': '
                            + email['Message'] + ' (código '
                            + str(email['ErrorCode']) + ')')
        except Exception as ex:
            print(str(i+1) + '. ' + ex.args[0])
            pass
print('Mensagens enviadas.')

No próximo artigo, publicarei o script para envio de notificação geral, usando entradas na linha de comandos.

Adicionar Comentário

comments powered by Disqus

Vitor Pinho

avatar

Administrador de sistemas e informático a mais de 19 anos, desde o tempo do MS-DOS e Windows NT e autodidata em tudo que é relaccionado a IT.