Implémentation des backends pr@tic d'authentification

- X509 utilisateur
- X509 collectivité + login/password
- collectivité/login/mot de passe

La page d'accueil du manager a été adapté:
- pour un super-administrateur on montre tout
- pour un administrateur de collectivité on montre directement des liens
	profonds vers la gestion des utilisateurs, des instances de service et
	des accès pour sa collectivité.
This commit is contained in:
Benjamin Dauvergne 2014-11-18 17:51:00 +01:00
parent 81813ab91a
commit 8a4205b4bc
16 changed files with 667 additions and 164 deletions

View File

@ -13,3 +13,6 @@ class Plugin(object):
def get_auth_frontends(self):
return ['authentic2_pratic.auth_frontends.PraticFrontend']
def get_after_middleware(self):
return ('authentic2_pratic.middleware.PraticAuthMiddleware',)

View File

@ -1,5 +1,12 @@
class AppSettings(object):
__DEFAULTS = {
'X509_KEYS': {
'subject_dn': 'SSL_CLIENT_S_DN',
'issuer_dn': 'SSL_CLIENT_I_DN',
'serial': ('SSL_CLIENT_M_SERIAL', 'SSL_CLIENT_SERIAL'),
'cert': 'SSL_CLIENT_CERT',
'verify': 'SSL_CLIENT_VERIFY',
}
}
def __init__(self, prefix):

View File

@ -1,21 +1,68 @@
from django.utils.translation import gettext_noop
from django import forms
from django.http import HttpResponseRedirect
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
from django.shortcuts import render
from django.conf import settings
from authentic2.constants import NONCE_FIELD_NAME
from authentic2.models import AuthenticationEvent
from . import constants, forms, models
class PraticFrontend(object):
def enabled(self):
True
def name(self):
return gettext_noop('Pr@tic')
def id(self):
return 'pratic'
def form(self):
return forms.Form
def name(self):
return gettext_noop('Pr@tic')
def post(self, request, form, nonce, next):
raise NotImplemented
def template(self):
return 'authentic2_pratic/login.html'
def login(self, request, *args, **kwargs):
next_url = request.GET.get(REDIRECT_FIELD_NAME) or settings.LOGIN_REDIRECT_URL
nonce = request.GET.get(NONCE_FIELD_NAME,'')
# if there is only one ssl user, autolog him
# only autologin every 10 minutes
user = None
how = None
autologin = False
if constants.PRATIC_AUTOLOGIN_COOKIE_NAME not in request.COOKIES:
if hasattr(request, 'ssl_info') and request.ssl_info and \
len(request.ssl_info.users) == 1:
user = authenticate(ssl_user=request.ssl_info.users[0])
how = 'ssl'
autologin = True
# login SSL users by selection
if not user and request.method == 'POST' and 'ssl-user' in request.POST \
and hasattr(request, 'ssl_info') and request.ssl_info:
try:
ssl_user_id = int(request.POST['ssl-user'])
except ValueError:
pass
else:
try:
ssl_user = request.ssl_info.users.get(pk=ssl_user_id)
except models.User.DoesNotExist:
pass
else:
user = authenticate(ssl_user=ssl_user)
how = 'ssl'
# now try to login using collectivity/login/password
form = forms.AuthenticationForm(request=request, data=request.POST or None)
if not user and request.method == 'POST' and form.is_valid():
how = form.get_how()
if how == 'password' and request.is_secure():
how += '-on-https'
user = form.get_user()
if user and how:
login(request, user)
AuthenticationEvent.objects.create(who=user.username,
how=how, nonce=nonce)
response = HttpResponseRedirect(next_url)
if autologin:
response.set_cookie(constants.PRATIC_AUTOLOGIN_COOKIE_NAME,
max_age=constants.PRATIC_AUTOLOGIN_COOKIE_MAX_AGE)
return response
return render(request, 'authentic2_pratic/login.html',
{'form': form})

View File

@ -1,8 +1,54 @@
import logging
class PraticBackend(object):
def __init__(self):
self.logger = logging.getLogger(__name__)
from django.contrib.auth.backends import ModelBackend
def authenticate(self, **kwargs):
raise NotImplemented
from . import models, constants
class BaseBackend(ModelBackend):
def __init__(self, *args, **kwargs):
self.logger = logging.getLogger(__name__)
super(BaseBackend, self).__init__(*args, **kwargs)
def get_user(self, user_id):
try:
return models.User._default_manager.get(pk=user_id)
except models.User.DoesNotExist:
return None
class PraticLoginPasswordBackend(BaseBackend):
def authenticate(self, collectivity, username, password, **kwargs):
try:
user = models.User.objects.select_related().get(collectivity=collectivity, uid=username)
except models.User.DoesNotExist:
pass
else:
if user.check_password(password):
return user
def get_saml2_authn_context(self, **kwargs):
import lasso
request = kwargs.pop('request', None)
if request and request.is_secure():
return lasso.SAML2_AUTHN_CONTEXT_PASSWORD_PROTECTED_TRANSPORT
else:
return lasso.SAML2_AUTHN_CONTEXT_PASSWORD
class PraticSSLBackend(BaseBackend):
def authenticate(self, ssl_user, **kwargs):
return ssl_user
def get_saml2_authn_context(self, **kwargs):
import lasso
return lasso.SAML2_AUTHN_CONTEXT_X509
class PraticLoginPasswordSSLBackend(PraticLoginPasswordBackend):
def authenticate(self, collectivity, username, password, ssl_info):
user = super(PraticLoginPasswordSSLBackend, self).authenticate(collectivity, username, password)
if user:
col = user.collectivity
if col.certificate_issuer_dn == ssl_info.issuer_dn and \
col.certificate_subject_dn == ssl_info.subject_dn:
return user
def get_saml2_authn_context(self, **kwargs):
return constants.PRATIC_AUTHN_CONTEXT_SSL_COLLECTIVITY

View File

@ -0,0 +1,4 @@
PRATIC_AUTOLOGIN_COOKIE_NAME = 'pratic-autologin'
PRATIC_AUTOLOGIN_COOKIE_MAX_AGE = 60*10 # 10 minutes
PRATIC_AUTHN_CONTEXT_SSL_COLLECTIVITY = 'urn:cdg59.fr:names:tc:SAML:2.0:ac:classes:X509Collectivity'

View File

@ -1,4 +1,11 @@
import re
import tempfile
import subprocess
from django import forms
from django.utils.translation import ugettext_lazy as _, ugettext
from django.contrib.auth import authenticate
from . import models
@ -10,7 +17,29 @@ class ServiceForm(BaseForm):
class Meta:
model = models.Service
class CollectivityForm(BaseForm):
class CertificateMixin(forms.ModelForm):
certificate = forms.FileField(label=_('X509 Certificate'), help_text=_('You can set '
'the issuer and subject DNs by uploading the certificate'), required=False)
def clean(self):
cleaned_data = super(CertificateMixin, self).clean()
certificate = cleaned_data.get('certificate')
if certificate is not None:
with tempfile.NamedTemporaryFile() as certificate_file:
certificate_file.write(certificate.read())
certificate_file.flush()
description = subprocess.check_output(['/usr/bin/openssl', 'x509', '-text', '-in', certificate_file.name])
match = re.findall(r'(?:Issuer|Subject): (.*)', description)
if len(match) == 2:
cleaned_data['certificate_issuer_dn'] = '/' + match[0].replace(', ', '/')
cleaned_data['certificate_subject_dn'] = '/' + match[1].replace(', ', '/')
if not cleaned_data.get('certificate_issuer_dn') or \
not cleaned_data.get('certificate_subject_dn'):
cleaned_data['certificate_issuer_dn'] = None
cleaned_data['certificate_subject_dn'] = None
return cleaned_data
class CollectivityForm(CertificateMixin, BaseForm):
class Meta:
model = models.Collectivity
@ -29,7 +58,7 @@ class AccessForm(BaseForm):
class Meta:
model = models.Access
class UserForm(BaseForm):
class UserForm(CertificateMixin, BaseForm):
class Meta:
model = models.User
fields = (
@ -43,6 +72,92 @@ class UserForm(BaseForm):
'postal_address',
'fax',
'mobile',
'phone'
'phone',
'certificate_issuer_dn',
'certificate_subject_dn',
'certificate',
)
class AuthenticationForm(forms.Form):
how = None
collectivity = forms.ModelChoiceField(queryset=models.Collectivity.objects,
label=_('Collectivity'))
username = forms.CharField(label=_('Username'))
password = forms.CharField(label=_('Password'), widget=forms.PasswordInput)
error_messages = {
'invalid_login': _("Please enter a correct username and password. "
"Note that both fields may be case-sensitive."),
'inactive': _("This account is inactive."),
}
def __init__(self, request=None, *args, **kwargs):
"""
The 'request' parameter is set for custom auth use by subclasses.
The form data comes in via the standard 'data' kwarg.
"""
self.request = request
self.user_cache = None
super(AuthenticationForm, self).__init__(*args, **kwargs)
if request and hasattr(request, 'ssl_info') and request.ssl_info \
and request.ssl_info.collectivity:
self.fields['collectivity'].initial = request.ssl_info.collectivity.pk
def clean(self):
collectivity = self.cleaned_data.get('collectivity')
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
if username and password and collectivity:
if self.request and hasattr(self.request, 'ssl_info') and self.request.ssl_info:
self.user_cache = authenticate(collectivity=collectivity,
username=username,
password=password,
ssl_info=self.request.ssl_info)
if self.user_cache:
self.how = 'ssl-collectivity'
else:
self.user_cache = authenticate(collectivity=collectivity,
username=username,
password=password)
if self.user_cache:
self.how = 'password'
if self.user_cache is None:
raise forms.ValidationError(
self.error_messages['invalid_login'],
code='invalid_login',
params={'username': ugettext('username')},
)
else:
self.confirm_login_allowed(self.user_cache)
return self.cleaned_data
def confirm_login_allowed(self, user):
"""
Controls whether the given User may log in. This is a policy setting,
independent of end-user authentication. This default behavior is to
allow login by active users, and reject login by inactive users.
If the given user cannot log in, this method should raise a
``forms.ValidationError``.
If the given user may log in, this method should return None.
"""
if not user.is_active:
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',
)
def get_user_id(self):
if self.user_cache:
return self.user_cache.id
return None
def get_user(self):
return self.user_cache
def get_how(self):
return self.how

View File

@ -1,32 +1,70 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# Authentic2 Pr@tic French Locale
# Copyright (C) 2014 CDG59
# This file is distributed under the same license as the authentic2-pratic package.
# Benjamin Dauvergne <bdauvergne@entrouvert.com>, 2014.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: authentic2-pratic 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-10-30 17:20+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"POT-Creation-Date: 2014-11-19 14:17+0100\n"
"PO-Revision-Date: 2014-11-19 14:17+0100\n"
"Last-Translator: Benjamin Dauvergne <bdauvergne@entrouvert.com>\n"
"Language-Team: France <fr@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: auth_frontends.py:9 dashboard.py:8
#: auth_frontends.py:20 dashboard.py:8
msgid "Pr@tic"
msgstr ""
#: models.py:20 models.py:92 tables.py:36 tables.py:49
#: forms.py:21
msgid "X509 Certificate"
msgstr "Certificat X509 au format PEM"
#: forms.py:21
msgid "You can set the issuer and subject DNs by uploading the certificate"
msgstr ""
"Vous pouvez aussi définir les noms distingués du certificat en le "
"téléchargeant."
#: forms.py:85
msgid "Collectivity"
msgstr "Collectivité"
#: forms.py:86
msgid "Username"
msgstr "Identifiant"
#: forms.py:87
msgid "Password"
msgstr "Mot de passe"
#: forms.py:90
msgid ""
"Please enter a correct username and password. Note that both fields may be "
"case-sensitive."
msgstr ""
"Veuillez entrer un identifiant et un mot de passe corrects. Remarquez que "
"chacun de ces champs est sensible à la casse (différenciation des majuscules/"
"minuscules)."
#: forms.py:92
msgid "This account is inactive."
msgstr "Ce compte est inactif"
#: forms.py:130
msgid "username"
msgstr "identifiant"
#: models.py:20 models.py:107 tables.py:37 tables.py:50
msgid "identifier"
msgstr "identifiant"
#: models.py:24 models.py:212 models.py:279
#: models.py:24 models.py:243 models.py:312
msgid "collectivity"
msgstr "collectivité"
@ -34,7 +72,7 @@ msgstr "collectivité"
msgid "is admin"
msgstr "Administrateur?"
#: models.py:32 models.py:106 models.py:111
#: models.py:32 models.py:121 models.py:126
msgid "SIRH Code"
msgstr ""
@ -50,11 +88,11 @@ msgstr "durée de la dernière connexion"
msgid "employee type"
msgstr "type de poste"
#: models.py:53 models.py:129
#: models.py:53 models.py:144
msgid "postal address"
msgstr "Adresse postale"
#: models.py:57 models.py:189
#: models.py:57 models.py:204
msgid "fax"
msgstr ""
@ -62,223 +100,245 @@ msgstr ""
msgid "mobile"
msgstr ""
#: models.py:67 models.py:184
#: models.py:67 models.py:199
msgid "phone"
msgstr "téléphone"
#: models.py:72
#: models.py:71 models.py:218
msgid "certificate issuer DN"
msgstr "nom distingué de l'émetteur du certificat"
#: models.py:76 models.py:223
msgid "certificate subject DN"
msgstr "nom distingué du sujet du certificat"
#: models.py:85
msgid "agent"
msgstr ""
#: models.py:73
#: models.py:86
msgid "agents"
msgstr ""
#: models.py:88 models.py:221 tables.py:10 tables.py:23
#: models.py:103 models.py:254 tables.py:11 tables.py:24
msgid "name"
msgstr "nom"
#: models.py:96
#: models.py:111
msgid "is superuser"
msgstr "Tous les utilisateurs sont des super-administrateurs?"
#: models.py:101
#: models.py:116
msgid "collectivity id"
msgstr "identifiant de la collectivité"
#: models.py:116
#: models.py:131
msgid "INSEE Code"
msgstr "code INSEE"
#: models.py:121
#: models.py:136
msgid "SIRET Code"
msgstr "code SIRET"
#: models.py:133
#: models.py:148
msgid "street number"
msgstr "numéro dans la rue"
#: models.py:138
#: models.py:153
msgid "street"
msgstr "rue"
#: models.py:143
#: models.py:158
msgid "postal code"
msgstr "code postal"
#: models.py:148
#: models.py:163
msgid "complementary address"
msgstr "adresse complémentaire"
#: models.py:153
#: models.py:168
msgid "address mention"
msgstr "adresse (mention complémentaire)"
#: models.py:158
#: models.py:173
msgid "arrondissement code"
msgstr "code d'arrondissement"
#: models.py:163
#: models.py:178
msgid "canton code"
msgstr "numéro de canton"
#: models.py:168
#: models.py:183
msgid "departement code"
msgstr "numéro de département"
#: models.py:173 models.py:178
#: models.py:188 models.py:193
msgid "distribution office"
msgstr "centre de distribution du courrier"
#: models.py:194
#: models.py:209
msgid "email"
msgstr "courriel"
#: models.py:199 models.py:233 models.py:281
#: models.py:214 models.py:266 models.py:314
msgid "URL"
msgstr ""
#: models.py:213
#: models.py:244
msgid "collectivities"
msgstr "collectivités"
#: models.py:229
#: models.py:262
msgid "is global"
msgstr "service global?"
#: models.py:235 models.py:284
#: models.py:268 models.py:317
msgid "SAML Metadata URL"
msgstr "URL des métadonnées SAML"
#: models.py:238 models.py:287
#: models.py:271 models.py:320
msgid "OAuth2 URL"
msgstr "URL du point d'accès OAuth2"
#: models.py:241 models.py:290
#: models.py:274 models.py:323
msgid "OAuth2 Key"
msgstr "Clé OAuth2"
#: models.py:254 models.py:276
#: models.py:287 models.py:309
msgid "service"
msgstr ""
#: models.py:255
#: models.py:288
msgid "services"
msgstr ""
#: models.py:308
#: models.py:341
msgid "There can be only one instance of a global service by collectivity"
msgstr ""
"Il ne peut y avoir qu'une seule instance d'un service global par collectivité"
#: models.py:315
#: models.py:348
msgid "Service URL field is required"
msgstr "L'URL de service est requise pour un service non global"
#: models.py:318 models.py:339
#: models.py:351 models.py:372
msgid "service instance"
msgstr "instance de service"
#: models.py:319
#: models.py:352
msgid "service instances"
msgstr "instances de service"
#: models.py:337
#: models.py:370
msgid "user"
msgstr "agent"
#: models.py:347
#: models.py:380
msgid "access"
msgstr "accréditation"
#: models.py:348
#: models.py:381
msgid "accesses"
msgstr "accréditations"
#: pratic_attribute_source.py:28
msgid "User domain"
msgstr ""
msgstr "Domaine de l'utilisateur"
#: pratic_attribute_source.py:29
msgid "User identifier"
msgstr ""
msgstr "Identifiant utilisateur"
#: tables.py:13 tables.py:26 tables.py:39 tables.py:52
msgid "delete"
msgstr "supprimer"
#: tables.py:61
#: tables.py:62
msgid "Last name"
msgstr "Nom"
#: tables.py:63
#: tables.py:64
msgid "First name"
msgstr "Prénom"
#: views.py:28 templates/authentic2_pratic/delete.html:27
msgid "Delete"
msgstr "Supprimer"
#: views.py:37
msgid "You are not a super-administrator or a collectivity administrator"
msgstr ""
"Vous n'êtes pas un super-administrateur ou un administrateur de collectivité."
#: views.py:29
#, python-format
msgid "Do you really want to delete \"%s\" ?"
msgstr "Êtes-vous sûr de vouloir supprimer \"%s\" ?"
#: views.py:48
msgid "You are not a super-administrator"
msgstr "Vous n'êtes pas un super-administrateur."
#: views.py:63 templates/authentic2_pratic/services.html:10
#: views.py:65 templates/authentic2_pratic/services.html:10
msgid "Add service"
msgstr "Ajouter un service"
#: views.py:65 views.py:91 views.py:141 views.py:168 views.py:207
#: views.py:67 views.py:93 views.py:152 views.py:186 views.py:225
msgid "Add"
msgstr "Ajouter"
#: views.py:70
#: views.py:72
msgid "Edit service"
msgstr "Éditer un service"
#: views.py:77
#: views.py:79
msgid "Delete service"
msgstr "Supprimer un service"
#: views.py:89 templates/authentic2_pratic/collectivities.html:10
#: views.py:91 templates/authentic2_pratic/collectivities.html:10
msgid "Add collectivity"
msgstr "Ajouter une collectivité"
#: views.py:96
#: views.py:98
msgid "Edit collectivity"
msgstr "Éditer une collectivité"
#: views.py:104
#: views.py:106
msgid "Delete collectivity"
msgstr "Supprimer une collectivité"
#: views.py:139 templates/authentic2_pratic/users.html:10
#: views.py:121
#, python-format
msgid "You are not a super-administrator or an administrator of %s"
msgstr "Vous n'êtes pas un super-administrateur ou un administrateur de %s"
#: views.py:150 templates/authentic2_pratic/users.html:10
msgid "Add agent"
msgstr "Ajouter un agent"
#: views.py:146
#: views.py:157
msgid "Edit agent"
msgstr "Éditer un agent"
#: views.py:154
#: views.py:162
msgid "Reset password"
msgstr "Ré-initialiser le mot de passe"
#: views.py:164
msgid "Deactivate"
msgstr "Désactiver"
#: views.py:166
msgid "Activate"
msgstr "Activer"
#: views.py:172
msgid "Delete agent"
msgstr "Supprimer un agent"
#: views.py:166 views.py:205
#: views.py:184 views.py:223
msgid "Add service instance"
msgstr "Ajouter une instance de service"
#: views.py:174 views.py:213
#: views.py:192 views.py:231
msgid "Edit service instance"
msgstr "Éditer une instance de service"
#: views.py:182 views.py:221
#: views.py:200 views.py:239
msgid "Delete service instance"
msgstr "Supprimer une instance de service"
#: templates/authentic2_pratic/accesses.html:4
#: templates/authentic2_pratic/accesses.html:6
#: templates/authentic2_pratic/collectivity_edit.html:14
#: templates/authentic2_pratic/homepage.html:29
msgid "Accesses management"
msgstr "Gestion des accréditations"
@ -317,12 +377,14 @@ msgstr[0] "%(count)s collectivité"
msgstr[1] "%(count)s collectivités"
#: templates/authentic2_pratic/collectivity_edit.html:12
#: templates/authentic2_pratic/homepage.html:27
#: templates/authentic2_pratic/users.html:4
#: templates/authentic2_pratic/users.html:6
msgid "Agents management"
msgstr "Gestion des agents"
#: templates/authentic2_pratic/collectivity_edit.html:13
#: templates/authentic2_pratic/homepage.html:28
#: templates/authentic2_pratic/service_instances.html:4
#: templates/authentic2_pratic/service_instances.html:6
msgid "Service instances management"
@ -341,6 +403,7 @@ msgstr "Sauvegarder"
#: templates/authentic2_pratic/collectivity_edit.html:49
#: templates/authentic2_pratic/delete.html:28
#: templates/authentic2_pratic/form.html:27
#: templates/authentic2_pratic/login.html:50
msgid "Cancel"
msgstr "Annuler"
@ -349,6 +412,10 @@ msgstr "Annuler"
msgid "Do you really want to delete « %(object)s » ?"
msgstr "Êtez-vous sûr de vouloir supprimer « %(object)s » ?"
#: templates/authentic2_pratic/delete.html:27
msgid "Delete"
msgstr "Supprimer"
#: templates/authentic2_pratic/homepage.html:8
msgid "Welcome"
msgstr "Bievenue"
@ -357,14 +424,44 @@ msgstr "Bievenue"
msgid "Password change"
msgstr "Changer de mot de passe"
#: templates/authentic2_pratic/homepage.html:19
#: templates/authentic2_pratic/homepage.html:21
msgid "Collectivities"
msgstr "Collectivités"
#: templates/authentic2_pratic/homepage.html:20
#: templates/authentic2_pratic/homepage.html:22
msgid "Services"
msgstr ""
#: templates/authentic2_pratic/homepage.html:25
#, python-format
msgid "Management of %(collectivity)s"
msgstr "Gestion de %(collectivity)s"
#: templates/authentic2_pratic/login.html:20
msgid "You are authenticated with certificate of users :"
msgstr "Vous êtez authentifié avec le certificat des utilisateurs&nbsp;:"
#: templates/authentic2_pratic/login.html:26
#, python-format
msgid "<em>%(user)s</em> from <em>%(collectivity)s</em>"
msgstr "<em>%(user)s</em> de <em>%(collectivity)s</em>"
#: templates/authentic2_pratic/login.html:29
#: templates/authentic2_pratic/login.html:49
msgid "Log in"
msgstr "Connexion"
#: templates/authentic2_pratic/login.html:40
#, python-format
msgid ""
"You are\n"
" authenticated with certificate of collectivity <em>%(collectivity)s.</"
"em> It has\n"
" been pre-selected for you."
msgstr ""
"Vous êtez authentifié avec le certificat de la collectivité <em>"
"%(collectivity)s</em>. Cette collectivité a été pré-selectionné pour vous."
#: templates/authentic2_pratic/service_instances.html:10
msgid "Add service-instance"
msgstr "Ajouter une instance de service"
@ -394,9 +491,3 @@ msgid "%(count)s agent"
msgid_plural "%(count)s agents"
msgstr[0] "%(count)s agent"
msgstr[1] "%(count)s agents"
#~ msgid "Add user"
#~ msgstr "Ajouter un agent"
#~ msgid "Edit user"
#~ msgstr "Éditer un agent"

View File

@ -0,0 +1,9 @@
from . import utils
class PraticAuthMiddleware(object):
"""
attempts to find a valid user based on the client certificate info
"""
def process_request(self, request):
if request.user and not request.user.is_authenticated():
request.ssl_info = utils.SSLInfo(request)

View File

@ -68,12 +68,12 @@ class User(AuthUser):
max_length=32,
blank=True)
certificate_issuer_dn = CharField(
verbose_name=_('Certificate Issuer DN'),
verbose_name=_('certificate issuer DN'),
max_length=256,
blank=True,
null=True)
certificate_subject_dn = CharField(
verbose_name=_('Certificate Subject DN'),
verbose_name=_('certificate subject DN'),
max_length=256,
blank=True,
null=True)
@ -91,6 +91,8 @@ class User(AuthUser):
# prevent collisions between users from multiple collectivities
if self.uid and not self.username and self.collectivity:
self.username = u'%s@%s' % (self.uid, self.collectivity.slug)
if self.collectivity:
self.is_superuser = self.collectivity.is_superuser
super(User, self).clean()
# Fields to support
@ -213,12 +215,12 @@ class Collectivity(Model):
max_length=128,
blank=True)
certificate_issuer_dn = CharField(
verbose_name=_('Certificate Issuer DN'),
verbose_name=_('certificate issuer DN'),
max_length=256,
blank=True,
null=True)
certificate_subject_dn = CharField(
verbose_name=_('Certificate Subject DN'),
verbose_name=_('certificate subject DN'),
max_length=256,
blank=True,
null=True)
@ -231,6 +233,12 @@ class Collectivity(Model):
def __unicode__(self):
return self.name
def save(self, *args, **kwargs):
# set is_superuser on users
response = super(Collectivity, self).save(*args, **kwargs)
User.objects.filter(collectivity=self).update(is_superuser=self.is_superuser)
return response
class Meta:
verbose_name = _('collectivity')
verbose_name_plural = _('collectivities')
@ -337,8 +345,6 @@ class ServiceInstance(Model):
self.oauth2_url = self.service.oauth2_url
self.oauth2_key = self.oauth2_key
if not self.service.is_global and not self.service_url:
import pdb
pdb.set_trace()
raise ValidationError(_('Service URL field is required'))
class Meta:

View File

@ -31,7 +31,7 @@
</form>
</div>
{% endif %}
<form method="post">
<form method="post" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
<div class="form-inner-container">
{% if messages %}
<ul class="messages">

View File

@ -2,7 +2,12 @@
{% load i18n staticfiles django_tables2 %}
{% block page_title %}
<a href="{% url "a2-pratic-collectivity-edit" collectivity_pk=collectivity.pk %}">
{{ collectivity }}
</a>
{% comment %}
Only show link for super-admins, collectivity's admins just go back to the homepag
{% endcomment %}
{% if user.is_superuser %}
<a href="{% url "a2-pratic-collectivity-edit" collectivity_pk=collectivity.pk %}">
{{ collectivity }}
</a>
{% endif %}
{% endblock %}

View File

@ -9,7 +9,7 @@
{% if title %}
<div id="appbar"><h2>{{ title }}</h2></div>
{% endif %}
<form method="post">
<form method="post" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %}>
<div class="form-inner-container">
{% if messages %}
<ul class="messages">

View File

@ -15,10 +15,20 @@
(<a href="{% url "auth_password_change" %}">{% trans "Password change" %}</a>)
</div>
<ul class="apps">
<li id="collectivities"><a href="{% url "a2-pratic-collectivities" %}">{% trans "Collectivities" %}</a></li>
<li id="services"><a href="{% url "a2-pratic-services" %}">{% trans "Services" %}</a></li>
</ul>
<br style="clear: both;"/>
{% if user.is_superuser %}
<ul class="apps">
<li id="pratic-collectivities"><a href="{% url "a2-pratic-collectivities" %}">{% trans "Collectivities" %}</a></li>
<li id="pratic-services"><a href="{% url "a2-pratic-services" %}">{% trans "Services" %}</a></li>
</ul>
{% elif user.is_admin %}
<h3>{% blocktrans with collectivity=user.collectivity %}Management of {{ collectivity }}{% endblocktrans %}</h3>
<ul class="apps">
<li id="pratic-collectivity-users"><a href="{% url "a2-pratic-users" collectivity_pk=user.collectivity.pk %}">{% trans "Agents management" %}</a></li>
<li id="pratic-collectivity-service-instances"><a href="{% url "a2-pratic-service-instances" collectivity_pk=user.collectivity.pk %}">{% trans "Service instances management" %}</a></li>
<li id="pratic-collectivity-accesses"><a href="{% url "a2-pratic-accesses" collectivity_pk=user.collectivity.pk %}">{% trans "Accesses management" %}</a></li>
</ul>
{% endif %}
<br style="clear: both;"/>
</div>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% load i18n %}
<!--
issuer dn: {{ request.ssl_info.issuer_dn }}
subject dn: {{ request.ssl_info.subject_dn }}
-->
<div id="pratic-login">
{% if request.ssl_info and request.ssl_info.users %}
<style>
.pratic-ssl-user-auth, .pratic-ssl-collectivity-auth {
background: lightblue;
color: black;
border: 1px solid black;
padding: 10px;
}
.pratic-ssl-user-auth-button {
float: right;
}
</style>
<div class="pratic-ssl-user-auth">{% trans "You are authenticated with certificate of users :" %}
<ul>
{% for user in request.ssl_info.users %}
<li>
<p>
<form method="post">
<span>{% blocktrans with collectivity=user.collectivity %}<em>{{user}}</em> from <em>{{collectivity}}</em>{% endblocktrans %}</span>
{% csrf_token %}
<input type="hidden" name="ssl-user" value="{{ user.pk }}"/>
<input type="submit" name="{{ submit_name }}" value="{% trans "Log in" %}" class="pratic-ssl-user-auth-button"/>
</form>
</p>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if request.ssl_info and request.ssl_info.collectivity %}
<p class="pratic-ssl-collectivity-auth">
{% blocktrans with collectivity=request.ssl_info.collectivity %}You are
authenticated with certificate of collectivity <em>{{collectivity}}.</em> It has
been pre-selected for you.{% endblocktrans %}
</p>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="pratic-login" value="{% trans "Log in" %}"/>
<input type="submit" name="pratic-cancel" value="{% trans "Cancel" %}"/>
</form>
</div>

View File

@ -0,0 +1,86 @@
import base64
from . import models, app_settings
def normalize_cert(certificate_pem):
'''Normalize content of the certificate'''
base64_content = ''.join(certificate_pem.splitlines()[1:-1])
content = base64.b64decode(base64_content)
return base64.b64encode(content)
def explode_dn(dn):
'''Extract sub element of a DN as displayed by mod_ssl or nginx_ssl'''
dn = dn.strip('/')
parts = dn.split('/')
parts = [part.split('=') for part in parts]
parts = [(part[0], part[1].decode('string_escape').decode('utf-8'))
for part in parts]
return parts
TRANSFORM = {
'cert': normalize_cert,
}
class SSLInfo(object):
"""
Encapsulates the SSL environment variables in a read-only object. It
attempts to find the ssl vars based on the type of request passed to the
constructor. Currently only WSGIRequest and ModPythonRequest are
supported.
"""
def __init__(self, request):
name = request.__class__.__name__
if name == 'WSGIRequest':
env = request.environ
elif name == 'ModPythonRequest':
env = request._req.subprocess_env
else:
raise EnvironmentError, 'The SSL authentication currently only \
works with mod_python or wsgi requests'
self.read_env(env);
if self.issuer_dn and self.subject_dn:
kwargs = dict(certificate_issuer_dn=self.issuer_dn,
certificate_subject_dn=self.subject_dn)
try:
self.__dict__['collectivity'] = models.Collectivity.objects.get(**kwargs)
except models.Collectivity.DoesNotExist:
self.__dict__['collectivity'] = None
self.__dict__['users'] = models.User.objects.filter(**kwargs).select_related()
else:
self.__dict__['collectivity'] = None
self.__dict__['users'] = models.User.objects.none()
def read_env(self, env):
for attr, keys in app_settings.X509_KEYS.iteritems():
if isinstance(keys, basestring):
keys = [keys]
for key in keys:
if key in env and env[key]:
v = env[key]
if attr in TRANSFORM:
v = TRANSFORM[attr](v)
self.__dict__[attr] = v
break
else:
self.__dict__[attr] = None
if self.__dict__['verify'] == 'SUCCESS':
self.__dict__['verify'] = True
else:
self.__dict__['verify'] = False
def get(self, attr):
return self.__getattr__(attr)
def __getattr__(self, attr):
if attr in self.__dict__:
return self.__dict__[attr]
else:
raise AttributeError, 'SSLInfo does not contain key %s' % attr
def __setattr__(self, attr, value):
raise AttributeError, 'SSL vars are read only!'
def __repr__(self):
return '<SSLInfo %s>' % self.__dict__

View File

@ -1,13 +1,11 @@
import logging
from django.contrib.auth.decorators import login_required, user_passes_test
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.db.models.query import Q
from django.views.generic import (TemplateView, FormView, UpdateView,
CreateView, DeleteView, View, ListView)
from django.views.generic import (TemplateView, UpdateView,
CreateView, DeleteView)
from django_tables2 import SingleTableView
from authentic2.manager.views import (AjaxFormViewMixin,
@ -15,13 +13,6 @@ from authentic2.manager.views import (AjaxFormViewMixin,
from . import models, tables, forms
def is_pratic_admin(user):
if user.is_superuser:
return True
return getattr(user, 'is_admin', False)
pratic_admin = user_passes_test(is_pratic_admin)
class SearchMixin(object):
search_filter = []
@ -36,51 +27,72 @@ class SearchMixin(object):
qs = qs.filter(filters)
return qs
class AdminMixin(object):
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return redirect_to_login(request.get_full_path())
class HomepageView(TemplateView):
if not user.is_superuser and (not hasattr(user, 'is_admin') or not user.is_admin):
messages.warning(request, _('You are not a super-administrator or a collectivity administrator'))
return redirect('auth_homepage')
return super(AdminMixin, self).dispatch(request, *args, **kwargs)
class SuperAdminMixin(object):
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return redirect_to_login(request.get_full_path())
if not user.is_superuser:
messages.warning(request, _('You are not a super-administrator'))
return redirect('auth_homepage')
return super(SuperAdminMixin, self).dispatch(request, *args, **kwargs)
class HomepageView(AdminMixin, TemplateView):
template_name = 'authentic2_pratic/homepage.html'
# Services
class ServicesView(SingleTableView):
class ServicesView(SuperAdminMixin, SingleTableView):
template_name = 'authentic2_pratic/services.html'
model = models.Service
table_class = tables.ServiceTable
class ServiceAddView(TitleMixin, ActionMixin, AjaxFormViewMixin, CreateView):
class ServiceAddView(SuperAdminMixin, TitleMixin, ActionMixin, AjaxFormViewMixin, CreateView):
model = models.Service
form_class = forms.ServiceForm
title = _('Add service')
template_name = 'authentic2_pratic/form.html'
action = _('Add')
class ServiceView(TitleMixin, OtherActionsMixin,
class ServiceView(SuperAdminMixin, TitleMixin, OtherActionsMixin,
AjaxFormViewMixin, UpdateView):
model = models.Service
title = _('Edit service')
template_name = 'authentic2_pratic/form.html'
form_class = forms.ServiceForm
class ServiceDeleteView(TitleMixin, AjaxFormViewMixin, DeleteView):
class ServiceDeleteView(SuperAdminMixin, TitleMixin, AjaxFormViewMixin, DeleteView):
model = models.Service
template_name = 'authentic2_pratic/delete.html'
title = _('Delete service')
success_url = 'a2-pratic-services'
# Collectivities
class CollectivitiesView(SearchMixin, SingleTableView):
class CollectivitiesView(SuperAdminMixin, SearchMixin, SingleTableView):
search_filter = ('name', 'slug', 'sirh_label', 'postal_code')
template_name = 'authentic2_pratic/collectivities.html'
model = models.Collectivity
table_class = tables.CollectivityTable
class CollectivityAddView(TitleMixin, ActionMixin, AjaxFormViewMixin, CreateView):
class CollectivityAddView(SuperAdminMixin, TitleMixin, ActionMixin, AjaxFormViewMixin, CreateView):
model = models.Collectivity
title = _('Add collectivity')
template_name = 'authentic2_pratic/form.html'
action = _('Add')
class CollectivityView(TitleMixin, OtherActionsMixin,
class CollectivityView(SuperAdminMixin, TitleMixin, OtherActionsMixin,
AjaxFormViewMixin, UpdateView):
model = models.Collectivity
title = _('Edit collectivity')
@ -88,7 +100,7 @@ class CollectivityView(TitleMixin, OtherActionsMixin,
form_class = forms.CollectivityForm
pk_url_kwarg = 'collectivity_pk'
class CollectivityDeleteView(TitleMixin, AjaxFormViewMixin, DeleteView):
class CollectivityDeleteView(SuperAdminMixin, TitleMixin, AjaxFormViewMixin, DeleteView):
model = models.Service
template_name = 'authentic2_pratic/delete.html'
title = _('Delete collectivity')
@ -99,6 +111,15 @@ class CollectivityMixin(object):
def dispatch(self, request, *args, **kwargs):
self.collectivity = get_object_or_404(models.Collectivity,
pk=kwargs['collectivity_pk'])
user = request.user
if not user.is_authenticated:
return redirect_to_login(request.get_full_path())
if not user.is_superuser and \
(not hasattr(user, 'is_admin') or
not (user.is_admin and user.collectivity == self.collectivity)):
messages.warning(request, _('You are not a super-administrator or an administrator of %s') % self.collectivity)
return redirect('auth_homepage')
return super(CollectivityMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@ -219,35 +240,35 @@ class AccessDeleteView(AccessMixin, CollectivityMixin, TitleMixin,
success_url = 'a2-pratic-users'
# general views
homepage = pratic_admin(HomepageView.as_view())
homepage = HomepageView.as_view()
# services
services = pratic_admin(ServicesView.as_view())
service_edit = pratic_admin(ServiceView.as_view())
service_add = pratic_admin(ServiceAddView.as_view())
service_delete = pratic_admin(ServiceDeleteView.as_view())
services = ServicesView.as_view()
service_edit = ServiceView.as_view()
service_add = ServiceAddView.as_view()
service_delete = ServiceDeleteView.as_view()
# collectivities
collectivities = pratic_admin(CollectivitiesView.as_view())
collectivity_edit = pratic_admin(CollectivityView.as_view())
collectivity_add = pratic_admin(CollectivityAddView.as_view())
collectivity_delete = pratic_admin(CollectivityDeleteView.as_view())
collectivities = CollectivitiesView.as_view()
collectivity_edit = CollectivityView.as_view()
collectivity_add = CollectivityAddView.as_view()
collectivity_delete = CollectivityDeleteView.as_view()
# collectivity users
collectivity_users = pratic_admin(UsersView.as_view())
user_add = pratic_admin(UserAddView.as_view())
user_edit = pratic_admin(UserView.as_view())
user_delete = pratic_admin(UserDeleteView.as_view())
collectivity_users = UsersView.as_view()
user_add = UserAddView.as_view()
user_edit = UserView.as_view()
user_delete = UserDeleteView.as_view()
# service instances
collectivity_service_instances = pratic_admin(ServiceInstancesView.as_view())
service_instance_add = pratic_admin(ServiceInstanceAddView.as_view())
service_instance_edit = pratic_admin(ServiceInstanceView.as_view())
service_instance_delete = pratic_admin(ServiceInstanceDeleteView.as_view())
collectivity_service_instances = ServiceInstancesView.as_view()
service_instance_add = ServiceInstanceAddView.as_view()
service_instance_edit = ServiceInstanceView.as_view()
service_instance_delete = ServiceInstanceDeleteView.as_view()
# accesses
collectivity_accesses = pratic_admin(AccessesView.as_view())
access_add = pratic_admin(AccessAddView.as_view())
access_edit = pratic_admin(AccessView.as_view())
access_delete = pratic_admin(AccessDeleteView.as_view())
collectivity_accesses = AccessesView.as_view()
access_add = AccessAddView.as_view()
access_edit = AccessView.as_view()
access_delete = AccessDeleteView.as_view()