feat Usuarios y autenticación Single Sign On mediante SAML

parent 54ba38fb
......@@ -8,6 +8,10 @@ pylint = "*"
[packages]
django = "==2.1"
mysqlclient = "*"
social-auth-app-django = "*"
social-auth-core = {extras = ["saml"],version = "*"}
python3-saml = "*"
[requires]
python_version = "3.7"
{
"_meta": {
"hash": {
"sha256": "3468132bb0496a3a812649cc2c7b0c2f053736fb11b895e5c88f33dce9f1923b"
"sha256": "1016a34e3eff9effad91951bda6cc38484050da89650720ba18800ff9557daf0"
},
"pipfile-spec": 6,
"requires": {
......@@ -16,6 +16,28 @@
]
},
"default": {
"certifi": {
"hashes": [
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"defusedxml": {
"hashes": [
"sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4",
"sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20"
],
"markers": "python_version >= '3.0'",
"version": "==0.5.0"
},
"django": {
"hashes": [
"sha256:7f246078d5a546f63c28fc03ce71f4d7a23677ce42109219c24c9ffb28416137",
......@@ -24,12 +46,159 @@
"index": "pypi",
"version": "==2.1"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"isodate": {
"hashes": [
"sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8",
"sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"
],
"version": "==0.6.0"
},
"lxml": {
"hashes": [
"sha256:0537eee4902e8bf4f41bfee8133f7edf96533dd175930a12086d6a40d62376b2",
"sha256:0562ec748abd230ab87d73384e08fa784f9b9cee89e28696087d2d22c052cc27",
"sha256:09e91831e749fbf0f24608694e4573be0ef51430229450c39c83176cc2e2d353",
"sha256:1ae4c0722fc70c0d4fba43ae33c2885f705e96dce1db41f75ae14a2d2749b428",
"sha256:1c630c083d782cbaf1f7f37f6cac87bda9cff643cf2803a5f180f30d97955cef",
"sha256:2fe74e3836bd8c0fa7467ffae05545233c7f37de1eb765cacfda15ad20c6574a",
"sha256:37af783c2667ead34a811037bda56a0b142ac8438f7ed29ae93f82ddb812fbd6",
"sha256:3f2d9eafbb0b24a33f56acd16f39fc935756524dcb3172892721c54713964c70",
"sha256:47d8365a8ef14097aa4c65730689be51851b4ade677285a3b2daa03b37893e26",
"sha256:510e904079bc56ea784677348e151e1156040dbfb736f1d8ea4b9e6d0ab2d9f4",
"sha256:58d0851da422bba31c7f652a7e9335313cf94a641aa6d73b8f3c67602f75b593",
"sha256:7940d5c2185ffb989203dacbb28e6ae88b4f1bb25d04e17f94b0edd82232bcbd",
"sha256:7cf39bb3a905579836f7a8f3a45320d9eb22f16ab0c1e112efb940ced4d057a5",
"sha256:9563a23c1456c0ab550c087833bc13fcc61013a66c6420921d5b70550ea312bf",
"sha256:95b392952935947e0786a90b75cc33388549dcb19af716b525dae65b186138fc",
"sha256:983129f3fd3cef5c3cf067adcca56e30a169656c00fcc6c648629dbb850b27fa",
"sha256:a0b75b1f1854771844c647c464533def3e0a899dd094a85d1d4ed72ecaaee93d",
"sha256:b5db89cc0ef624f3a81214b7961a99f443b8c91e88188376b6b322fd10d5b118",
"sha256:c0a7751ba1a4bfbe7831920d98cee3ce748007eab8dfda74593d44079568219a",
"sha256:c0c5a7d4aafcc30c9b6d8613a362567e32e5f5b708dc41bc3a81dac56f8af8bb",
"sha256:d4d63d85eacc6cb37b459b16061e1f100d154bee89dc8d8f9a6128a5a538e92e",
"sha256:da5e7e941d6e71c9c9a717c93725cda0708c2474f532e3680ac5e39ec57d224d",
"sha256:dccad2b3c583f036f43f80ac99ee212c2fa9a45151358d55f13004d095e683b2",
"sha256:df46307d39f2aeaafa1d25309b8a8d11738b73e9861f72d4d0a092528f498baa",
"sha256:e70b5e1cb48828ddd2818f99b1662cb9226dc6f57d07fc75485405c77da17436",
"sha256:ea825562b8cd057cbc9810d496b8b5dec37a1e2fc7b27bc7c1e72ce94462a09a"
],
"version": "==4.3.1"
},
"mysqlclient": {
"hashes": [
"sha256:425e733b05e359a714d6007c0fc44582be66b63e5a3df0a50949274ae16f4bc6",
"sha256:62e4770b6a797b9416bcf70488365b7d6b9c9066878108499c559293bb464380",
"sha256:f257d250f2675d0ef99bd318906f3cfc05cef4a2f385ea695ff32a3f04b9f9a7"
],
"index": "pypi",
"version": "==1.4.2.post1"
},
"oauthlib": {
"hashes": [
"sha256:0ce32c5d989a1827e3f1148f98b9085ed2370fc939bf524c9c851d8714797298",
"sha256:3e1e14f6cde7e5475128d30e97edc3bfb4dc857cb884d8714ec161fdbb3b358e"
],
"version": "==3.0.1"
},
"pkgconfig": {
"hashes": [
"sha256:048c3b457da7b6f686b647ab10bf09e2250e4c50acfe6f215398a8b5e6fcdb52",
"sha256:3eb03a6345d4916489d3433f60e6d044a21f50e1d86fb611a52fd28061582065"
],
"version": "==1.4.0"
},
"pyjwt": {
"hashes": [
"sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e",
"sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"
],
"version": "==1.7.1"
},
"python3-openid": {
"hashes": [
"sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa",
"sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502"
],
"markers": "python_version >= '3.0'",
"version": "==3.1.0"
},
"python3-saml": {
"hashes": [
"sha256:199ef88a296b5eb72cf18a4070c7ee566e15588efbf8d16710225226d725c447",
"sha256:75d5d17a75f23ab4e940d1ca557be125e3138749a552d089bccf2cace2ff403d",
"sha256:d4f92c0242511315c5260dd43ee6f4920ab16e78e0841f15cbcabb5ec5ff18d3"
],
"index": "pypi",
"version": "==1.5.0"
},
"pytz": {
"hashes": [
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.9"
},
"requests": {
"hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"version": "==2.21.0"
},
"requests-oauthlib": {
"hashes": [
"sha256:bd6533330e8748e94bf0b214775fed487d309b8b8fe823dc45641ebcd9a32f57",
"sha256:d3ed0c8f2e3bbc6b344fa63d6f933745ab394469da38db16bdddb461c7e25140"
],
"version": "==1.2.0"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"social-auth-app-django": {
"hashes": [
"sha256:6d0dd18c2d9e71ca545097d57b44d26f59e624a12833078e8e52f91baf849778",
"sha256:9237e3d7b6f6f59494c3b02e0cce6efc69c9d33ad9d1a064e3b2318bcbe89ae3",
"sha256:f151396e5b16e2eee12cd2e211004257826ece24fc4ae97a147df386c1cd7082"
],
"index": "pypi",
"version": "==3.1.0"
},
"social-auth-core": {
"extras": [
"saml"
],
"hashes": [
"sha256:65122fb4287c70ff7915be0f52150fc1a9b9515eab3c3f0e4cd9dbb2a442a5c3",
"sha256:cc871fb4528f7cbba67efdba0bc0f7d7c6eeb92113b0cdc9368dd91ffe965782",
"sha256:f9f36dfa6af2823efb35a5ef65dfd02f66c944f389c33c25dd9621f8bb75a7da"
],
"index": "pypi",
"version": "==3.1.0"
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24.1"
},
"xmlsec": {
"hashes": [
"sha256:e573c0172174973223d874ffd158ecd4e0faa761015474385289a6468dd29ed6"
],
"version": "==1.3.3"
}
},
"develop": {
......@@ -42,11 +211,9 @@
},
"isort": {
"hashes": [
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
"sha256:f19b23b22fb5a919a081bc31aabcc0991614c244d9215267e11abf2ca7b684ce"
],
"version": "==4.3.4"
"version": "==4.3.9"
},
"lazy-object-proxy": {
"hashes": [
......
......@@ -13,7 +13,15 @@ Requisitos
* Python 3.7 o superior (el script `compile_python.sh` lo instala en Debian o Ubuntu).
* [pip](https://pip.pypa.io/en/stable/installing/) (puede venir con la instalación de Python).
* [pipenv](https://github.com/pypa/pipenv) (se puede instalar con `sudo -H pip3.7 install pipenv`).
* Paquetes libxmlsec1-dev pkg-config
* Un SGBD aceptado por Django (vg PostgreSQL o MariaDB).
La configuración MariaDB/MySQL deberá incluir:
```
innodb_file_per_table
innodb_file_format = Barracuda
innodb_large_prefix
innodb_default_row_format = dynamic
```
Instalación
......@@ -25,15 +33,23 @@ pipenv --python 3.7 install --dev
```
Ejecución
---------
Configuración inicial
---------------------
Configurar la base de datos en la sección `DATABASES` de `manhattan_project/settings.py`.
Configurar los datos para el _Single Sign On_ (SAML).
```shell
pipenv shell
./manage.py runserver
./manage.py migrate
./manage.py createsuperuser
```
Configuración
-------------
Ejecución
---------
```shell
pipenv shell
./manage.py runserver
```
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = [
"username",
"email",
"first_name",
"last_name",
] # Campos que se muestran en el listado
admin.site.register(CustomUser, CustomUserAdmin)
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = "accounts"
from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm):
model = CustomUser
fields = UserCreationForm.Meta.fields
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = UserChangeForm.Meta.fields
# Generated by Django 2.1 on 2019-02-26 12:15
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0009_alter_user_last_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser(AbstractUser):
# add additional fields in here
def __str__(self):
return self.username
from django.test import TestCase
# Create your tests here.
from django.contrib.auth import views as auth_views
from django.urls import path
from . import views
urlpatterns = [
# Para evitar que un usuario ya autenticado pueda volver a la página de inicio de sesión
path(
"login/",
auth_views.LoginView.as_view(redirect_authenticated_user=True),
name="login",
),
path("metadata", views.metadataView, name="metadata"),
]
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse
from social_django.utils import load_strategy, load_backend
# Create your views here.
def metadataView(request):
complete_url = reverse("social:complete", args=("saml",))
saml_backend = load_backend(
load_strategy(request), "saml", redirect_uri=complete_url
)
metadata, errors = saml_backend.generate_metadata_xml()
if not errors:
return HttpResponse(content=metadata, content_type="text/xml")
import urllib.parse
from django import template
from django.urls import reverse
register = template.Library()
@register.simple_tag
def lord_url():
return "{base}?{params}".format(
base=reverse("social:begin", kwargs={"backend": "saml"}),
params=urllib.parse.urlencode({"next": "/", "idp": "lord"}),
)
......@@ -25,7 +25,7 @@ SECRET_KEY = "xk6ujnt_zj7xlnt@c&$jc9f_=u3io5e!87imbqz4)=li*$tu%w"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = [] # ['*']
# Application definition
......@@ -39,6 +39,9 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
# Local
"indo.apps.IndoConfig",
"accounts.apps.AccountsConfig",
# 3rd Party
"social_django", # https://github.com/python-social-auth/social-app-django
]
MIDDLEWARE = [
......@@ -64,6 +67,8 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
]
},
}
......@@ -77,8 +82,16 @@ WSGI_APPLICATION = "manhattan_project.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
"ENGINE": "django.db.backends.mysql", # Database engine
"NAME": "manhattan", # Database name
"USER": "albert", # Database user
"PASSWORD": "einstein", # Database password
"HOST": "", # Set to empty string for localhost.
"PORT": "", # Set to empty string for default.
# Additional database options
"OPTIONS": {
'charset': 'utf8mb4', # Requires `innodb_default_row_format = dynamic`
}
}
}
......@@ -115,3 +128,54 @@ USE_TZ = True
STATIC_URL = "/static/"
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
AUTH_USER_MODEL = "accounts.CustomUser"
LOGIN_REDIRECT_URL = "home" # TODO
LOGOUT_REDIRECT_URL = "home"
### SAML with Python Social Auth ###
# https://python-social-auth.readthedocs.io/en/latest/backends/saml.html
AUTHENTICATION_BACKENDS = (
"social_core.backends.saml.SAMLAuth",
"django.contrib.auth.backends.ModelBackend",
)
# When using PostgreSQL, it’s recommended to use the built-in JSONB field to store the extracted extra_data.
# To enable it define the setting:
# SOCIAL_AUTH_POSTGRES_JSONFIELD = True
SOCIAL_AUTH_SAML_SP_ENTITY_ID = (
"https://manhattan.local/accounts/metadata" # Identifier of the SP entity (must be a URI)
)
SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = """Spam, ham and eggs"""
SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = """Spam, sausages and bacon"""
SOCIAL_AUTH_SAML_ORG_INFO = {
"en-US": {
"name": "manhattan",
"displayname": "Proyectos de Innovación Docente",
"url": "http://manhattan.local",
}
}
SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {
"givenName": "Quique",
"emailAddress": "quique@manhattan.local",
}
SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {
"givenName": "Vicerrectorado de Política Académica",
"emailAddress": "innova.docen@manhattan.local",
}
SOCIAL_AUTH_SAML_ENABLED_IDPS = {
"lord": {
"entity_id": 'https://FIXME.idp.com/saml2/idp/metadata.php',
"url": 'https://FIXME.idp.com/saml2/idp/SSOService.php',
"x509cert": 'Lovely spam, wonderful spam',
"attr_user_permanent_id": "uid",
'attr_full_name': "cn", # "urn:oid:2.5.4.3"
"attr_first_name": "givenName", # "urn:oid:2.5.4.42"
"attr_last_name": "sn", # "urn:oid:2.5.4.4"
"attr_username": "uid", # "urn:oid:0.9.2342.19200300.100.1.1"
# "attr_email": "email", # "urn:oid:0.9.2342.19200300.100.1.3"
}
}
SOCIAL_AUTH_URL_NAMESPACE = "social"
......@@ -21,6 +21,8 @@ from django.views.generic.base import RedirectView
urlpatterns = [
path("admin/", admin.site.urls),
path("accounts/", include("accounts.urls")),
path("accounts/", include("django.contrib.auth.urls")),
path(
"browserconfig.xml",
RedirectView.as_view(url=staticfiles_storage.url("favicons/browserconfig.xml")),
......@@ -36,5 +38,6 @@ urlpatterns = [
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
name="robots_file",
),
path("", include("social_django.urls", namespace="social")),
path("", include("indo.urls")),
]
{% load static %}
{% load static %}{% load custom_tags %}
<!doctype html>
<html lang="es">
......@@ -51,7 +51,7 @@
<li class="nav-item"><a class='nav-link' href="{% url 'ayuda' %}"><i class="fas fa-question-circle"></i>
&nbsp;Ayuda</a>
</li>
<!-- li class="nav-item">
<li class="nav-item">
{% if user.is_authenticated %}
<a class="nav-link dropdown-toggle" href="#" id="userMenu" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
......@@ -61,12 +61,13 @@
<a class="dropdown-item" href="{% url 'password_change' %}"><i class="fas fa-key"></i> Cambiar
contraseña</a>
<div class="dropdown-divider"></div>
{# Follow https://github.com/python-social-auth/social-core/issues/199 #}
<a class="dropdown-item" href="{% url 'logout' %}"><i class="fas fa-sign-out-alt"></i> Cerrar sesión</a>
</div>
{% else %}
<a class='nav-link' href="{ % url 'login' %}"><i class="fas fa-sign-in-alt"></i> &nbsp;Iniciar sesión</a>
<a class='nav-link' href={% lord_url %}><i class="fas fa-sign-in-alt"></i> &nbsp;Iniciar sesión</a>
{% endif %}
</li -->
</li>
</ul>
</div>
</nav>
......
{% extends 'base.html' %}
{% load static %}
{% block title %}Inicio de sesión{% endblock title %}
{% block description %}Inicio de sesión local{% endblock description %}
{% block extracss %}
<link rel="stylesheet" href="{% static 'css/signin.css' %}">
{% endblock extracss %}
{% block content %}
<div class="jumbotron">
<a href="{% url 'home' %}"><i class="fas fa-project-diagram" style="font-size: 400%;"></i></a><br><br>
<form class="form-signin" method='post'>
{% csrf_token %}
<label for="id_username" class="sr-only">Usuario</label>
<input type="text" id="id_username" name='username' class="form-control" placeholder="Usuario" required autofocus>
<label for="id_password" class="sr-only">Contraseña</label>
<input type="password" id="id_password" name='password' class="form-control" placeholder="Contraseña" required>
<button class="btn btn-lg btn-primary btn-block" type="submit">Iniciar sesión</button>
<!-- p class="mt-5 mb-3 text-muted">TODO: He olvidado mi usuario o contraseña</p -->
</form>
</div>
{% endblock content %}
\ No newline at end of file
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