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 nomemail
- endereço de correiogivenName
- Primeiro nomepwdLastSet
- Data da última alteração de senha
Os campo usados estão disponiveis na tabela Atribute Editor nas propriedades de cada utilizador.
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 podes incluir um print
para te notificar sobre 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.')