From 5671f9bc5c27166e694ebca3cfa10533b4d2ce9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ses=C3=B3stris=20Vieira?= Date: Fri, 20 Oct 2023 08:45:04 -0300 Subject: [PATCH] =?UTF-8?q?Corrige=20sincroniza=C3=A7=C3=A3o=20LDAP=20e=20?= =?UTF-8?q?dados=20de=20servidores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sigi/apps/servidores/jobs/__init__.py | 0 sigi/apps/servidores/jobs/daily/__init__.py | 0 sigi/apps/servidores/jobs/daily/sync_ldap.py | 136 ++++++++ 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 | 0 .../management/commands/mescla_servidor.py | 97 +++--- .../management/commands/mescla_usuarios.py | 62 ++++ .../servidores/management/commands/migra.py | 247 -------------- .../management/commands/sync_ldap.py | 203 ------------ .../management/commands/test_sync_ldap.py | 284 ---------------- ...14_servidor_ldap_dn_alter_servidor_user.py | 30 ++ .../migrations/0015_limpa_servidores_users.py | 313 ++++++++++++++++++ sigi/apps/servidores/models.py | 49 +-- sigi/apps/servidores/utils.py | 207 ++++++++++++ sigi/settings.py | 9 +- 17 files changed, 814 insertions(+), 823 deletions(-) create mode 100644 sigi/apps/servidores/jobs/__init__.py create mode 100644 sigi/apps/servidores/jobs/daily/__init__.py create mode 100644 sigi/apps/servidores/jobs/daily/sync_ldap.py create mode 100644 sigi/apps/servidores/jobs/hourly/__init__.py create mode 100644 sigi/apps/servidores/jobs/monthly/__init__.py create mode 100644 sigi/apps/servidores/jobs/weekly/__init__.py create mode 100644 sigi/apps/servidores/jobs/yearly/__init__.py create mode 100644 sigi/apps/servidores/management/commands/mescla_usuarios.py delete mode 100644 sigi/apps/servidores/management/commands/migra.py delete mode 100644 sigi/apps/servidores/management/commands/sync_ldap.py delete mode 100644 sigi/apps/servidores/management/commands/test_sync_ldap.py create mode 100644 sigi/apps/servidores/migrations/0014_servidor_ldap_dn_alter_servidor_user.py create mode 100644 sigi/apps/servidores/migrations/0015_limpa_servidores_users.py create mode 100644 sigi/apps/servidores/utils.py diff --git a/sigi/apps/servidores/jobs/__init__.py b/sigi/apps/servidores/jobs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/servidores/jobs/daily/__init__.py b/sigi/apps/servidores/jobs/daily/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/servidores/jobs/daily/sync_ldap.py b/sigi/apps/servidores/jobs/daily/sync_ldap.py new file mode 100644 index 0000000..92f2990 --- /dev/null +++ b/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")) diff --git a/sigi/apps/servidores/jobs/hourly/__init__.py b/sigi/apps/servidores/jobs/hourly/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/servidores/jobs/monthly/__init__.py b/sigi/apps/servidores/jobs/monthly/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/servidores/jobs/weekly/__init__.py b/sigi/apps/servidores/jobs/weekly/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/servidores/jobs/yearly/__init__.py b/sigi/apps/servidores/jobs/yearly/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigi/apps/servidores/management/commands/mescla_servidor.py b/sigi/apps/servidores/management/commands/mescla_servidor.py index 020d450..46cf159 100644 --- a/sigi/apps/servidores/management/commands/mescla_servidor.py +++ b/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") + source_id = options["source_id"][0] + target_id = options["target_id"][0] + + 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 - - old_id = args[0] - new_id = args[1] - - old = Servidor.objects.get(id=old_id) - new = Servidor.objects.get(id=new_id) 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) + resp = input("Continuar? [sim / NÃO]: ") - 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) - - 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!") diff --git a/sigi/apps/servidores/management/commands/mescla_usuarios.py b/sigi/apps/servidores/management/commands/mescla_usuarios.py new file mode 100644 index 0000000..1071c3e --- /dev/null +++ b/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!") diff --git a/sigi/apps/servidores/management/commands/migra.py b/sigi/apps/servidores/management/commands/migra.py deleted file mode 100644 index 38ee517..0000000 --- a/sigi/apps/servidores/management/commands/migra.py +++ /dev/null @@ -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() diff --git a/sigi/apps/servidores/management/commands/sync_ldap.py b/sigi/apps/servidores/management/commands/sync_ldap.py deleted file mode 100644 index 7cb4ec1..0000000 --- a/sigi/apps/servidores/management/commands/sync_ldap.py +++ /dev/null @@ -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.") diff --git a/sigi/apps/servidores/management/commands/test_sync_ldap.py b/sigi/apps/servidores/management/commands/test_sync_ldap.py deleted file mode 100644 index 089ee69..0000000 --- a/sigi/apps/servidores/management/commands/test_sync_ldap.py +++ /dev/null @@ -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 == "" diff --git a/sigi/apps/servidores/migrations/0014_servidor_ldap_dn_alter_servidor_user.py b/sigi/apps/servidores/migrations/0014_servidor_ldap_dn_alter_servidor_user.py new file mode 100644 index 0000000..66dc4e6 --- /dev/null +++ b/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, + ), + ), + ] diff --git a/sigi/apps/servidores/migrations/0015_limpa_servidores_users.py b/sigi/apps/servidores/migrations/0015_limpa_servidores_users.py new file mode 100644 index 0000000..628af83 --- /dev/null +++ b/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)] diff --git a/sigi/apps/servidores/models.py b/sigi/apps/servidores/models.py index 8456a84..3f22b3e 100644 --- a/sigi/apps/servidores/models.py +++ b/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,25 +107,14 @@ 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, - ) - 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.save() + + from sigi.apps.servidores.utils import servidor_create_or_update + + result, servidor = servidor_create_or_update( + instance.ldap_user.attrs, commit=False + ) + servidor.user = instance + servidor.save() # Hack horrível para ajustar o first_name e o last_name do User criado pelo @@ -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) diff --git a/sigi/apps/servidores/utils.py b/sigi/apps/servidores/utils.py new file mode 100644 index 0000000..325c758 --- /dev/null +++ b/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" diff --git a/sigi/settings.py b/sigi/settings.py index 9578d89..6bf9165 100644 --- a/sigi/settings.py +++ b/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(