Browse Source

Corrige sincronização LDAP e dados de servidores

pull/167/head 3.0.55
Sesóstris Vieira 1 year ago
parent
commit
5671f9bc5c
  1. 0
      sigi/apps/servidores/jobs/__init__.py
  2. 0
      sigi/apps/servidores/jobs/daily/__init__.py
  3. 136
      sigi/apps/servidores/jobs/daily/sync_ldap.py
  4. 0
      sigi/apps/servidores/jobs/hourly/__init__.py
  5. 0
      sigi/apps/servidores/jobs/monthly/__init__.py
  6. 0
      sigi/apps/servidores/jobs/weekly/__init__.py
  7. 0
      sigi/apps/servidores/jobs/yearly/__init__.py
  8. 97
      sigi/apps/servidores/management/commands/mescla_servidor.py
  9. 62
      sigi/apps/servidores/management/commands/mescla_usuarios.py
  10. 247
      sigi/apps/servidores/management/commands/migra.py
  11. 203
      sigi/apps/servidores/management/commands/sync_ldap.py
  12. 284
      sigi/apps/servidores/management/commands/test_sync_ldap.py
  13. 30
      sigi/apps/servidores/migrations/0014_servidor_ldap_dn_alter_servidor_user.py
  14. 313
      sigi/apps/servidores/migrations/0015_limpa_servidores_users.py
  15. 45
      sigi/apps/servidores/models.py
  16. 207
      sigi/apps/servidores/utils.py
  17. 9
      sigi/settings.py

0
sigi/apps/servidores/jobs/__init__.py

0
sigi/apps/servidores/jobs/daily/__init__.py

136
sigi/apps/servidores/jobs/daily/sync_ldap.py

@ -0,0 +1,136 @@
import ldap
from django.conf import settings
from django.utils.translation import gettext as _
from django_auth_ldap.config import _DeepStringCoder
from django_extensions.management.jobs import DailyJob
from sigi.apps.utils.management.jobs import JobReportMixin
from sigi.apps.servidores.models import Servico, Servidor
from sigi.apps.servidores.utils import (
servidor_update_from_ldap,
servidor_create_or_update,
user_staff_and_group,
)
class Job(JobReportMixin, DailyJob):
help = _("Sincroniza servidores com o ldap")
report_data = []
def do_job(self):
coder = _DeepStringCoder("utf8")
connect = ldap.initialize(settings.AUTH_LDAP_SERVER_URI)
connect.protocol_version = 3
connect.set_option(ldap.OPT_REFERRALS, 0)
connect.simple_bind_s(
settings.AUTH_LDAP_BIND_DN, settings.AUTH_LDAP_BIND_PASSWORD
)
page_control = ldap.controls.SimplePagedResultsControl(
True, size=1000, cookie=""
)
total_ldap = 0
total_create = 0
total_update = 0
total_deactive = 0
servidores = {
s.ldap_dn: s
for s in Servidor.objects.exclude(ldap_dn="").exclude(externo=True)
}
while True:
response = connect.search_ext(
settings.AUTH_LDAP_USER,
ldap.SCOPE_ONELEVEL,
settings.LDAP_GET_ALL_USERS,
serverctrls=[page_control],
)
rtype, rdata, rmsgid, serverctrls = connect.result3(response)
decoded_data = coder.decode(rdata)
controls = [
control
for control in serverctrls
if control.controlType
== ldap.controls.SimplePagedResultsControl.controlType
]
if not controls:
raise Exception("The LDAP server ignores RFC 2696 control")
for dn, ldap_user in decoded_data:
total_ldap += 1
resp, servidor = servidor_create_or_update(
ldap_attrs=ldap_user
)
if servidor.user:
user_staff_and_group(servidor.user, ldap_user)
if resp == servidor_create_or_update.UPDATED:
total_update += 1
self.report_data.append(
_(f"{servidor.nome_completo} atualizado")
)
elif resp == servidor_create_or_update.CREATED:
total_create += 1
self.report_data.append(
_(f"{servidor.nome_completo} criado")
)
if dn in servidores:
del servidores[dn]
if not controls[0].cookie:
break
page_control.cookie = controls[0].cookie
for dn, servidor in servidores.items():
rdata = connect.search_s(
settings.AUTH_LDAP_USER,
ldap.SCOPE_SUBTREE,
ldap.filter.filter_format("(distinguishedName=%s)", [dn]),
)
if rdata:
ldap_attrs = coder.decode(rdata[0][1])
if servidor.user:
user_staff_and_group(servidor.user, ldap_attrs)
if (
servidor_update_from_ldap(servidor, ldap_attrs)
== servidor_create_or_update.UPDATED
):
total_update += 1
else:
if servidor.user:
servidor.user.is_active = False
total_deactive += 1
# Reporta servidores que não estão no LDAP e também não são externos
self.report_data.append("")
self.report_data.append(
_(
"Servidores que não estão no LDAP e também não estão marcados "
"como Externos"
)
)
self.report_data.append(
_(
"============================================================="
"============="
)
)
self.report_data.append("")
for s in Servidor.objects.filter(
ldap_dn="", externo=False, sigi=False
).order_by("nome_completo"):
self.report_data.append(s.nome_completo)
self.report_data.append("")
self.report_data.append(_("RESUMO"))
self.report_data.append(_("======"))
self.report_data.append("")
self.report_data.append(_(f"{total_ldap} usuários lidos do LDAP"))
self.report_data.append(_(f"{total_create} novos servidores criados"))
self.report_data.append(_(f"{total_update} servidores atualizados"))
self.report_data.append(_(f"{total_deactive} usuários desativados"))

0
sigi/apps/servidores/jobs/hourly/__init__.py

0
sigi/apps/servidores/jobs/monthly/__init__.py

0
sigi/apps/servidores/jobs/weekly/__init__.py

0
sigi/apps/servidores/jobs/yearly/__init__.py

97
sigi/apps/servidores/management/commands/mescla_servidor.py

@ -1,70 +1,61 @@
# coding: utf-8
from django.contrib.auth.models import User, Group
from sigi.apps.servidores.models import Servidor
from django.core.management.base import BaseCommand
from sigi.apps.servidores.models import Servidor
from sigi.apps.servidores.utils import mescla_servidores
class Command(BaseCommand):
help = "Transfere os dados do servidor OLD para o servidor NEW."
args = "old_id new_id"
help = "Transfere os dados do servidor SOURCE para o servidor TARGET."
def add_arguments(self, parser):
parser.add_argument(
"source_id",
help="ID do servidor que será removido",
nargs=1,
type=int,
)
parser.add_argument(
"target_id",
help="ID do servidor que receberá os dados do que será removido",
nargs=1,
type=int,
)
def handle(self, *args, **options):
if len(args) != 2:
self.stderr.write("Informe old_id e new_id")
return
old_id = args[0]
new_id = args[1]
source_id = options["source_id"][0]
target_id = options["target_id"][0]
old = Servidor.objects.get(id=old_id)
new = Servidor.objects.get(id=new_id)
try:
servidor_source = Servidor.objects.get(id=source_id)
except Servidor.DoesNotExist:
self.stdout.write(
self.style.WARNING(f"Não existe servidor com ID {source_id}")
)
return
try:
servidor_target = Servidor.objects.get(id=target_id)
except Servidor.DoesNotExist:
self.stdout.write(
self.style.WARNING(f"Não existe servidor com ID {target_id}")
)
return
self.stdout.write(
self.style.WARNING(
"Transferir dados de {old_name} para {new_name}".format(
old_name=old.nome_completo, new_name=new.nome_completo
f"Transferir dados de {servidor_source.nome_completo} "
f"para {servidor_target.nome_completo}"
)
)
)
self.stdout.write("\t* Transferindo a carteira de atendimento...")
for casa in old.casas_que_gerencia.all():
new.casas_que_gerencia.add(casa)
old.casas_que_gerencia.remove(casa)
self.stdout.write("\t* Transferindo ocorrências registradas...")
old.ocorrencia_set.all().update(servidor_registro=new)
self.stdout.write("\t* Transferindo comentários de ocorrências...")
old.comentario_set.all().update(usuario=new)
self.stdout.write("\t* Transferindo convênios geridos...")
old.convenio_set.all().update(servidor_gestao=new)
self.stdout.write("\t* Transferindo convênios acompanhados...")
old.convenio_set.all().update(acompanha=new)
self.stdout.write("\t* Transferindo participação em eventos...")
old.equipe_evento.all().update(membro=new)
resp = input("Continuar? [sim / NÃO]: ")
self.stdout.write("\t* Transferindo convites para eventos...")
old.convite_set.all().update(servidor=new)
self.stdout.write("\t* Transferindo diagnósticos...")
old.diagnostico_set.all().update(responsavel=new)
self.stdout.write("\t* Transferindo participação em diagnósticos...")
old.equipe_set.all().update(membro=new)
self.stdout.write("\t* Transferindo dados de autenticação...")
if resp.lower() != "sim":
self.stdout.write(self.style.NOTICE("Abortado!"))
return
if new.user:
old.user.logentry_set.all().update(user=new)
old.user.delete()
else:
new.user = old.user
new.save()
old.user = None
old.save()
mescla_servidores(
servidor_source=servidor_source,
servidor_target=servidor_target,
verbose=True,
)
self.stdout.write("Concluído!")

62
sigi/apps/servidores/management/commands/mescla_usuarios.py

@ -0,0 +1,62 @@
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from sigi.apps.servidores.models import Servidor
from sigi.apps.servidores.utils import mescla_users
class Command(BaseCommand):
help = "Transfere os dados do usuário SOURCE para o usuário TARGET."
def add_arguments(self, parser):
parser.add_argument(
"source_name",
help="username do usuário que será removido",
nargs=1,
type=str,
)
parser.add_argument(
"target_name",
help="username do usuário que receberá os dados do que será removido",
nargs=1,
type=str,
)
def handle(self, *args, **options):
source_name = options["source_name"][0]
target_name = options["target_name"][0]
try:
user_source = User.objects.get(username__iexact=source_name)
except User.DoesNotExist:
self.stdout.write(
self.style.WARNING(f"Não existe o usuário {source_name}")
)
return
try:
user_target = User.objects.get(username__iexact=target_name)
except User.DoesNotExist:
self.stdout.write(
self.style.WARNING(f"Não existe o usuário {target_name}")
)
return
self.stdout.write(
self.style.WARNING(
f"Transferir dados de {user_source.get_full_name()} "
f"para {user_target.get_full_name()}"
)
)
resp = input("Continuar? [sim / NÃO]: ")
if resp.lower() != "sim":
self.stdout.write(self.style.NOTICE("Abortado!"))
return
mescla_users(
user_source=user_source,
user_target=user_target,
verbose=True,
)
self.stdout.write("Concluído!")

247
sigi/apps/servidores/management/commands/migra.py

@ -1,247 +0,0 @@
# coding= utf-8
import csv
import re
from datetime import datetime
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from django.utils.translation import gettext as _
from sigi.apps.contatos.models import Municipio
from sigi.apps.servidores.models import Servidor, Servico, Subsecretaria
# Funcao.objects.all().delete()
# Ferias.objects.all().delete()
# Licenca.objects.all().delete()
# for u in User.objects.filter(date_joined__gte=datetime(2011, 12, 9, 10, 58, 49, 83734)).all():
# u.servidor_set.all().delete()
# u.delete()
class MigrationError(Exception):
pass
class Command(BaseCommand):
help = _("Migra usuários do antigo Sistema de RH")
def to_date(self, data):
return datetime.strptime(data, "%Y-%m-%d 00:00:00")
def handle(self, *args, **options):
reader = csv.reader(
open("/tmp/pessoal.csv"), delimiter=",", quotechar='"'
)
BRASILIA = Municipio.objects.get(codigo_ibge=5300108)
# Read the column names from the first line of the file
fields = reader.next()
for row in reader:
# cria um dict com a primeira e a linha atual
pessoa = zip(fields, row)
p = {}
for name, value in pessoa:
p[name] = value.strip()
user = None
if not p["email"]:
username = ""
email = ""
elif not ("@interlegis" in p["email"]):
username = p["email"].split("@")[0].strip().lower()
email = ""
else:
username = p["email"].split("@")[0].strip().lower()
email = username + "@interlegis.leg.br"
# buscar usuário e servidor da linha atual
try:
# procuro o usuario por email do interlegis
if email:
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
email = username + "@interlegis.leg.br"
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
pass
if not user and username:
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
try:
user = User.objects.get(username=username + "__")
except User.DoesNotExist:
pass
if not user:
if not username:
raise MigrationError
if not email:
# cria um username a partir do email sem
# colidir com os usuarios ldap
username = username + "__"
names = p["nome_completo"].split(" ")
first_name = names[0]
last_name = " ".join(names[1:])
user = User.objects.create(
username=username,
email=email,
first_name=first_name,
last_name=last_name[:30],
is_active=False,
)
servidor = user.servidor
except Servidor.DoesNotExist:
servidor = Servidor.objects.create(
user=user,
nome_completo="%s %s" % (user.first_name, user.last_name),
)
except MigrationError as e:
continue
# mapeando dados simples
servidor.nome_completo = p["nome_completo"]
servidor.cpf = p["cpf"]
servidor.rg = p["identidade"]
servidor.apelido = p["username"]
servidor.matricula = p["matricula"]
servidor.ato_exoneracao = p["ato_exoneracao"]
servidor.ato_numero = p["ato_numero"]
servidor.ramal = p["ramal"]
if p["email"] and not "@interlegis" in p["email"]:
servidor.email_pessoal = p["email"]
if p["inativo"] == "-1":
servidor.user.is_active = False
else:
servidor.user.is_active = True
servidor.user.save()
if p["de_fora"] == "-1":
servidor.de_fora = True
else:
servidor.de_fora = False
if p["sexo"].upper() == "M":
servidor.sexo = "M"
elif p["sexo"].upper() == "F":
servidor.sexo = "F"
if p["turno"] == "1":
servidor.turno = "M"
elif p["turno"] == "2":
servidor.turno = "T"
elif p["turno"] == "3":
servidor.turno = "N"
if p["aniversario"]:
servidor.data_nascimento = self.to_date(p["aniversario"])
if p["data_nomeacao"]:
servidor.data_nomeacao = self.to_date(p["data_nomeacao"])
if p["secretaria_sigla"]:
if " - " in p["secretaria_nome"]:
secretaria_nome = p["secretaria_nome"].split(" - ")[1]
else:
secretaria_nome = p["secretaria_nome"]
secretaria = Subsecretaria.objects.get_or_create(
sigla=p["secretaria_sigla"], nome=secretaria_nome
)[0]
if " - " in p["servico_nome"]:
servico_nome = p["servico_nome"].split(" - ")[1]
else:
servico_nome = p["servico_nome"]
servico = Servico.objects.get_or_create(
sigla=p["servico_sigla"], nome=servico_nome
)[0]
servico.subsecretaria = secretaria
servico.save()
servidor.servico = servico
if p["telefone"]:
try:
t = servidor.telefones.get(numero=p["telefone"])
except:
t = servidor.telefones.create(numero=p["telefone"])
t.tipo = "F"
t.save()
if p["celular"]:
try:
t = servidor.telefones.get(numero=p["celular"])
except:
t = servidor.telefones.create(numero=p["celular"])
t.tipo = "M"
t.save()
if p["endereco"]:
try:
e = servidor.endereco.get(logradouro=p["endereco"])
except:
e = servidor.endereco.create(logradouro=p["endereco"])
e.municipio = BRASILIA
e.bairro = p["cidade"] # bizarro mas é isso mesmo
e.cep = re.sub("\D", "", p["cep"])
e.save()
servidor.apontamentos = p["apontamentos"]
servidor.obs = p["obs"]
if p["cargo"] or p["funcao"]:
funcao = servidor.funcao_set.get_or_create(
funcao=p["funcao"],
cargo=p["cargo"],
)[0]
if p["data_bap_entrada"]:
funcao.data_bap_entrada = self.to_date(
p["data_bap_entrada"]
)
if p["data_bap_saida"]:
funcao.data_bap_saida = self.to_date(p["data_bap_saida"])
if p["data_entrada"]:
funcao.inicio_funcao = self.to_date(p["data_entrada"])
if p["data_saida"]:
funcao.fim_funcao = self.to_date(p["data_saida"])
funcao.bap_entrada = p["bap_entrada"]
funcao.bap_saida = p["bap_saida"]
funcao.save()
if re.search(r"estagi.ri[o|a]", p["cargo"], re.I): # XXX i18n
# TODO inserir dados de estagio
pass
if p["inicio_ferias"] and p["final_ferias"]:
servidor.ferias_set.get_or_create(
inicio_ferias=self.to_date(p["inicio_ferias"]),
fim_ferias=self.to_date(p["final_ferias"]),
obs=p["obs_ferias"],
)
if p["inicio_licenca"] and p["fim_licenca"]:
servidor.licenca_set.get_or_create(
inicio_licenca=self.to_date(p["inicio_licenca"]),
fim_licenca=self.to_date(p["fim_licenca"]),
obs=p["obs_licenca"],
)
servidor.save()

203
sigi/apps/servidores/management/commands/sync_ldap.py

@ -1,203 +0,0 @@
# coding: utf-8
import ldap
from django.contrib.auth.models import User, Group
from django.core.management.base import BaseCommand
from sigi.apps.servidores.models import Servidor
from sigi.settings import *
class Command(BaseCommand):
help = "Sincroniza Usuários e Servidores com o LDAP"
def handle(self, *args, **options):
self.sync_groups()
self.sync_users()
def get_ldap_groups(self):
filter = "(&(objectclass=Group))"
values = [
"cn",
]
l = ldap.initialize(AUTH_LDAP_SERVER_URI)
try:
l.protocol_version = ldap.VERSION3
l.set_option(ldap.OPT_REFERRALS, 0)
l.simple_bind_s(
AUTH_LDAP_BIND_DN.encode("utf-8"), AUTH_LDAP_BIND_PASSWORD
)
page_control = ldap.controls.SimplePagedResultsControl(
True, size=1000, cookie=""
)
result = []
pages = 0
while True:
pages += 1
response = l.search_ext(
AUTH_LDAP_GROUP,
ldap.SCOPE_SUBTREE,
filter,
values,
serverctrls=[page_control],
)
rtype, rdata, rmsgid, serverctrls = l.result3(response)
result.extend(rdata)
controls = [
control
for control in serverctrls
if control.controlType
== ldap.controls.SimplePagedResultsControl.controlType
]
if not controls:
raise Exception("The server ignores RFC 2696 control")
if not controls[0].cookie:
break
page_control.cookie = controls[0].cookie
# result_id = l.search(AUTH_LDAP_GROUP, ldap.SCOPE_SUBTREE, filter, values)
# result_type, result_data = l.result(result_id, 1)
finally:
l.unbind()
return result
def get_ldap_users(self):
filter = "(&(objectclass=user)(|(memberof=CN=lgs_ilb,OU=GruposAutomaticosOU,DC=senado,DC=gov,DC=br)(memberof=CN=lgt_ilb,OU=GruposAutomaticosOU,DC=senado,DC=gov,DC=br)(memberof=CN=lge_ilb,OU=GruposAutomaticosOU,DC=senado,DC=gov,DC=br)))"
values = [
"sAMAccountName",
"userPrincipalName",
"givenName",
"sn",
"cn",
]
l = ldap.initialize(AUTH_LDAP_SERVER_URI)
try:
l.protocol_version = ldap.VERSION3
l.set_option(ldap.OPT_REFERRALS, 0)
l.simple_bind_s(
AUTH_LDAP_BIND_DN.encode("utf-8"), AUTH_LDAP_BIND_PASSWORD
)
page_control = ldap.controls.SimplePagedResultsControl(
True, size=1000, cookie=""
)
result = []
pages = 0
while True:
pages += 1
response = l.search_ext(
AUTH_LDAP_USER.encode("utf-8"),
ldap.SCOPE_SUBTREE,
filter,
values,
serverctrls=[page_control],
)
rtype, rdata, rmsgid, serverctrls = l.result3(response)
result.extend(rdata)
controls = [
control
for control in serverctrls
if control.controlType
== ldap.controls.SimplePagedResultsControl.controlType
]
if not controls:
raise Exception("The server ignores RFC 2696 control")
if not controls[0].cookie:
break
page_control.cookie = controls[0].cookie
# result_id = l.search(AUTH_LDAP_USER.encode('utf-8'), ldap.SCOPE_SUBTREE, filter, values)
# result_type, result_data = l.result(result_id, 1)
finally:
l.unbind()
return result
def sync_groups(self):
print("Syncing groups...")
ldap_groups = self.get_ldap_groups()
print(f"\tFetched groups: {ldap_groups}")
for ldap_group in ldap_groups:
try:
group_name = ldap_group[1]["cn"][0]
except:
pass
else:
try:
group = Group.objects.get(name=group_name)
except Group.DoesNotExist:
group = Group(name=group_name)
group.save()
print(f"\tGroup '{group_name}' created.")
print("Groups are synchronized.")
def sync_users(self):
print("Syncing users...")
ldap_users = self.get_ldap_users()
print(f"\tFetched users: {ldap_users}")
def get_ldap_property(ldap_user, property_name, default_value=None):
value = ldap_user[1].get(property_name, None)
return value[0].decode("utf8") if value else default_value
for ldap_user in ldap_users:
username = get_ldap_property(ldap_user, "sAMAccountName")
if username:
email = get_ldap_property(ldap_user, "userPrincipalName", "")
first_name = get_ldap_property(
ldap_user, "givenName", username
)
last_name = get_ldap_property(ldap_user, "sn", "")[:30]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
try:
user = User.objects.get(email=email)
old_username = user.username
user.username = username
print(
f"\tUser with email {email} had his/her username updated from [{old_username}] to [{username}]."
)
except User.DoesNotExist:
user = User.objects.create_user(
username=username,
first_name=first_name,
last_name=last_name,
email=email,
)
print(f"\tUser '{username}' created.")
if not user.first_name == first_name:
user.first_name = first_name
print(f"\tUser '{username}' first name updated.")
if not user.last_name == last_name:
user.last_name = last_name
print(f"\tUser '{username}' last name updated.")
if not user.email == email:
user.email = email
print(f"\tUser '{username}' email updated.")
nome_completo = get_ldap_property(ldap_user, "cn", "")
try:
servidor = user.servidor
except Servidor.DoesNotExist:
try:
servidor = Servidor.objects.get(
nome_completo=nome_completo
)
except Servidor.DoesNotExist:
servidor = user.servidor_set.create(
nome_completo=nome_completo
)
print(f"\tServidor '{nome_completo}' created.")
else:
if not servidor.nome_completo == nome_completo:
servidor.nome_completo = nome_completo
print(
f"\tFull name of Servidor '{nome_completo}' updated."
)
servidor.user = user
servidor.save()
user.save()
print("Users are synchronized.")

284
sigi/apps/servidores/management/commands/test_sync_ldap.py

@ -1,284 +0,0 @@
# -*- coding: utf-8 -*-
import pytest
from django.contrib.auth.models import User
from django_dynamic_fixture import G
from sigi.apps.servidores.management.commands.sync_ldap import Command
pytestmark = pytest.mark.django_db
class StubCommand(Command):
def __init__(self, users):
super(StubCommand, self).__init__()
self.users = users
def get_ldap_users(self):
return self.users
def create_stub_user(username, nome_completo, first_name, last_name, email):
user = G(
User,
username=username,
first_name=first_name,
last_name=last_name,
email=email,
)
user.servidor.nome_completo = nome_completo
return user
ALEX_LDAP, BRUNO_LDAP, RITA_LDAP = [
(
"...",
{
"cn": ["Alex Lima"],
"givenName": ["Alex"],
"sAMAccountName": ["alexlima"],
"sn": ["Lima"],
"userPrincipalName": ["alexlima@interlegis.leg.br"],
},
),
(
"...",
{
"cn": ["Bruno Almeida Prado"],
"givenName": ["Bruno"],
"sAMAccountName": ["bruno"],
"sn": ["Almeida Prado"],
"userPrincipalName": ["bruno@interlegis.leg.br"],
},
),
(
"...",
{
"cn": ["Cl\xc3\xa1udia de C\xc3\xa1ssia"],
"givenName": ["Cl\xc3\xa1udia"],
"sAMAccountName": ["claudia"],
"sn": ["de C\xc3\xa1ssia"],
"userPrincipalName": ["claudia@interlegis.leg.br"],
},
),
]
@pytest.mark.parametrize(
"before, ldap_users, after, messages",
[
# new user from ldap is created
(
[],
[ALEX_LDAP],
[
(
"alexlima",
"Alex Lima",
"Alex",
"Lima",
"alexlima@interlegis.leg.br",
)
],
"""
User 'alexlima' created.
Users are synchronized.
""",
),
# nothing changes
(
[
(
"alexlima",
"Alex Lima",
"Alex",
"Lima",
"alexlima@interlegis.leg.br",
)
],
[ALEX_LDAP],
[
(
"alexlima",
"Alex Lima",
"Alex",
"Lima",
"alexlima@interlegis.leg.br",
)
],
"""
Users are synchronized.
""",
),
# unicode encoding from LDAP data works well
(
[
(
"claudia",
"Cláudia de Cássia",
"Cláudia",
"de Cássia",
"claudia@interlegis.leg.br",
)
],
[RITA_LDAP],
[
(
"claudia",
"Cláudia de Cássia",
"Cláudia",
"de Cássia",
"claudia@interlegis.leg.br",
)
],
"""
Users are synchronized.
""",
),
# update: full name, first name, last name, email
(
[
(
"alexlima",
"___",
"___",
"___",
"___",
),
(
"bruno",
"Bruno Almeida Prado",
"___",
"Almeida Prado",
"___",
),
(
"claudia",
"___",
"Cláudia",
"___",
"claudia@interlegis.leg.br",
),
],
[ALEX_LDAP, BRUNO_LDAP, RITA_LDAP],
[
(
"alexlima",
"Alex Lima",
"Alex",
"Lima",
"alexlima@interlegis.leg.br",
),
(
"bruno",
"Bruno Almeida Prado",
"Bruno",
"Almeida Prado",
"bruno@interlegis.leg.br",
),
(
"claudia",
"Cláudia de Cássia",
"Cláudia",
"de Cássia",
"claudia@interlegis.leg.br",
),
],
"""
User 'alexlima' first name updated.
User 'alexlima' last name updated.
User 'alexlima' email updated.
Full name of Servidor 'Alex Lima' updated.
User 'bruno' first name updated.
User 'bruno' email updated.
Full name of Servidor 'Bruno Almeida Prado' updated.
User 'claudia' last name updated.
Full name of Servidor 'Cláudia de Cássia' updated.
Users are synchronized.
""",
),
# update username (username from LDAP not in base, so match user by email and update username)
# TODO: is this functionality really necessary? If not remove this and corresponding code
# connect servidor with nome_completo to user
# TODO: is this functionality really necessary? If not remove this and corresponding code
# create new servidor with nome_completo and connect to user
# TODO: is this functionality really necessary? If not remove this and corresponding code
# user not present in ldap is NOT deleted
(
[
(
"alexlima",
"Alex Lima",
"Alex",
"Lima",
"alexlima@interlegis.leg.br",
),
(
"bruno",
"Bruno Almeida Prado",
"Bruno",
"Almeida Prado",
"bruno@interlegis.leg.br",
),
(
"claudia",
"Cláudia de Cássia",
"Cláudia",
"de Cássia",
"claudia@interlegis.leg.br",
),
],
[ALEX_LDAP, RITA_LDAP],
[
(
"alexlima",
"Alex Lima",
"Alex",
"Lima",
"alexlima@interlegis.leg.br",
),
(
"bruno",
"Bruno Almeida Prado",
"Bruno",
"Almeida Prado",
"bruno@interlegis.leg.br",
),
(
"claudia",
"Cláudia de Cássia",
"Cláudia",
"de Cássia",
"claudia@interlegis.leg.br",
),
],
"""
Users are synchronized.
""",
),
],
)
def test_sync_users(before, ldap_users, after, messages, capsys):
# setup
for user_setup in before:
if type(user_setup) == tuple:
create_stub_user(*user_setup)
assert User.objects.count() == len(before)
command = StubCommand(ldap_users)
command.sync_users()
users = User.objects.all().order_by("username")
for user, expected in zip(users, after):
real = (
user.username,
user.servidor.nome_completo,
user.first_name,
user.last_name,
user.email,
)
assert real == expected
# feedbak messages
out, err = capsys.readouterr()
assert out.strip() == messages.strip()
assert err == ""

30
sigi/apps/servidores/migrations/0014_servidor_ldap_dn_alter_servidor_user.py

@ -0,0 +1,30 @@
# Generated by Django 4.2.4 on 2023-10-09 19:31
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("servidores", "0013_servidor_moodle_userid"),
]
operations = [
migrations.AddField(
model_name="servidor",
name="ldap_dn",
field=models.CharField(blank=True, editable=False, max_length=200),
),
migrations.AlterField(
model_name="servidor",
name="user",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
]

313
sigi/apps/servidores/migrations/0015_limpa_servidores_users.py

@ -0,0 +1,313 @@
# Generated by Django 4.2.4 on 2023-10-10 11:26
import ldap
from django.db import migrations
from django.db.models import Q
from django.conf import settings
from django.contrib.auth import get_user_model
from django_auth_ldap.config import _DeepStringCoder
from sigi.apps.utils import to_ascii
from sigi.apps.servidores.models import Servidor
from sigi.apps.servidores.utils import (
mescla_users,
mescla_servidores,
servidor_update_from_ldap,
user_staff_and_group,
)
def update_user_from_ldap(user, ldap_attrs):
for user_attr, ldap_attr in settings.AUTH_LDAP_USER_ATTR_MAP.items():
setattr(user, user_attr, ldap_attrs.get(ldap_attr, [""])[0])
user.username = ldap_attrs.get("sAMAccountName", [""])[0]
user_staff_and_group(user, ldap_attrs)
def forwards(apps, schema_editor):
User = get_user_model()
coder = _DeepStringCoder("utf8")
connect = ldap.initialize(settings.AUTH_LDAP_SERVER_URI)
connect.protocol_version = 3
connect.set_option(ldap.OPT_REFERRALS, 0)
connect.simple_bind_s(
settings.AUTH_LDAP_BIND_DN, settings.AUTH_LDAP_BIND_PASSWORD
)
# Eliminar usuário "administrador", transferindo seu log para usuário
# "interlegis".
if (
User.objects.filter(
username__in=["administrador", "interlegis"]
).count()
== 2
):
user = User.objects.get(username="administrador")
user_target = User.objects.get(username="interlegis")
user.logentry_set.update(user=user_target)
user.delete()
print(
f"\n\tUser {user.username} deleted. Logs to {user_target.username}"
)
# Unificar usuários com nomes iguais minusculos > MAIÚSCULOS
print("\tJoin users with same usernames in upper/lower cases...")
joined_names = set()
for user in User.objects.all():
if (
user.username.upper() not in joined_names
and User.objects.filter(username__iexact=user.username)
.exclude(id=user.id)
.exists()
):
user_source = User.objects.get(username=user.username.lower())
user_target = User.objects.get(username=user.username.upper())
print(f"\t\t{user_source.username} > {user_target.username}")
mescla_users(user_source, user_target)
joined_names.add(user.username.upper())
# Identificar e atualizar todos os users que existem no LDAP #
valid_users = dict()
for user in User.objects.all():
rdata = connect.search_s(
settings.AUTH_LDAP_USER,
ldap.SCOPE_SUBTREE,
ldap.filter.filter_format("(sAMAccountName=%s)", [user.username]),
)
if rdata:
dn, ldap_attrs = coder.decode(rdata[0])
valid_users[dn] = user
update_user_from_ldap(user, ldap_attrs)
if user.servidor:
servidor_update_from_ldap(user.servidor, ldap_attrs)
# Identifica servidores que estão no LDAP mas, por algum motivo, não estão
# vinculados a algum 'valid_user' identificado ali acima
for servidor in Servidor.objects.exclude(
user__in=valid_users.values()
).exclude(externo=True):
ldap_filter = ldap.filter.filter_format(
"(cn=%s)", [servidor.nome_completo]
)
rdata = connect.search_s(
settings.AUTH_LDAP_USER, ldap.SCOPE_SUBTREE, ldap_filter
)
if rdata:
dn, ldap_attrs = coder.decode(rdata[0])
ldap_user_name = ldap_attrs.get("sAMAccountName", [""])[0]
if servidor.user:
if User.objects.filter(username=ldap_user_name).exists():
user_target = User.objects.get(username=ldap_user_name)
mescla_users(servidor.user, user_target)
valid_users[dn] = user_target
else:
update_user_from_ldap(servidor.user, ldap_attrs)
valid_users[dn] = servidor.user
else:
if User.objects.filter(username=ldap_user_name).exists():
servidor.user = User.objects.get(username=ldap_user_name)
servidor.save()
valid_users[dn] = servidor.user
else:
servidor.ldap_dn = dn
servidor.save()
# Eliminar servidores e seus usuários que não têm nenhum vínculo no sigi
# e não constam na lista de valid_users
print("\tRemoving inactive users and servidores")
filter = (
Q(externo=False)
& Q(casas_que_gerencia=None)
& Q(chefe=None)
& Q(comentario=None)
& Q(convenio=None)
& Q(convenios_acompanhados=None)
& Q(equipe_evento=None)
& Q(itemsolicitado=None)
& Q(modulo_apresentador=None)
& Q(modulo_monitor=None)
& Q(ocorrencia=None)
& Q(orgao=None)
& Q(solicitacao=None)
)
inativos = Servidor.objects.filter(filter).exclude(
user__in=valid_users.values()
)
removed = User.objects.filter(servidor__in=inativos).delete()
print(
"\n".join(
[
f"\t\t{value} {key} records removed."
for key, value in removed[1].items()
]
)
)
removed = inativos.delete()
print(
"\n".join(
[
f"\t\t{value} {key} records removed."
for key, value in removed[1].items()
]
)
)
# Tratar os usuários que têm entrada de log mas não têm Servidor atribuído
for user in User.objects.filter(servidor=None, is_active=True).exclude(
logentry=None
):
nome_completo = user.get_full_name().strip().lower() or user.username
filter = [
Q(nome_completo__icontains=to_ascii(n.strip()))
for n in nome_completo.split(" ")
]
try:
# Tenta encontrar um servidor que tenha o mesmo nome e transferir
# todo o log para o usuário desse servidor.
servidor = Servidor.objects.get(*filter)
if servidor.user:
mescla_users(user, servidor.user)
print(
f"\tUser {user.username} deleted. "
f"Logs to {servidor.user.username}"
)
else:
servidor.user = user
servidor.save()
print(
f"\tUser {user.username} linked to "
f"servidor {servidor.nome_completo}"
)
except Servidor.DoesNotExist:
# Realmente não possui um servidor com o mesmo nome. O que resta é
# desativar o usuário para que não possa fazer login
user.is_active = False
user.save()
print(f"\tUser {user.username} deactivated.")
# Atualizar o campo ldap_dn dos servidores ativos do ILB
print("\tUpdating ldap_dn field in servidor objects...", end=" ")
ldap_filter = (
"(&(department=*ILB*)(!(title=*Desligad*))(!(title=*inativ*)))"
)
page_control = ldap.controls.SimplePagedResultsControl(
True, size=1000, cookie=""
)
updated = 0
while True:
response = connect.search_ext(
settings.AUTH_LDAP_USER,
ldap.SCOPE_ONELEVEL,
ldap_filter,
serverctrls=[page_control],
)
rtype, rdata, rmsgid, serverctrls = connect.result3(response)
decoded_data = coder.decode(rdata)
controls = [
control
for control in serverctrls
if control.controlType
== ldap.controls.SimplePagedResultsControl.controlType
]
if not controls:
raise Exception("The LDAP server ignores RFC 2696 control")
for dn, ldap_user in decoded_data:
user_name = ldap_user.get("sAMAccountName", [""])[0]
if User.objects.filter(username=user_name).exists():
user = User.objects.get(username=user_name)
servidor = user.servidor
if servidor:
servidor.ldap_dn = dn
servidor.save()
updated += 1
if not controls[0].cookie:
break
page_control.cookie = controls[0].cookie
print(f"{updated} servidores updated.")
# Mesclar usuários duplicados
print("\tJoin duplicated users ...")
joined = 0
for user in User.objects.exclude(Q(first_name="") & Q(last_name="")):
query = User.objects.filter(
first_name__iexact=user.first_name,
last_name__iexact=user.last_name,
).exclude(id=user.id)
if query.exists():
user2 = query.get()
dn1 = user.servidor.ldap_dn if user.servidor else ""
if dn1:
print(f"\t\t{user2} > {user}")
mescla_users(user_source=user2, user_target=user)
else:
print(f"\t\t{user} > {user2}")
mescla_users(user_source=user, user_target=user2)
# Vincular servidores com seu distinguishedName no LDAP, se existir
print("\tLink servidor with LDAP by distinguishedNames ... ")
for servidor in Servidor.objects.filter(externo=False, ldap_dn=""):
if servidor.user:
rdata = connect.search_s(
settings.AUTH_LDAP_USER,
ldap.SCOPE_SUBTREE,
ldap.filter.filter_format(
"(sAMAccountName=%s)", [servidor.user.username]
),
)
else:
rdata = connect.search_s(
settings.AUTH_LDAP_USER,
ldap.SCOPE_SUBTREE,
ldap.filter.filter_format(
"(cn=%s*)", [servidor.nome_completo]
),
)
if rdata:
servidor.ldap_dn = coder.decode(rdata[0][0])
servidor.save()
print(f"\t\t{servidor.nome_completo} > {servidor.ldap_dn}")
manuais = [
(1139, 1147), # Ricardo de Oliveira Murta
(1129, 274), # Adalberto Alves de Oliveira
(1134, 166), # Cláudio Morale
(1130, 108), # Janary Carvão Nunes
]
for source_id, target_id in manuais:
servidor_source = Servidor.objects.get(id=source_id)
servidor_target = Servidor.objects.get(id=target_id)
print(
f"\tJoining {servidor_source.nome_completo} "
f"to {servidor_target.nome_completo}"
)
mescla_servidores(servidor_source, servidor_target)
class Migration(migrations.Migration):
dependencies = [
("servidores", "0014_servidor_ldap_dn_alter_servidor_user"),
]
operations = [migrations.RunPython(forwards, migrations.RunPython.noop)]

45
sigi/apps/servidores/models.py

@ -39,7 +39,7 @@ class Servico(models.Model):
class Servidor(models.Model):
user = models.ForeignKey(
User, on_delete=models.CASCADE, null=True, blank=True
User, on_delete=models.SET_NULL, null=True, blank=True
)
nome_completo = models.CharField(max_length=128)
apelido = models.CharField(max_length=50, blank=True)
@ -69,6 +69,7 @@ class Servidor(models.Model):
sigi = models.BooleanField(
_("Servidor SIGI"), default=False, editable=False
)
ldap_dn = models.CharField(max_length=200, blank=True, editable=False)
class Meta:
ordering = ("nome_completo",)
@ -106,24 +107,13 @@ User.servidor = property(
def create_user_profile(sender, instance, created, **kwargs):
if not hasattr(instance, "ldap_user"):
return
sigla_servico = instance.ldap_user.attrs.get("department", [""])[0].split(
"-"
)[-1]
servico = Servico.objects.filter(sigla=sigla_servico).first()
if created and instance.is_staff:
Servidor.objects.create(
user=instance,
nome_completo="%s %s" % (instance.first_name, instance.last_name),
servico=servico,
from sigi.apps.servidores.utils import servidor_create_or_update
result, servidor = servidor_create_or_update(
instance.ldap_user.attrs, commit=False
)
elif (
not created
and instance.is_staff
and instance.servidor is not None
and instance.servidor.servico != servico
):
servidor = instance.servidor
servidor.servico = servico
servidor.user = instance
servidor.save()
@ -141,18 +131,7 @@ def ajusta_nome_usuario(sender, instance, *args, **kwargs):
# a propriedade Department, do LDAP, é uma unidade do ILB. Também desmembra
# o campo Department para gerar os nomes dos grupos que o User vai integrar
@receiver(populate_user, sender=LDAPBackend)
def user_staff_and_group(user, ldap_user, **kwargs):
dep = ldap_user.attrs.get("department", [""])[0]
title = ldap_user.attrs.get("title", [""])[0]
deps = dep.split("-")
titles = [s.strip().upper() for s in title.split("-", 1)]
group_names = [f"{d}-{t}" for d in deps for t in titles]
group_names.extend(deps)
group_names.extend(titles)
group_names.extend([dep, title.upper()])
user.is_staff = "ILB" in dep
user.save()
user.groups.clear()
for name in group_names:
group, created = Group.objects.get_or_create(name=name)
user.groups.add(group)
def populate_user_from_ldap(user, ldap_user, **kwargs):
from sigi.apps.servidores.utils import user_staff_and_group
user_staff_and_group(user, ldap_user.attrs)

207
sigi/apps/servidores/utils.py

@ -0,0 +1,207 @@
# Define algumas funções para manutenção de usuários e servidores
import ldap
from django.contrib.auth.models import Group
from django.conf import settings
from django.db.models import F
from django.db.models.fields.reverse_related import (
ForeignObjectRel,
ManyToOneRel,
ManyToManyRel,
)
from django.forms.models import model_to_dict
from sigi.apps.servidores.models import Servidor, Servico
def _message_out(message, verbose):
"""_message_out Imprime mensagem apenas em modo verboso
Arguments:
message -- Mensagem
verbose -- True para imprimir, False para não imprimir
"""
if verbose:
print(message)
def mescla_users(user_source, user_target, verbose=False):
"""mescla_users
Transfere todos os dados de user_source para user_target e exclui
user_target e o servidor vinculado a ele.
Arguments:
user_source -- Usuário a ser eliminado
user_target -- Usuário que receberá os dados de user_source
Keyword Arguments:
verbose -- Se True, imprime mensagens de progresso (default: {False})
"""
servidor_source = user_source.servidor
servidor_target = user_target.servidor
# Transfere registros de log
_message_out("Transferring log entries...", verbose)
user_source.logentry_set.update(user=user_target)
# Se a origem não tem servidor, só resta matá-lo agora
if servidor_source is None:
user_source.delete()
_message_out(f"User {user_source.username} deleted!", verbose)
return
# Se só a origem tem servidor
if servidor_source and not servidor_target:
# transfere o servidor inteiro e pronto
servidor_source.user = user_target
servidor_source.save()
_message_out(
f"Servidor {servidor_source.nome_completo} transferred "
f"to {user_target.username}",
verbose,
)
user_source.delete()
_message_out(f"User {user_source.username} deleted!", verbose)
return
# Só chega aqui se ambos tiverem servidor vinculado
_message_out("Merging servidores...", verbose)
mescla_servidores(servidor_source, servidor_target, verbose)
def mescla_servidores(servidor_source, servidor_target, verbose=False):
"""mescla_servidor
Transfere todos os dados de servidor_source para servidor_target e exclui
servidor_target e o user vinculado a ele.
Arguments:
servidor_source -- Servidor a ser eliminado
servidor_target -- Servidor que receberá os dados de servidor_source
Keyword Arguments:
verbose -- Se True, imprime mensagens de progresso (default: {False})
"""
user_source = servidor_source.user
user_target = servidor_target.user
for field in Servidor._meta.get_fields():
if not isinstance(field, ForeignObjectRel):
continue
accessor = field.get_accessor_name()
remote_field = field.remote_field.name
if isinstance(field, ManyToOneRel):
getattr(servidor_source, accessor).update(
**{remote_field: servidor_target}
)
_message_out(
f"Updating field {remote_field} in "
f"{field.remote_field.model._meta.verbose_name} "
f"to {servidor_target}",
verbose,
)
if isinstance(field, ManyToManyRel):
for obj in getattr(servidor_source, accessor).all():
getattr(servidor_target, accessor).add(obj)
getattr(servidor_source, accessor).remove(obj)
_message_out(
f"Transferring {obj} from source {accessor} "
f"to {servidor_target}",
verbose,
)
# Log dos usuários
if user_source and not user_target:
servidor_target.user = user_source
_message_out(
f"Target has no user. Transferring user {user_source.username} "
"from source",
verbose,
)
elif user_source and user_target:
user_source.logentry_set.update(user=user_target)
_message_out(
f"Transferring {user_source.username} logentries "
f"to {user_target.username}",
verbose,
)
user_source.delete()
_message_out(f"{user_source.username} deleted", verbose)
servidor_source.delete()
_message_out(f"{servidor_source.nome_completo} deleted", verbose)
def user_staff_and_group(user, ldap_attrs):
dep = ldap_attrs.get("department", [""])[0]
title = ldap_attrs.get("title", [""])[0]
deps = dep.split("-")
titles = [s.strip().upper() for s in title.split("-", 1)]
group_names = [f"{d}-{t}" for d in deps for t in titles]
group_names.extend(deps)
group_names.extend(titles)
group_names.extend([dep, title.upper()])
user.is_staff = "ILB" in dep
user.save()
user.groups.clear()
if user.is_staff:
# Só cria grupos para o ILB #
for name in group_names:
group, created = Group.objects.get_or_create(name=name)
user.groups.add(group)
def servidor_update_from_ldap(servidor, ldap_attrs, commit=True):
sigla_servico = ldap_attrs.get("department", [""])[0].split("-")[-1]
nome_cargo = ldap_attrs.get("title", [""])[0].split("-")[-1].strip()
nome_completo = ldap_attrs.get("name", [""])[0]
dn = ldap_attrs.get("distinguishedName", [""])[0]
cargo = f"{nome_cargo} - {sigla_servico}"
servico = Servico.objects.filter(sigla=sigla_servico).first()
initial = model_to_dict(servidor)
servidor.nome_completo = nome_completo
servidor.servico = servico
servidor.cargo = cargo
servidor.ldap_dn = dn
if servico is not None and nome_cargo.lower() in [
"chefe de serviço",
"coordenador",
]:
servidor.save() # Commit is needed to update servico instance
servico.responsavel = servidor
servico.save()
elif commit:
servidor.save()
return (
servidor_create_or_update.UNCHANGED
if initial == model_to_dict(servidor)
else servidor_create_or_update.UPDATED
)
def servidor_create_or_update(ldap_attrs, commit=True):
dn = ldap_attrs.get("distinguishedName", [""])[0]
if dn != "" and Servidor.objects.filter(ldap_dn=dn).exists():
servidor = Servidor.objects.get(ldap_dn=dn)
result_code = servidor_update_from_ldap(servidor, ldap_attrs, commit)
else:
servidor = Servidor()
servidor_update_from_ldap(servidor, ldap_attrs, commit)
result_code = servidor_create_or_update.CREATED
return (result_code, servidor)
servidor_create_or_update.UNCHANGED = "unchanged"
servidor_create_or_update.UPDATED = "updated"
servidor_create_or_update.CREATED = "created"

9
sigi/settings.py

@ -174,12 +174,19 @@ if env("AUTH_LDAP_SERVER_URI", default=None):
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
import ldap
LDAP_GET_ALL_USERS = env(
"LDAP_GET_ALL_USERS",
default="(&(department=*ILB*)(!(title=*Desligad*))(!(title=*inativ*)))",
)
AUTH_LDAP_SERVER_URI = env("AUTH_LDAP_SERVER_URI")
AUTH_LDAP_BIND_DN = env("AUTH_LDAP_BIND_DN")
AUTH_LDAP_BIND_PASSWORD = env("AUTH_LDAP_BIND_PASSWORD")
AUTH_LDAP_USER = env("AUTH_LDAP_USER")
AUTH_LDAP_USER_SEARCH = LDAPSearch(
AUTH_LDAP_USER, ldap.SCOPE_SUBTREE, env("AUTH_LDAP_USER_SEARCH_STRING")
AUTH_LDAP_USER,
ldap.SCOPE_ONELEVEL,
env("AUTH_LDAP_USER_SEARCH_STRING"),
)
AUTH_LDAP_GROUP = env("AUTH_LDAP_GROUP")
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(

Loading…
Cancel
Save