enh Obtener los datos de Gestión de Identidades mediante un Web Service

Solicité a G.I. que crearan un WS para obtener los datos, en vez de tener queusar
una conexión directa a la base de datos.

Esto permite prescindir de los drivers de Oracle, adelgazando el contenedor Docker.
parent 1520f785
Pipeline #530 failed with stage
in 0 seconds
......@@ -11,3 +11,7 @@ DJANGO_SECRET_KEY="s+s6-^@s&=xg@l7!qsprhd5-1-0*wuh*0tjm_5)%uq(5q(nc4c"
# For debugging and error reporting
DEBUG=False
WSDL_IDENTIDAD=https://sitio.red/ruta/Identidad?wsdl
USER_IDENTIDAD=neo
PASS_IDENTIDAD=swordfish
......@@ -4,19 +4,17 @@ LABEL maintainer="Enrique Matías Sánchez <quique@unizar.es>"
# Set environment variables
# Don't write .pyc files
ENV PYTHONDONTWRITEBYTECODE 1
# All output to stdout will be flushed immediately
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1 \
PYTHONUNBUFFERED 1
# Install packages needed to run your application (not build deps):
# libao1 -- for Oracle Instant Client
# libmariadbclient-client -- for running database commands
# libpcre3 -- for uWSGI internal routing support
# xmlsec1 -- required for SAML auth
# mime-support -- for mime types when serving static files
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
libaio1 \
libmariadb3 \
libpcre3 \
libxmlsec1-openssl \
......@@ -25,16 +23,6 @@ RUN apt-get update \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# In order to use this Dockerfile, you should first download the following RPM files:
# oracle-instantclient19.3-basiclite_19.3.0.0.0-2_amd64.deb
# oracle-instantclient19.3-devel_19.3.0.0.0-2_amd64.deb
# from <http://www.oracle.com/technetwork/topics/linuxx86-64soft-092277.html>.
# Then use `alien` to convert them to Debian packages,
# and put the resulting .deb files in the same directory as this Dockerfile.
COPY *deb ./
RUN apt-get install ./*deb \
&& rm *deb
# Copy the requirements file to the container image
COPY requirements.txt ./
......
......@@ -6,6 +6,7 @@ verify_ssl = true
[dev-packages]
pylint = "*"
coverage = "*"
rope = "*"
[packages]
django = "==3.0.3" # Remember to re-generate `requirements.txt`
......@@ -21,6 +22,7 @@ django-tables2 = "==2.2.1"
django-templated-email = "*"
pypandoc = "*"
bleach = "*"
zeep = "*"
[requires]
python_version = "3.7"
{
"_meta": {
"hash": {
"sha256": "8baca2a9970e9d57480fbb508d2dd643674edd37cf2387da8d7f038fdcbb9293"
"sha256": "3f7508a20e55456ca24d123f67347712c58a4fec96b5c2b36f7461de7193a0cc"
},
"pipfile-spec": 6,
"requires": {
......@@ -16,6 +16,13 @@
]
},
"default": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"asgiref": {
"hashes": [
"sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
......@@ -23,6 +30,13 @@
],
"version": "==3.2.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"bleach": {
"hashes": [
"sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16",
......@@ -31,6 +45,13 @@
"index": "pypi",
"version": "==3.1.0"
},
"cached-property": {
"hashes": [
"sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f",
"sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504"
],
"version": "==1.5.1"
},
"certifi": {
"hashes": [
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
......@@ -250,6 +271,13 @@
],
"version": "==1.3.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.9.1"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
......@@ -311,6 +339,14 @@
"sha256:e573c0172174973223d874ffd158ecd4e0faa761015474385289a6468dd29ed6"
],
"version": "==1.3.3"
},
"zeep": {
"hashes": [
"sha256:0e98669cfeb60756231ae185498f9ae21b30b2681786b8de58ed34c3b93e41dd",
"sha256:59a4068ab3817b589ee21c6c34166c35baa8f2be7ad69f045d005076a29911c1"
],
"index": "pypi",
"version": "==3.4.0"
}
},
"develop": {
......@@ -406,6 +442,15 @@
"index": "pypi",
"version": "==2.4.4"
},
"rope": {
"hashes": [
"sha256:52423a7eebb5306a6d63bdc91a7c657db51ac9babfb8341c9a1440831ecf3203",
"sha256:ae1fa2fd56f64f4cc9be46493ce54bed0dd12dee03980c61a4393d89d84029ad",
"sha256:d2830142c2e046f5fc26a022fe680675b6f48f81c7fc1f03a950706e746e9dfe"
],
"index": "pypi",
"version": "==0.16.0"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
......
# standard library
import json
from django.core.validators import ValidationError, validate_email
from django.db import connections
class UsuarioNoEncontrado(Exception):
"""Excepción elevada cuando no se encuentra al usuario en Gestión de Identidades"""
# third-party
import zeep
from annoying.functions import get_config
from requests import Session
from requests.auth import HTTPBasicAuth
from requests.exceptions import ConnectionError as RequestConnectionError
def dictfetchall(cursor):
"""Return all rows from a cursor as a dict."""
columns = [col[0] for col in cursor.description]
return [dict(zip(columns, row)) for row in cursor.fetchall()]
# Django
from django.contrib import messages
from django.core.validators import ValidationError, validate_email
def dictfetchone(cursor):
"""Return a row from a cursor as a dict."""
columns = (col[0] for col in cursor.description)
row = cursor.fetchone()
if row:
return dict(zip(columns, row))
def get_identidad(strategy, response, user, *args, **kwargs):
"""Actualiza el usuario con los datos obtenidos de Gestión de Identidades."""
raise UsuarioNoEncontrado(
"Usuario desconocido. No se ha encontrado en Gestión de Identidades."
wsdl = get_config('WSDL_IDENTIDAD')
session = Session()
session.auth = HTTPBasicAuth(
get_config("USER_IDENTIDAD"), get_config("PASS_IDENTIDAD")
)
def get_identidad(strategy, response, user, *args, **kwargs):
"""Actualiza el usuario con los datos obtenidos de Gestión de Identidades."""
with connections["identidades"].cursor() as cursor:
cursor.execute(
"SELECT * FROM GESTIDEN.GI_V_IDENTIDAD WHERE NIP = %s AND INDBAJA = 'N'",
[user.username],
)
identidad = dictfetchone(cursor)
cursor.execute(
"SELECT * FROM GESTIDEN.GI_V_IDENTIDAD_PERFIL WHERE NIP = %s",
[user.username],
)
perfiles = dictfetchall(cursor)
cursor.execute(
"SELECT * FROM GESTIDEN.GI_V_IDENTIDAD_VINCULACION WHERE NIP = %s",
[user.username],
try:
client = zeep.Client(
wsdl=wsdl, transport=zeep.transports.Transport(session=session)
)
vinculaciones = dictfetchall(cursor)
user.first_name = identidad.get("NOMBRE")
user.last_name = identidad.get("APELLIDO_1")
user.last_name_2 = identidad.get("APELLIDO_2")
except RequestConnectionError:
raise RequestConnectionError('No fue posible conectarse al WS de Identidades.')
except: # noqa: E722
raise
response = client.service.obtenIdentidad(user.username)
if response.aviso:
# El WS produjo una advertencia. La mostramos y seguimos.
messages.warning(strategy.request, response.descripcionAviso)
if response.error:
# La comunicación con el WS fue correcta, pero éste devolvió un error. Finalizamos.
raise Exception(response.descripcionResultado)
identidad = response.identidad
user.first_name = identidad.nombre
user.last_name = identidad.primerApellido
user.last_name_2 = identidad.segundoApellido
correo_personal = (
identidad.get("CORREO_PERSONAL")
if is_email_valid(identidad.get("CORREO_PERSONAL"))
else None
identidad.correoPersonal if is_email_valid(identidad.correoPersonal) else None
)
correo_principal = (
identidad.get("CORREO_PRINCIPAL")
if is_email_valid(identidad.get("CORREO_PRINCIPAL"))
else None
identidad.correoPrincipal if is_email_valid(identidad.correoPrincipal) else None
)
# El email es un campo NOT NULL en el modelo.
user.email = correo_personal or correo_principal or ""
user.is_active = identidad.get("ACTIVO") != "N"
user.nombre_oficial = identidad.get("NOMBRE_ADMIN")
user.numero_documento = identidad.get("DOC_ID")
user.sexo = identidad.get("SEXO")
user.sexo_oficial = identidad.get("SEXO_ADMIN")
user.tipo_documento = identidad.get("TIPO_DOC_ID")
perfil_centro_id_nks = {
perfil.get("COD_CENTRO")
for perfil in perfiles
if perfil.get("COD_CENTRO") not in (0, None)
}
vinculacion_centro_id_nks = {
vinculacion.get("COD_CENTRO")
for vinculacion in vinculaciones
if vinculacion.get("COD_CENTRO") not in (0, None)
}
# Los objetos de tipo `set` no son serializables a JSON.
centro_id_nks = list(perfil_centro_id_nks.union(vinculacion_centro_id_nks))
user.centro_id_nks = json.dumps(centro_id_nks)
perfil_departamento_id_nks = {
perfil.get("COD_DEPARTAMENTO")
for perfil in perfiles
if perfil.get("COD_DEPARTAMENTO") not in (0, None)
}
vinculacion_departamento_id_nks = {
vinculacion.get("COD_DEPARTAMENTO")
for vinculacion in vinculaciones
if vinculacion.get("COD_DEPARTAMENTO") not in (0, None)
}
departamento_id_nks = list(
perfil_departamento_id_nks.union(vinculacion_departamento_id_nks)
)
user.departamento_id_nks = json.dumps(departamento_id_nks)
colectivos = list({perfil.get("COD_PERFIL") for perfil in perfiles})
# Si el usuario es PDI o PAS de un centro adscrito, añadimos el colectivo "ADS".
cod_vinculaciones = {
vinculacion.get("COD_VINCULACION") for vinculacion in vinculaciones
}
if any(cod_adscritos in cod_vinculaciones for cod_adscritos in (12, 13, 42)):
colectivos.append("ADS")
user.email = correo_personal or correo_principal or ''
user.is_active = identidad.activo != 'N'
user.nombre_oficial = identidad.nombreAdmin
user.numero_documento = identidad.documento
user.sexo = identidad.sexo
user.sexo_oficial = identidad.sexoAdmin
user.tipo_documento = identidad.tipoDocumento
user.centro_id_nks = json.dumps(identidad.centros)
user.departamento_id_nks = json.dumps(identidad.departamentos)
colectivos = identidad.perfiles
cods_vinculaciones = identidad.vinculaciones
if any(cod_adscritos in cods_vinculaciones for cod_adscritos in (12, 13, 42)):
colectivos.append('ADS')
user.colectivos = json.dumps(colectivos)
# user.save()
......
from datetime import date
from accounts.models import CustomUser
from accounts.pipeline import UsuarioNoEncontrado, get_identidad
from accounts.pipeline import get_identidad
from django import forms
from django.db.models import BLANK_CHOICE_DASH
from django.utils.translation import gettext_lazy as _
......@@ -15,8 +15,7 @@ class InvitacionForm(forms.ModelForm):
nip = forms.IntegerField(
label=_("NIP"),
help_text=_(
"Número de Identificación Personal en la Universidad de Zaragoza "
"de la persona a invitar."
"Número de Identificación Personal en la Universidad de Zaragoza de la persona a invitar."
),
)
......@@ -27,30 +26,50 @@ class InvitacionForm(forms.ModelForm):
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)
def _crear_usuario(self, nip):
"""Crea un registro de usuario con el nip indicado y los datos de G.I."""
usuario = CustomUser.objects.create_user(username=nip)
try:
get_identidad(load_strategy(self.request), None, usuario)
except Exception as ex:
# Si Gestión de Identidades devuelve un error, borramos el usuario
# y finalizamos mostrando el mensaje de error.
usuario.delete()
raise forms.ValidationError("ERROR: " + str(ex))
# HACK - Indicamos que la autenticación es vía Single Sign On con SAML.
usuario_social = UserSocialAuth(
uid=f"lord:{usuario.username}", provider="saml", user=usuario
)
usuario_social.save()
return usuario
def clean(self):
cleaned_data = super().clean()
nip = cleaned_data.get("nip")
# Comprobamos si el usuario ya existe en el sistema.
usuario = CustomUser.objects.get_or_none(username=nip)
# Si no existe previamente, lo creamos y actualizamos con los datos de Gestión de Identidades.
if not usuario:
usuario = CustomUser.objects.create_user(username=nip)
try:
get_identidad(load_strategy(self.request), None, usuario)
except UsuarioNoEncontrado:
usuario.delete()
raise forms.ValidationError(
_(f"¡Usuario desconocido! No se ha encontrado el NIP «{nip}».")
)
# HACK
usuario_social = UserSocialAuth(
uid=f"lord:{usuario.username}", provider="saml", user=usuario
)
usuario_social.save()
usuario = self._crear_usuario(nip)
get_identidad(load_strategy(self.request), None, usuario)
# El usuario existe. Actualizamos sus datos con los de Gestión de Identidades.
try:
get_identidad(load_strategy(self.request), None, usuario)
except Exception as ex:
# Si Gestión de Identidades devuelve un error, y finalizamos mostrando el mensaje de error.
raise forms.ValidationError('ERROR: ' + str(ex))
# Si el usuario no está activo, finalizamos explicando esta circunstancia.
if not usuario.is_active:
raise forms.ValidationError(
_("Usuario inactivo en el sistema de Gestión de Identidades")
)
# Si el usuario no tiene un email válido, finalizamos explicando esta circunstancia.
if not usuario.email:
raise forms.ValidationError(
_(
......@@ -76,10 +95,11 @@ class InvitacionForm(forms.ModelForm):
self.proyecto.programa.nombre_corto != "PIPOUZ"
and usuario.get_colectivo_principal() == "EST"
):
estudiantes = []
for vinculado in vinculados:
if vinculado.get_colectivo_principal() == "EST":
estudiantes.append(vinculado)
estudiantes = [
vinculado
for vinculado in vinculados
if vinculado.get_colectivo_principal() == "EST"
]
if len(estudiantes) >= self.proyecto.programa.max_estudiantes:
nombres_estudiantes = ", ".join(
list(map(lambda e: e.get_full_name(), estudiantes))
......
......@@ -114,19 +114,7 @@ DATABASES = {
"OPTIONS": {
"charset": "utf8mb4" # Requires `innodb_default_row_format = dynamic`
},
},
"identidades": {
"ENGINE": "django.db.backends.oracle", # Database engine
"NAME": "DELFOS", # Database name
"USER": "dodona", # Database user
"PASSWORD": "PopolWuj", # Database password
"HOST": "oraculo.unizar.es", # Set to empty string for localhost.
"PORT": "1521", # Set to empty string for default.
# Additional database options
# "OPTIONS": {
# "charset": "WE8ISO8859P1",
# }
},
}
}
......@@ -375,3 +363,8 @@ ALLOWED_STYLES = ["background-color", "color", "text-align"]
ALLOWED_PROTOCOLS = ["data", "http", "https", "mailto"]
X_FRAME_OPTIONS = "SAMEORIGIN" # Required by SummernoteWidget on Django 3.x
# WEB SERVICE DE GESTIÓN DE IDENTIDADES
WSDL_IDENTIDAD = os.environ.get("WSDL_IDENTIDAD")
USER_IDENTIDAD = os.environ.get("USER_IDENTIDAD")
PASS_IDENTIDAD = os.environ.get("PASS_IDENTIDAD")
-i https://pypi.org/simple
appdirs==1.4.3
asgiref==3.2.3
attrs==19.3.0
bleach==3.1.0
cached-property==1.5.1
certifi==2019.11.28
chardet==3.0.4
cx-oracle==7.3.0
......@@ -24,6 +27,7 @@ python3-openid==3.1.0 ; python_version >= '3.0'
python3-saml==1.9.0
pytz==2019.3
requests-oauthlib==1.3.0
requests-toolbelt==0.9.1
requests==2.22.0
six==1.14.0
social-auth-app-django==3.1.0
......@@ -33,3 +37,4 @@ urllib3==1.25.8
webencodings==0.5.1
wheel==0.34.2
xmlsec==1.3.3
zeep==3.4.0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment