Projet

Général

Profil

0001-reorganize-views-and-forms-32934.patch

Benjamin Dauvergne, 09 mai 2019 19:00

Télécharger (123 ko)

Voir les différences:

Subject: [PATCH 1/2] reorganize views and forms (#32934)

 src/authentic2/admin.py                       |   2 +-
 src/authentic2/app_settings.py                |  10 -
 src/authentic2/attribute_aggregator/models.py |   0
 src/authentic2/auth2_auth/models.py           |   0
 src/authentic2/authenticators.py              |   5 +-
 src/authentic2/forms/__init__.py              | 273 -------
 src/authentic2/forms/authentication.py        | 119 +++
 src/authentic2/forms/passwords.py             | 128 ++++
 src/authentic2/forms/profile.py               | 168 +++++
 .../forms.py => forms/registration.py}        | 119 +--
 src/authentic2/forms/utils.py                 |  33 +
 src/authentic2/idp/models.py                  |   0
 src/authentic2/idp/utils.py                   |   0
 src/authentic2/idp/views.py                   |   0
 src/authentic2/manager/forms.py               |   7 +-
 src/authentic2/manager/models.py              |   0
 src/authentic2/manager/views.py               |   6 +-
 src/authentic2/middleware.py                  |  11 +-
 src/authentic2/passwords.py                   |   2 +-
 src/authentic2/profile_forms.py               |  38 -
 src/authentic2/profile_urls.py                |  97 ---
 src/authentic2/profile_views.py               | 127 ----
 .../registration_backend/__init__.py          |   0
 src/authentic2/registration_backend/urls.py   |  23 -
 src/authentic2/registration_backend/views.py  | 416 -----------
 src/authentic2/urls.py                        | 121 ++-
 src/authentic2/views.py                       | 696 ++++++++++++++++--
 27 files changed, 1231 insertions(+), 1170 deletions(-)
 delete mode 100644 src/authentic2/attribute_aggregator/models.py
 delete mode 100644 src/authentic2/auth2_auth/models.py
 create mode 100644 src/authentic2/forms/authentication.py
 create mode 100644 src/authentic2/forms/passwords.py
 create mode 100644 src/authentic2/forms/profile.py
 rename src/authentic2/{registration_backend/forms.py => forms/registration.py} (53%)
 create mode 100644 src/authentic2/forms/utils.py
 delete mode 100644 src/authentic2/idp/models.py
 delete mode 100644 src/authentic2/idp/utils.py
 delete mode 100644 src/authentic2/idp/views.py
 delete mode 100644 src/authentic2/manager/models.py
 delete mode 100644 src/authentic2/profile_forms.py
 delete mode 100644 src/authentic2/profile_urls.py
 delete mode 100644 src/authentic2/profile_views.py
 delete mode 100644 src/authentic2/registration_backend/__init__.py
 delete mode 100644 src/authentic2/registration_backend/urls.py
 delete mode 100644 src/authentic2/registration_backend/views.py
src/authentic2/admin.py
18 18
from .nonce.models import Nonce
19 19
from . import (models, compat, app_settings, decorators,
20 20
        attribute_kinds, utils)
21
from .forms import modelform_factory, BaseUserForm
21
from .forms.profile import BaseUserForm, modelform_factory
22 22
from .custom_user.models import User
23 23

  
24 24
def cleanup_action(modeladmin, request, queryset):
src/authentic2/app_settings.py
88 88
    CAFILE = Setting(names=('AUTHENTIC2_CAFILE', 'CAFILE'),
89 89
            default=None,
90 90
            definition='File containing certificate chains as PEM certificates'),
91
    A2_REGISTRATION_URLCONF = Setting(default='authentic2.registration_backend.urls',
92
                definition='Root urlconf for the /accounts endpoints'),
93
    A2_REGISTRATION_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.RegistrationForm',
94
                definition='Default registration form'),
95
    A2_REGISTRATION_COMPLETION_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.RegistrationCompletionForm',
96
                definition='Default registration completion form'),
97
    A2_REGISTRATION_SET_PASSWORD_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.SetPasswordForm',
98
                definition='Default set password form'),
99
    A2_REGISTRATION_CHANGE_PASSWORD_FORM_CLASS = Setting(default='authentic2.registration_backend.forms.PasswordChangeForm',
100
                definition='Default change password form'),
101 91
    A2_REGISTRATION_CAN_DELETE_ACCOUNT = Setting(default=True,
102 92
                definition='Can user self delete their account and all their data'),
103 93
    A2_REGISTRATION_CAN_CHANGE_PASSWORD = Setting(default=True, definition='Allow user to change its own password'),
src/authentic2/authenticators.py
1 1
from django.shortcuts import render
2 2
from django.utils.translation import ugettext as _, ugettext_lazy
3 3

  
4
from . import views, app_settings, utils, constants, forms
4
from . import views, app_settings, utils, constants
5
from .forms import authentication as authentication_forms
5 6

  
6 7

  
7 8
class LoginPasswordAuthenticator(object):
......
20 21
        context = kwargs.get('context', {})
21 22
        is_post = request.method == 'POST' and self.submit_name in request.POST
22 23
        data = request.POST if is_post else None
23
        form = forms.AuthenticationForm(request=request, data=data)
24
        form = authentication_forms.AuthenticationForm(request=request, data=data)
24 25
        if app_settings.A2_ACCEPT_EMAIL_AUTHENTICATION:
25 26
            form.fields['username'].label = _('Username or email')
26 27
        if app_settings.A2_USERNAME_LABEL:
src/authentic2/forms/__init__.py
1
#
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import math
18

  
19
from django import forms
20
from django.forms.models import modelform_factory as django_modelform_factory
21
from django.utils.translation import ugettext_lazy as _
22
from django.contrib.auth import REDIRECT_FIELD_NAME, forms as auth_forms
23
from django.utils import html
24

  
25
from django.contrib.auth import authenticate
26

  
27
from django_rbac.utils import get_ou_model
28

  
29
from authentic2.utils import lazy_label
30
from authentic2.compat import get_user_model
31
from authentic2.forms.fields import PasswordField
32

  
33
from .. import app_settings
34
from ..exponential_retry_timeout import ExponentialRetryTimeout
35

  
36
OU = get_ou_model()
37

  
38

  
39
class EmailChangeFormNoPassword(forms.Form):
40
    email = forms.EmailField(label=_('New email'))
41

  
42
    def __init__(self, user, *args, **kwargs):
43
        self.user = user
44
        super(EmailChangeFormNoPassword, self).__init__(*args, **kwargs)
45

  
46

  
47
class EmailChangeForm(EmailChangeFormNoPassword):
48
    password = forms.CharField(label=_("Password"),
49
                               widget=forms.PasswordInput)
50

  
51
    def clean_email(self):
52
        email = self.cleaned_data['email']
53
        if email == self.user.email:
54
            raise forms.ValidationError(_('This is already your email address.'))
55
        return email
56

  
57
    def clean_password(self):
58
        password = self.cleaned_data["password"]
59
        if not self.user.check_password(password):
60
            raise forms.ValidationError(
61
                _('Incorrect password.'),
62
                code='password_incorrect',
63
            )
64
        return password
65

  
66

  
67
class NextUrlFormMixin(forms.Form):
68
    next_url = forms.CharField(widget=forms.HiddenInput(), required=False)
69

  
70
    def __init__(self, *args, **kwargs):
71
        from authentic2.middleware import StoreRequestMiddleware
72

  
73
        next_url = kwargs.pop('next_url', None)
74
        request = StoreRequestMiddleware.get_request()
75
        if not next_url and request:
76
            next_url = request.GET.get(REDIRECT_FIELD_NAME)
77
        super(NextUrlFormMixin, self).__init__(*args, **kwargs)
78
        if next_url:
79
            self.fields['next_url'].initial = next_url
80

  
81

  
82
class BaseUserForm(forms.ModelForm):
83
    error_messages = {
84
        'duplicate_username': _("A user with that username already exists."),
85
    }
86

  
87
    def __init__(self, *args, **kwargs):
88
        from authentic2 import models
89

  
90
        self.attributes = models.Attribute.objects.all()
91
        initial = kwargs.setdefault('initial', {})
92
        if kwargs.get('instance'):
93
            instance = kwargs['instance']
94
            for av in models.AttributeValue.objects.with_owner(instance):
95
                if av.attribute.name in self.declared_fields:
96
                    if av.verified:
97
                        self.declared_fields[av.attribute.name].widget.attrs['readonly'] = 'readonly'
98
                    initial[av.attribute.name] = av.to_python()
99
        super(BaseUserForm, self).__init__(*args, **kwargs)
100

  
101
    def clean(self):
102
        from authentic2 import models
103

  
104
        # make sure verified fields are not modified
105
        for av in models.AttributeValue.objects.with_owner(
106
                self.instance).filter(verified=True):
107
            self.cleaned_data[av.attribute.name] = av.to_python()
108
        super(BaseUserForm, self).clean()
109

  
110
    def save_attributes(self):
111
        # only save non verified attributes here
112
        verified_attributes = set(
113
            self.instance.attribute_values.filter(verified=True).values_list('attribute__name', flat=True)
114
        )
115
        for attribute in self.attributes:
116
            name = attribute.name
117
            if name in self.fields and name not in verified_attributes:
118
                value = self.cleaned_data[name]
119
                setattr(self.instance.attributes, name, value)
120

  
121
    def save(self, commit=True):
122
        result = super(BaseUserForm, self).save(commit=commit)
123
        if commit:
124
            self.save_attributes()
125
        else:
126
            old = self.save_m2m
127

  
128
            def save_m2m(*args, **kwargs):
129
                old(*args, **kwargs)
130
                self.save_attributes()
131
            self.save_m2m = save_m2m
132
        return result
133

  
134

  
135
class EditProfileForm(NextUrlFormMixin, BaseUserForm):
136
    pass
137

  
138

  
139
def modelform_factory(model, **kwargs):
140
    '''Build a modelform for the given model,
141

  
142
       For the user model also add attribute based fields.
143
    '''
144
    from authentic2 import models
145

  
146
    form = kwargs.pop('form', None)
147
    fields = kwargs.get('fields') or []
148
    required = list(kwargs.pop('required', []) or [])
149
    d = {}
150
    # KV attributes are only supported for the user model currently
151
    modelform = None
152
    if issubclass(model, get_user_model()):
153
        if not form:
154
            form = BaseUserForm
155
        attributes = models.Attribute.objects.all()
156
        for attribute in attributes:
157
            if attribute.name not in fields:
158
                continue
159
            d[attribute.name] = attribute.get_form_field()
160
        for field in app_settings.A2_REQUIRED_FIELDS:
161
            if field not in required:
162
                required.append(field)
163
    if not form or not hasattr(form, 'Meta'):
164
        meta_d = {'model': model, 'fields': '__all__'}
165
        meta = type('Meta', (), meta_d)
166
        d['Meta'] = meta
167
    if not form:  # fallback
168
        form = forms.ModelForm
169
    modelform = None
170
    if required:
171
        def __init__(self, *args, **kwargs):
172
            super(modelform, self).__init__(*args, **kwargs)
173
            for field in required:
174
                if field in self.fields:
175
                    self.fields[field].required = True
176
        d['__init__'] = __init__
177
    modelform = type(model.__name__ + 'ModelForm', (form,), d)
178
    kwargs['form'] = modelform
179
    modelform.required_css_class = 'form-field-required'
180
    return django_modelform_factory(model, **kwargs)
181

  
182

  
183
class AuthenticationForm(auth_forms.AuthenticationForm):
184
    password = PasswordField(label=_('Password'))
185
    remember_me = forms.BooleanField(
186
        initial=False,
187
        required=False,
188
        label=_('Remember me'),
189
        help_text=_('Do not ask for authentication next time'))
190
    ou = forms.ModelChoiceField(
191
        label=lazy_label(_('Organizational unit'), lambda: app_settings.A2_LOGIN_FORM_OU_SELECTOR_LABEL),
192
        required=True,
193
        queryset=OU.objects.all())
194

  
195
    def __init__(self, *args, **kwargs):
196
        super(AuthenticationForm, self).__init__(*args, **kwargs)
197
        self.exponential_backoff = ExponentialRetryTimeout(
198
            key_prefix='login-exp-backoff-',
199
            duration=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION,
200
            factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR)
201

  
202
        if not app_settings.A2_USER_REMEMBER_ME:
203
            del self.fields['remember_me']
204

  
205
        if not app_settings.A2_LOGIN_FORM_OU_SELECTOR:
206
            del self.fields['ou']
207

  
208
        if self.request:
209
            self.remote_addr = self.request.META['REMOTE_ADDR']
210
        else:
211
            self.remote_addr = '0.0.0.0'
212

  
213
    def exp_backoff_keys(self):
214
        return self.cleaned_data['username'], self.remote_addr
215

  
216
    def clean(self):
217
        username = self.cleaned_data.get('username')
218
        password = self.cleaned_data.get('password')
219

  
220
        keys = None
221
        if username and password:
222
            keys = self.exp_backoff_keys()
223
            seconds_to_wait = self.exponential_backoff.seconds_to_wait(*keys)
224
            if seconds_to_wait > app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION:
225
                seconds_to_wait -= app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION
226
                msg = _('You made too many login errors recently, you must '
227
                        'wait <span class="js-seconds-until">%s</span> seconds '
228
                        'to try again.')
229
                msg = msg % int(math.ceil(seconds_to_wait))
230
                msg = html.mark_safe(msg)
231
                raise forms.ValidationError(msg)
232

  
233
        try:
234
            self.clean_authenticate()
235
        except Exception:
236
            if keys:
237
                self.exponential_backoff.failure(*keys)
238
            raise
239
        else:
240
            if keys:
241
                self.exponential_backoff.success(*keys)
242
        return self.cleaned_data
243

  
244
    def clean_authenticate(self):
245
        # copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector
246
        username = self.cleaned_data.get('username')
247
        password = self.cleaned_data.get('password')
248
        ou = self.cleaned_data.get('ou')
249

  
250
        if username is not None and password:
251
            self.user_cache = authenticate(username=username, password=password, ou=ou, request=self.request)
252
            if self.user_cache is None:
253
                raise forms.ValidationError(
254
                    self.error_messages['invalid_login'],
255
                    code='invalid_login',
256
                    params={'username': self.username_field.verbose_name},
257
                )
258
            else:
259
                self.confirm_login_allowed(self.user_cache)
260

  
261
        return self.cleaned_data
262

  
263
    @property
264
    def media(self):
265
        media = super(AuthenticationForm, self).media
266
        media.add_js(['authentic2/js/js_seconds_until.js'])
267
        if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
268
            media.add_js(['authentic2/js/ou_selector.js'])
269
        return media
270

  
271

  
272
class SiteImportForm(forms.Form):
273
    site_json = forms.FileField(label=_('Site Export File'))
src/authentic2/forms/authentication.py
1
#
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import math
18

  
19
from django import forms
20
from django.utils.translation import ugettext_lazy as _
21
from django.contrib.auth import forms as auth_forms
22
from django.utils import html
23

  
24
from django.contrib.auth import authenticate
25

  
26
from authentic2.forms.fields import PasswordField
27

  
28
from ..a2_rbac.models import OrganizationalUnit as OU
29
from .. import app_settings, utils
30
from ..exponential_retry_timeout import ExponentialRetryTimeout
31

  
32

  
33
class AuthenticationForm(auth_forms.AuthenticationForm):
34
    password = PasswordField(label=_('Password'))
35
    remember_me = forms.BooleanField(
36
        initial=False,
37
        required=False,
38
        label=_('Remember me'),
39
        help_text=_('Do not ask for authentication next time'))
40
    ou = forms.ModelChoiceField(
41
        label=utils.lazy_label(_('Organizational unit'), lambda: app_settings.A2_LOGIN_FORM_OU_SELECTOR_LABEL),
42
        required=True,
43
        queryset=OU.objects.all())
44

  
45
    def __init__(self, *args, **kwargs):
46
        super(AuthenticationForm, self).__init__(*args, **kwargs)
47
        self.exponential_backoff = ExponentialRetryTimeout(
48
            key_prefix='login-exp-backoff-',
49
            duration=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_DURATION,
50
            factor=app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_FACTOR)
51

  
52
        if not app_settings.A2_USER_REMEMBER_ME:
53
            del self.fields['remember_me']
54

  
55
        if not app_settings.A2_LOGIN_FORM_OU_SELECTOR:
56
            del self.fields['ou']
57

  
58
        if self.request:
59
            self.remote_addr = self.request.META['REMOTE_ADDR']
60
        else:
61
            self.remote_addr = '0.0.0.0'
62

  
63
    def exp_backoff_keys(self):
64
        return self.cleaned_data['username'], self.remote_addr
65

  
66
    def clean(self):
67
        username = self.cleaned_data.get('username')
68
        password = self.cleaned_data.get('password')
69

  
70
        keys = None
71
        if username and password:
72
            keys = self.exp_backoff_keys()
73
            seconds_to_wait = self.exponential_backoff.seconds_to_wait(*keys)
74
            if seconds_to_wait > app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION:
75
                seconds_to_wait -= app_settings.A2_LOGIN_EXPONENTIAL_RETRY_TIMEOUT_MIN_DURATION
76
                msg = _('You made too many login errors recently, you must '
77
                        'wait <span class="js-seconds-until">%s</span> seconds '
78
                        'to try again.')
79
                msg = msg % int(math.ceil(seconds_to_wait))
80
                msg = html.mark_safe(msg)
81
                raise forms.ValidationError(msg)
82

  
83
        try:
84
            self.clean_authenticate()
85
        except Exception:
86
            if keys:
87
                self.exponential_backoff.failure(*keys)
88
            raise
89
        else:
90
            if keys:
91
                self.exponential_backoff.success(*keys)
92
        return self.cleaned_data
93

  
94
    def clean_authenticate(self):
95
        # copied from django.contrib.auth.forms.AuthenticationForm to add support for ou selector
96
        username = self.cleaned_data.get('username')
97
        password = self.cleaned_data.get('password')
98
        ou = self.cleaned_data.get('ou')
99

  
100
        if username is not None and password:
101
            self.user_cache = authenticate(username=username, password=password, ou=ou, request=self.request)
102
            if self.user_cache is None:
103
                raise forms.ValidationError(
104
                    self.error_messages['invalid_login'],
105
                    code='invalid_login',
106
                    params={'username': self.username_field.verbose_name},
107
                )
108
            else:
109
                self.confirm_login_allowed(self.user_cache)
110

  
111
        return self.cleaned_data
112

  
113
    @property
114
    def media(self):
115
        media = super(AuthenticationForm, self).media
116
        media.add_js(['authentic2/js/js_seconds_until.js'])
117
        if app_settings.A2_LOGIN_FORM_OU_SELECTOR:
118
            media.add_js(['authentic2/js/ou_selector.js'])
119
        return media
src/authentic2/forms/passwords.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import logging
18
from collections import OrderedDict
19

  
20
from django.contrib.auth import forms as auth_forms
21
from django.core.exceptions import ValidationError
22
from django.forms import Form
23
from django import forms
24
from django.utils.translation import ugettext_lazy as _
25

  
26
from .. import models, hooks, app_settings, utils
27
from ..backends import get_user_queryset
28
from .fields import PasswordField, NewPasswordField, CheckPasswordField
29
from .utils import NextUrlFormMixin
30

  
31

  
32
logger = logging.getLogger(__name__)
33

  
34

  
35
class PasswordResetForm(forms.Form):
36
    next_url = forms.CharField(widget=forms.HiddenInput, required=False)
37

  
38
    email = forms.EmailField(
39
        label=_("Email"), max_length=254)
40

  
41
    def save(self):
42
        """
43
        Generates a one-use only link for resetting password and sends to the
44
        user.
45
        """
46
        email = self.cleaned_data["email"].strip()
47
        users = get_user_queryset()
48
        active_users = users.filter(email__iexact=email, is_active=True)
49
        for user in active_users:
50
            # we don't set the password to a random string, as some users should not have
51
            # a password
52
            set_random_password = (user.has_usable_password()
53
                                   and app_settings.A2_SET_RANDOM_PASSWORD_ON_RESET)
54
            utils.send_password_reset_mail(
55
                user,
56
                set_random_password=set_random_password,
57
                next_url=self.cleaned_data.get('next_url'))
58
        if not active_users:
59
            logger.info(u'password reset requests for "%s", no user found')
60
        hooks.call_hooks('event', name='password-reset', email=email, users=active_users)
61

  
62

  
63
class PasswordResetMixin(Form):
64
    '''Remove all password reset object for the current user when password is
65
       successfully changed.'''
66

  
67
    def save(self, commit=True):
68
        ret = super(PasswordResetMixin, self).save(commit=commit)
69
        if commit:
70
            models.PasswordReset.objects.filter(user=self.user).delete()
71
        else:
72
            old_save = self.user.save
73

  
74
            def save(*args, **kwargs):
75
                ret = old_save(*args, **kwargs)
76
                models.PasswordReset.objects.filter(user=self.user).delete()
77
                return ret
78
            self.user.save = save
79
        return ret
80

  
81

  
82
class NotifyOfPasswordChange(object):
83
    def save(self, commit=True):
84
        user = super(NotifyOfPasswordChange, self).save(commit=commit)
85
        if user.email:
86
            ctx = {
87
                'user': user,
88
                'password': self.cleaned_data['new_password1'],
89
            }
90
            utils.send_templated_mail(user, "authentic2/password_change", ctx)
91
        return user
92

  
93

  
94
class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.SetPasswordForm):
95
    new_password1 = NewPasswordField(label=_("New password"))
96
    new_password2 = CheckPasswordField(label=_("New password confirmation"))
97

  
98
    def clean_new_password1(self):
99
        new_password1 = self.cleaned_data.get('new_password1')
100
        if new_password1 and self.user.check_password(new_password1):
101
            raise ValidationError(_('New password must differ from old password'))
102
        return new_password1
103

  
104

  
105
class PasswordChangeForm(NotifyOfPasswordChange, NextUrlFormMixin, PasswordResetMixin,
106
                         auth_forms.PasswordChangeForm):
107
    old_password = PasswordField(label=_('Old password'))
108
    new_password1 = NewPasswordField(label=_('New password'))
109
    new_password2 = CheckPasswordField(label=_("New password confirmation"))
110

  
111
    def clean_new_password1(self):
112
        new_password1 = self.cleaned_data.get('new_password1')
113
        old_password = self.cleaned_data.get('old_password')
114
        if new_password1 and new_password1 == old_password:
115
            raise ValidationError(_('New password must differ from old password'))
116
        return new_password1
117

  
118
# make old_password the first field
119
new_base_fields = OrderedDict()
120

  
121
for k in ['old_password', 'new_password1', 'new_password2']:
122
    new_base_fields[k] = PasswordChangeForm.base_fields[k]
123

  
124
for k in PasswordChangeForm.base_fields:
125
    if k not in ['old_password', 'new_password1', 'new_password2']:
126
        new_base_fields[k] = PasswordChangeForm.base_fields[k]
127

  
128
PasswordChangeForm.base_fields = new_base_fields
src/authentic2/forms/profile.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17

  
18
from django.forms.models import modelform_factory as dj_modelform_factory
19
from django import forms
20
from django.utils.translation import ugettext_lazy as _, ugettext
21

  
22
from ..custom_user.models import User
23
from .. import app_settings, models
24
from .utils import NextUrlFormMixin
25

  
26

  
27
class DeleteAccountForm(forms.Form):
28
    password = forms.CharField(widget=forms.PasswordInput, label=_("Password"))
29

  
30
    def __init__(self, *args, **kwargs):
31
        self.user = kwargs.pop('user')
32
        super(DeleteAccountForm, self).__init__(*args, **kwargs)
33

  
34
    def clean_password(self):
35
        password = self.cleaned_data.get('password')
36
        if password and not self.user.check_password(password):
37
            raise forms.ValidationError(ugettext('Password is invalid'))
38
        return password
39

  
40

  
41
class EmailChangeFormNoPassword(forms.Form):
42
    email = forms.EmailField(label=_('New email'))
43

  
44
    def __init__(self, user, *args, **kwargs):
45
        self.user = user
46
        super(EmailChangeFormNoPassword, self).__init__(*args, **kwargs)
47

  
48

  
49
class EmailChangeForm(EmailChangeFormNoPassword):
50
    password = forms.CharField(label=_("Password"),
51
                               widget=forms.PasswordInput)
52

  
53
    def clean_email(self):
54
        email = self.cleaned_data['email']
55
        if email == self.user.email:
56
            raise forms.ValidationError(_('This is already your email address.'))
57
        return email
58

  
59
    def clean_password(self):
60
        password = self.cleaned_data["password"]
61
        if not self.user.check_password(password):
62
            raise forms.ValidationError(
63
                _('Incorrect password.'),
64
                code='password_incorrect',
65
            )
66
        return password
67

  
68

  
69
class BaseUserForm(forms.ModelForm):
70
    error_messages = {
71
        'duplicate_username': _("A user with that username already exists."),
72
    }
73

  
74
    def __init__(self, *args, **kwargs):
75
        from authentic2 import models
76

  
77
        self.attributes = models.Attribute.objects.all()
78
        initial = kwargs.setdefault('initial', {})
79
        if kwargs.get('instance'):
80
            instance = kwargs['instance']
81
            for av in models.AttributeValue.objects.with_owner(instance):
82
                if av.attribute.name in self.declared_fields:
83
                    if av.verified:
84
                        self.declared_fields[av.attribute.name].widget.attrs['readonly'] = 'readonly'
85
                    initial[av.attribute.name] = av.to_python()
86
        super(BaseUserForm, self).__init__(*args, **kwargs)
87

  
88
    def clean(self):
89
        from authentic2 import models
90

  
91
        # make sure verified fields are not modified
92
        for av in models.AttributeValue.objects.with_owner(
93
                self.instance).filter(verified=True):
94
            self.cleaned_data[av.attribute.name] = av.to_python()
95
        super(BaseUserForm, self).clean()
96

  
97
    def save_attributes(self):
98
        # only save non verified attributes here
99
        verified_attributes = set(
100
            self.instance.attribute_values.filter(verified=True).values_list('attribute__name', flat=True)
101
        )
102
        for attribute in self.attributes:
103
            name = attribute.name
104
            if name in self.fields and name not in verified_attributes:
105
                value = self.cleaned_data[name]
106
                setattr(self.instance.attributes, name, value)
107

  
108
    def save(self, commit=True):
109
        result = super(BaseUserForm, self).save(commit=commit)
110
        if commit:
111
            self.save_attributes()
112
        else:
113
            old = self.save_m2m
114

  
115
            def save_m2m(*args, **kwargs):
116
                old(*args, **kwargs)
117
                self.save_attributes()
118
            self.save_m2m = save_m2m
119
        return result
120

  
121

  
122
class EditProfileForm(NextUrlFormMixin, BaseUserForm):
123
    pass
124

  
125

  
126
def modelform_factory(model, **kwargs):
127
    '''Build a modelform for the given model,
128

  
129
       For the user model also add attribute based fields.
130
    '''
131

  
132
    form = kwargs.pop('form', None)
133
    fields = kwargs.get('fields') or []
134
    required = list(kwargs.pop('required', []) or [])
135
    d = {}
136
    # KV attributes are only supported for the user model currently
137
    modelform = None
138
    if issubclass(model, User):
139
        if not form:
140
            form = profile_forms.BaseUserForm
141
        attributes = models.Attribute.objects.all()
142
        for attribute in attributes:
143
            if attribute.name not in fields:
144
                continue
145
            d[attribute.name] = attribute.get_form_field()
146
        for field in app_settings.A2_REQUIRED_FIELDS:
147
            if field not in required:
148
                required.append(field)
149
    if not form or not hasattr(form, 'Meta'):
150
        meta_d = {'model': model, 'fields': '__all__'}
151
        meta = type('Meta', (), meta_d)
152
        d['Meta'] = meta
153
    if not form:  # fallback
154
        form = forms.ModelForm
155
    modelform = None
156
    if required:
157
        def __init__(self, *args, **kwargs):
158
            super(modelform, self).__init__(*args, **kwargs)
159
            for field in required:
160
                if field in self.fields:
161
                    self.fields[field].required = True
162
        d['__init__'] = __init__
163
    modelform = type(model.__name__ + 'ModelForm', (form,), d)
164
    kwargs['form'] = modelform
165
    modelform.required_css_class = 'form-field-required'
166
    return dj_modelform_factory(model, **kwargs)
167

  
168

  
src/authentic2/registration_backend/forms.py → src/authentic2/forms/registration.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
1 17
import re
2
import copy
3
from collections import OrderedDict
4 18

  
5
from django.conf import settings
6 19
from django.core.exceptions import ValidationError
7 20
from django.utils.translation import ugettext_lazy as _, ugettext
8
from django.forms import ModelForm, Form, CharField, PasswordInput, EmailField
9
from django.db.models.fields import FieldDoesNotExist
10
from django.forms.utils import ErrorList
21
from django.forms import Form, EmailField
11 22

  
12 23
from django.contrib.auth.models import BaseUserManager, Group
13
from django.contrib.auth import forms as auth_forms, get_user_model, REDIRECT_FIELD_NAME
14
from django.core.mail import send_mail
15
from django.core import signing
16
from django.template import RequestContext
17
from django.template.loader import render_to_string
18
from django.core.urlresolvers import reverse
19
from django.core.validators import RegexValidator
20

  
21
from authentic2.forms.fields import PasswordField, NewPasswordField, CheckPasswordField
22
from .. import app_settings, compat, forms, utils, validators, models, middleware, hooks
24

  
25
from authentic2.forms.fields import NewPasswordField, CheckPasswordField
23 26
from authentic2.a2_rbac.models import OrganizationalUnit
24 27

  
28
from .. import app_settings, compat, forms, models
29
from . import profile as profile_forms
30

  
25 31
User = compat.get_user_model()
26 32

  
27 33

  
......
53 59
        return email
54 60

  
55 61

  
56
class RegistrationCompletionFormNoPassword(forms.BaseUserForm):
62
class RegistrationCompletionFormNoPassword(profile_forms.BaseUserForm):
57 63
    error_css_class = 'form-field-error'
58 64
    required_css_class = 'form-field-required'
59 65

  
......
67 73
                ou = OrganizationalUnit.objects.get(pk=self.data['ou'])
68 74
                username_is_unique |= ou.username_is_unique
69 75
            if username_is_unique:
70
                User = get_user_model()
71 76
                exist = False
72 77
                try:
73 78
                    User.objects.get(username=username)
......
86 91
        if self.cleaned_data.get('email'):
87 92
            email = self.cleaned_data['email']
88 93
            if app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE:
89
                User = get_user_model()
90 94
                exist = False
91 95
                try:
92 96
                    User.objects.get(email__iexact=email)
......
130 134
                raise ValidationError(_("The two password fields didn't match."))
131 135
            self.instance.set_password(self.cleaned_data['password1'])
132 136
        return self.cleaned_data
133

  
134

  
135
class PasswordResetMixin(Form):
136
    '''Remove all password reset object for the current user when password is
137
       successfully changed.'''
138

  
139
    def save(self, commit=True):
140
        ret = super(PasswordResetMixin, self).save(commit=commit)
141
        if commit:
142
            models.PasswordReset.objects.filter(user=self.user).delete()
143
        else:
144
            old_save = self.user.save
145
            def save(*args, **kwargs):
146
                ret = old_save(*args, **kwargs)
147
                models.PasswordReset.objects.filter(user=self.user).delete()
148
                return ret
149
            self.user.save = save
150
        return ret
151

  
152

  
153
class NotifyOfPasswordChange(object):
154
    def save(self, commit=True):
155
        user = super(NotifyOfPasswordChange, self).save(commit=commit)
156
        if user.email:
157
            ctx = {
158
                'user': user,
159
                'password': self.cleaned_data['new_password1'],
160
            }
161
            utils.send_templated_mail(user, "authentic2/password_change", ctx)
162
        return user
163

  
164

  
165
class SetPasswordForm(NotifyOfPasswordChange, PasswordResetMixin, auth_forms.SetPasswordForm):
166
    new_password1 = NewPasswordField(label=_("New password"))
167
    new_password2 = CheckPasswordField(label=_("New password confirmation"))
168

  
169
    def clean_new_password1(self):
170
        new_password1 = self.cleaned_data.get('new_password1')
171
        if new_password1 and self.user.check_password(new_password1):
172
            raise ValidationError(_('New password must differ from old password'))
173
        return new_password1
174

  
175

  
176
class PasswordChangeForm(NotifyOfPasswordChange, forms.NextUrlFormMixin, PasswordResetMixin,
177
                         auth_forms.PasswordChangeForm):
178
    old_password = PasswordField(label=_('Old password'))
179
    new_password1 = NewPasswordField(label=_('New password'))
180
    new_password2 = CheckPasswordField(label=_("New password confirmation"))
181

  
182
    def clean_new_password1(self):
183
        new_password1 = self.cleaned_data.get('new_password1')
184
        old_password = self.cleaned_data.get('old_password')
185
        if new_password1 and new_password1 == old_password:
186
            raise ValidationError(_('New password must differ from old password'))
187
        return new_password1
188

  
189
# make old_password the first field
190
PasswordChangeForm.base_fields = OrderedDict(
191
    [(k, PasswordChangeForm.base_fields[k])
192
    for k in ['old_password', 'new_password1', 'new_password2']] +
193
    [(k, PasswordChangeForm.base_fields[k])
194
    for k in PasswordChangeForm.base_fields if k not in ['old_password', 'new_password1',
195
                                                         'new_password2']]
196
)
197

  
198
class DeleteAccountForm(Form):
199
    password = CharField(widget=PasswordInput, label=_("Password"))
200

  
201
    def __init__(self, *args, **kwargs):
202
        self.user = kwargs.pop('user')
203
        super(DeleteAccountForm, self).__init__(*args, **kwargs)
204

  
205
    def clean_password(self):
206
        password = self.cleaned_data.get('password')
207
        if password and not self.user.check_password(password):
208
            raise ValidationError(ugettext('Password is invalid'))
209
        return password
src/authentic2/forms/utils.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django import forms
18
from django.contrib.auth import REDIRECT_FIELD_NAME
19

  
20
from ..middleware import StoreRequestMiddleware
21

  
22

  
23
class NextUrlFormMixin(forms.Form):
24
    next_url = forms.CharField(widget=forms.HiddenInput(), required=False)
25

  
26
    def __init__(self, *args, **kwargs):
27
        next_url = kwargs.pop('next_url', None)
28
        request = StoreRequestMiddleware.get_request()
29
        if not next_url and request:
30
            next_url = request.GET.get(REDIRECT_FIELD_NAME)
31
        super(NextUrlFormMixin, self).__init__(*args, **kwargs)
32
        if next_url:
33
            self.fields['next_url'].initial = next_url
src/authentic2/manager/forms.py
19 19
from django_rbac.utils import get_ou_model, get_role_model, get_permission_model
20 20
from django_rbac.backends import DjangoRBACBackend
21 21

  
22
from authentic2.forms import BaseUserForm
22
from authentic2.forms.profile import BaseUserForm
23 23
from authentic2.models import PasswordReset
24 24
from authentic2.utils import import_module_or_class
25 25
from authentic2.a2_rbac.utils import get_default_ou
......
694 694

  
695 695
    class Meta:
696 696
        fields = ()
697

  
698

  
699
class SiteImportForm(forms.Form):
700
    site_json = forms.FileField(
701
        label=_('Site Export File'))
src/authentic2/manager/views.py
25 25
from django_rbac.utils import get_ou_model
26 26

  
27 27
from authentic2.data_transfer import export_site, import_site, DataImportError, ImportContext
28
from authentic2.forms import modelform_factory, SiteImportForm
28
from authentic2.forms.profile import modelform_factory
29 29
from authentic2.utils import redirect, batch_queryset
30 30
from authentic2.decorators import json as json_view
31 31
from authentic2 import hooks
32 32

  
33
from . import app_settings, utils
33
from . import app_settings, utils, forms
34 34

  
35 35

  
36 36
# https://github.com/MongoEngine/django-mongoengine/blob/master/django_mongoengine/views/edit.py
......
680 680

  
681 681

  
682 682
class SiteImportView(MediaMixin, FormView):
683
    form_class = SiteImportForm
683
    form_class = forms.SiteImportForm
684 684
    template_name = 'authentic2/manager/site_import.html'
685 685
    success_url = reverse_lazy('a2-manager-homepage')
686 686

  
src/authentic2/middleware.py
18 18

  
19 19
from . import app_settings, utils, plugins
20 20

  
21

  
21 22
class ThreadCollector(object):
22 23
    def __init__(self):
23 24
        if threading is None:
......
48 49

  
49 50
MESSAGE_IF_STRING_REPRESENTATION_INVALID = '[Could not get log message]'
50 51

  
52

  
51 53
class ThreadTrackingHandler(logging.Handler):
52 54
    def __init__(self, collector):
53 55
        logging.Handler.__init__(self)
......
77 79
logging_handler = ThreadTrackingHandler(collector)
78 80
logging.root.addHandler(logging_handler)
79 81

  
82

  
80 83
class LoggingCollectorMiddleware(object):
81 84
    def process_request(self, request):
82 85
        collector.clear_collection()
......
90 93
            request.logs = collector.get_collection()
91 94
            request.exception = exception
92 95

  
96

  
93 97
class CollectIPMiddleware(object):
94 98
    def process_response(self, request, response):
95 99
        # only collect IP if session is used
......
104 108
            request.session.modified = True
105 109
        return response
106 110

  
111

  
107 112
class OpenedSessionCookieMiddleware(object):
108 113
    def process_response(self, request, response):
109 114
        # do not emit cookie for API requests
......
122 127
            response.delete_cookie(name, domain=domain)
123 128
        return response
124 129

  
130

  
125 131
class RequestIdMiddleware(object):
126 132
    def process_request(self, request):
127 133
        if not hasattr(request, 'request_id'):
......
136 142
                    hexlify(struct.pack('I', random_id)),
137 143
                    encoding='ascii')
138 144

  
145

  
139 146
class StoreRequestMiddleware(object):
140 147
    collection = {}
141 148

  
......
153 160
    def get_request(cls):
154 161
        return cls.collection.get(threading.currentThread())
155 162

  
163

  
156 164
class ViewRestrictionMiddleware(object):
157 165
    RESTRICTION_SESSION_KEY = 'view-restriction'
158 166

  
......
185 193
            messages.warning(request, _('You must change your password to continue'))
186 194
        return utils.redirect_and_come_back(request, view)
187 195

  
196

  
188 197
class XForwardedForMiddleware(object):
189 198
    '''Copy the first address from X-Forwarded-For header to the REMOTE_ADDR meta.
190 199

  
......
195 204
            request.META['REMOTE_ADDR'] = request.META['HTTP_X_FORWARDED_FOR'].split(",")[0].strip()
196 205
            return None
197 206

  
207

  
198 208
class DisplayMessageBeforeRedirectMiddleware(object):
199 209
    '''Verify if messages are currently stored and if there is a redirection to another domain, in
200 210
       this case show an intermediate page.
......
236 246

  
237 247

  
238 248
class ServiceAccessControlMiddleware(object):
239

  
240 249
    def process_exception(self, request, exception):
241 250
        if not isinstance(exception, (utils.ServiceAccessDenied,)):
242 251
            return None
src/authentic2/passwords.py
6 6
from django.utils.translation import ugettext as _
7 7
from django.utils.module_loading import import_string
8 8
from django.utils.functional import lazy
9
from django.utils.safestring import mark_safe
10 9
from django.utils import six
11 10
from django.core.exceptions import ValidationError
12 11

  
12

  
13 13
from . import app_settings
14 14

  
15 15

  
src/authentic2/profile_forms.py
1
import logging
2

  
3
from django import forms
4
from django.utils.translation import ugettext as _
5
from django.contrib.auth import get_user_model
6

  
7
from .backends import get_user_queryset
8
from .utils import send_password_reset_mail
9
from . import hooks, app_settings
10

  
11

  
12
logger = logging.getLogger(__name__)
13

  
14

  
15
class PasswordResetForm(forms.Form):
16
    next_url = forms.CharField(widget=forms.HiddenInput, required=False)
17

  
18
    email = forms.EmailField(
19
        label=_("Email"), max_length=254)
20

  
21
    def save(self):
22
        """
23
        Generates a one-use only link for resetting password and sends to the
24
        user.
25
        """
26
        email = self.cleaned_data["email"].strip()
27
        users = get_user_queryset()
28
        active_users = users.filter(email__iexact=email, is_active=True)
29
        for user in active_users:
30
            # we don't set the password to a random string, as some users should not have
31
            # a password
32
            set_random_password = (user.has_usable_password()
33
                                   and app_settings.A2_SET_RANDOM_PASSWORD_ON_RESET)
34
            send_password_reset_mail(user, set_random_password=set_random_password,
35
                                     next_url=self.cleaned_data.get('next_url'))
36
        if not active_users:
37
            logger.info(u'password reset requests for "%s", no user found')
38
        hooks.call_hooks('event', name='password-reset', email=email, users=active_users)
src/authentic2/profile_urls.py
1
from django.conf.urls import url
2
from django.contrib.auth import views as auth_views, REDIRECT_FIELD_NAME
3
from django.contrib.auth.decorators import login_required
4
from django.core.urlresolvers import reverse
5
from django.http import HttpResponseRedirect
6
from django.contrib import messages
7
from django.utils.translation import ugettext as _
8
from django.views.decorators.debug import sensitive_post_parameters
9

  
10
from authentic2.utils import import_module_or_class, redirect, user_can_change_password
11
from . import app_settings, decorators, profile_views, hooks
12
from .views import (logged_in, edit_profile, email_change, email_change_verify, profile)
13

  
14
SET_PASSWORD_FORM_CLASS = import_module_or_class(
15
        app_settings.A2_REGISTRATION_SET_PASSWORD_FORM_CLASS)
16
CHANGE_PASSWORD_FORM_CLASS = import_module_or_class(
17
        app_settings.A2_REGISTRATION_CHANGE_PASSWORD_FORM_CLASS)
18

  
19
@sensitive_post_parameters()
20
@login_required
21
@decorators.setting_enabled('A2_REGISTRATION_CAN_CHANGE_PASSWORD')
22
def password_change_view(request, *args, **kwargs):
23
    post_change_redirect = kwargs.pop('post_change_redirect', None)
24
    if 'next_url' in request.POST and request.POST['next_url']:
25
        post_change_redirect = request.POST['next_url']
26
    elif REDIRECT_FIELD_NAME in request.GET:
27
        post_change_redirect = request.GET[REDIRECT_FIELD_NAME]
28
    elif post_change_redirect is None:
29
        post_change_redirect = reverse('account_management')
30
    if not user_can_change_password(request=request):
31
        messages.warning(request, _('Password change is forbidden'))
32
        return redirect(request, post_change_redirect)
33
    if 'cancel' in request.POST:
34
        return redirect(request, post_change_redirect)
35
    kwargs['post_change_redirect'] = post_change_redirect
36
    extra_context = kwargs.setdefault('extra_context', {})
37
    extra_context['view'] = password_change_view
38
    extra_context[REDIRECT_FIELD_NAME] = post_change_redirect
39
    if not request.user.has_usable_password():
40
        kwargs['password_change_form'] = SET_PASSWORD_FORM_CLASS
41
    response = auth_views.password_change(request, *args, **kwargs)
42
    if isinstance(response, HttpResponseRedirect):
43
        hooks.call_hooks('event', name='change-password', user=request.user, request=request)
44
        messages.info(request, _('Password changed'))
45
    return response
46

  
47
password_change_view.title = _('Password Change')
48
password_change_view.do_not_call_in_templates = True
49

  
50

  
51
urlpatterns = [
52
    url(r'^logged-in/$', logged_in, name='logged-in'),
53
    url(r'^edit/$', edit_profile, name='profile_edit'),
54
    url(r'^edit/(?P<scope>[-\w]+)/$', edit_profile, name='profile_edit_with_scope'),
55
    url(r'^change-email/$', email_change, name='email-change'),
56
    url(r'^change-email/verify/$', email_change_verify,
57
        name='email-change-verify'),
58
    url(r'^$', profile, name='account_management'),
59
    url(r'^password/change/$',
60
        password_change_view,
61
        {'password_change_form': CHANGE_PASSWORD_FORM_CLASS},
62
        name='password_change'),
63
    url(r'^password/change/done/$',
64
        auth_views.password_change_done,
65
        name='password_change_done'),
66

  
67
    # Password reset
68
    url(r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
69
        profile_views.password_reset_confirm,
70
        name='password_reset_confirm'),
71
    url(r'^password/reset/$',
72
        profile_views.password_reset,
73
        name='password_reset'),
74

  
75
    # Legacy 
76
    url(r'^password/change/$',
77
        password_change_view,
78
        {'password_change_form': CHANGE_PASSWORD_FORM_CLASS},
79
        name='auth_password_change'),
80
    url(r'^password/change/done/$',
81
        auth_views.password_change_done,
82
        name='auth_password_change_done'),
83
    url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
84
        auth_views.password_reset_confirm,
85
        {'set_password_form': SET_PASSWORD_FORM_CLASS},
86
        name='auth_password_reset_confirm'),
87
    url(r'^password/reset/$',
88
        auth_views.password_reset,
89
        name='auth_password_reset'),
90
    url(r'^password/reset/complete/$',
91
        auth_views.password_reset_complete,
92
        name='auth_password_reset_complete'),
93
    url(r'^password/reset/done/$',
94
        auth_views.password_reset_done,
95
        name='auth_password_reset_done'),
96
    url(r'^switch-back/$', profile_views.switch_back, name='a2-switch-back'),
97
]
src/authentic2/profile_views.py
1
import logging
2

  
3
from django.views.generic import FormView
4
from django.contrib import messages
5
from django.contrib.auth import get_user_model, REDIRECT_FIELD_NAME, authenticate
6
from django.http import Http404
7
from django.utils.translation import ugettext as _
8
from django.utils.http import urlsafe_base64_decode
9

  
10
from .compat import default_token_generator
11
from .registration_backend.forms import SetPasswordForm
12
from . import app_settings, cbv, profile_forms, utils, hooks
13

  
14

  
15
class PasswordResetView(cbv.NextURLViewMixin, FormView):
16
    '''Ask for an email and send a password reset link by mail'''
17
    form_class = profile_forms.PasswordResetForm
18
    title = _('Password Reset')
19

  
20
    def get_template_names(self):
21
        return [
22
            'authentic2/password_reset_form.html',
23
            'registration/password_reset_form.html',
24
        ]
25

  
26
    def get_form_kwargs(self, **kwargs):
27
        kwargs = super(PasswordResetView, self).get_form_kwargs(**kwargs)
28
        initial = kwargs.setdefault('initial', {})
29
        initial['next_url'] = self.request.GET.get(REDIRECT_FIELD_NAME, '')
30
        return kwargs
31

  
32
    def get_context_data(self, **kwargs):
33
        ctx = super(PasswordResetView, self).get_context_data(**kwargs)
34
        if app_settings.A2_USER_CAN_RESET_PASSWORD is False:
35
            raise Http404('Password reset is not allowed.')
36
        ctx['title'] = _('Password reset')
37
        return ctx
38

  
39
    def form_valid(self, form):
40
        form.save()
41
        # return to next URL
42
        messages.info(self.request, _('If your email address exists in our '
43
                                      'database, you will receive an email '
44
                                      'containing instructions to reset '
45
                                      'your password'))
46
        return super(PasswordResetView, self).form_valid(form)
47

  
48
password_reset = PasswordResetView.as_view()
49

  
50

  
51
class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView):
52
    '''Validate password reset link, show a set password form and login
53
       the user.
54
    '''
55
    form_class = SetPasswordForm
56
    title = _('Password Reset')
57

  
58
    def get_template_names(self):
59
        return [
60
            'registration/password_reset_confirm.html',
61
            'authentic2/password_reset_confirm.html',
62
        ]
63

  
64
    def dispatch(self, request, *args, **kwargs):
65
        validlink = True
66
        uidb64 = kwargs['uidb64']
67
        self.token = token = kwargs['token']
68

  
69
        UserModel = get_user_model()
70
        # checked by URLconf
71
        assert uidb64 is not None and token is not None
72
        try:
73
            uid = urlsafe_base64_decode(uidb64)
74
            # use authenticate to eventually get an LDAPUser
75
            self.user = authenticate(user=UserModel._default_manager.get(pk=uid))
76
        except (TypeError, ValueError, OverflowError,
77
                UserModel.DoesNotExist):
78
            validlink = False
79
            messages.warning(request, _('User not found'))
80

  
81
        if validlink and not default_token_generator.check_token(self.user, token):
82
            validlink = False
83
            messages.warning(request, _('You reset password link is invalid '
84
                                        'or has expired'))
85
        if not validlink:
86
            return utils.redirect(request, self.get_success_url())
87
        can_reset_password = utils.get_user_flag(user=self.user,
88
                                                 name='can_reset_password',
89
                                                 default=self.user.has_usable_password())
90
        if not can_reset_password:
91
            messages.warning(request, _('It\'s not possible to reset your password. Please '
92
                                        'contact an administrator.'))
93
            return utils.redirect(request, self.get_success_url())
94
        return super(PasswordResetConfirmView, self).dispatch(request, *args,
95
                                                              **kwargs)
96

  
97
    def get_context_data(self, **kwargs):
98
        ctx = super(PasswordResetConfirmView, self).get_context_data(**kwargs)
99
        # compatibility with existing templates !
100
        ctx['title'] = _('Enter new password')
101
        ctx['validlink'] = True
102
        return ctx
103

  
104
    def get_form_kwargs(self):
105
        kwargs = super(PasswordResetConfirmView, self).get_form_kwargs()
106
        kwargs['user'] = self.user
107
        return kwargs
108

  
109
    def form_valid(self, form):
110
        # Changing password by mail validate the email
111
        form.user.email_verified = True
112
        form.save()
113
        hooks.call_hooks('event', name='password-reset-confirm', user=form.user, token=self.token,
114
                         form=form)
115
        logging.getLogger(__name__).info(u'user %s resetted its password with '
116
                                         'token %r...', self.user,
117
                                         self.token[:9])
118
        return self.finish()
119

  
120
    def finish(self):
121
        return utils.simulate_authentication(self.request, self.user, 'email')
122

  
123
password_reset_confirm = PasswordResetConfirmView.as_view()
124

  
125

  
126
def switch_back(request):
127
    return utils.switch_back(request)
src/authentic2/registration_backend/urls.py
1
from django.conf.urls import url
2

  
3
from django.views.generic.base import TemplateView
4
from django.contrib.auth.decorators import login_required
5

  
6
from .views import RegistrationView, registration_completion, DeleteView, registration_complete
7

  
8
urlpatterns = [
9
    url(r'^activate/(?P<registration_token>[\w: -]+)/$',
10
        registration_completion, name='registration_activate'),
11
    url(r'^register/$',
12
        RegistrationView.as_view(),
13
        name='registration_register'),
14
    url(r'^register/complete/$',
15
        registration_complete,
16
        name='registration_complete'),
17
    url(r'^register/closed/$',
18
        TemplateView.as_view(template_name='registration/registration_closed.html'),
19
        name='registration_disallowed'),
20
    url(r'^delete/$',
21
        login_required(DeleteView.as_view()),
22
        name='delete_account'),
23
]
src/authentic2/registration_backend/views.py
1
import collections
2
import logging
3
import random
4

  
5
from django.conf import settings
6
from django.shortcuts import get_object_or_404
7
from django.utils.translation import ugettext as _
8
from django.utils.http import urlquote
9
from django.contrib import messages
10
from django.contrib.auth import REDIRECT_FIELD_NAME
11
from django.core import signing
12
from django.views.generic.base import TemplateView
13
from django.views.generic.edit import FormView, CreateView
14
from django.contrib.auth import get_user_model
15
from django.forms import CharField, Form
16
from django.core.urlresolvers import reverse_lazy
17
from django.http import Http404, HttpResponseBadRequest
18

  
19
from authentic2.utils import (import_module_or_class, redirect, make_url, get_fields_and_labels,
20
                              simulate_authentication)
21
from authentic2.a2_rbac.utils import get_default_ou
22
from authentic2 import hooks
23

  
24
from django_rbac.utils import get_ou_model
25

  
26
from .. import models, app_settings, compat, cbv, forms, validators, utils, constants
27
from .forms import RegistrationCompletionForm, DeleteAccountForm
28
from .forms import RegistrationCompletionFormNoPassword
29
from authentic2.a2_rbac.models import OrganizationalUnit
30

  
31
logger = logging.getLogger(__name__)
32

  
33
User = compat.get_user_model()
34

  
35

  
36
def valid_token(method):
37
    def f(request, *args, **kwargs):
38
        try:
39
            request.token = signing.loads(kwargs['registration_token'].replace(' ', ''),
40
                                          max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24)
41
        except signing.SignatureExpired:
42
            messages.warning(request, _('Your activation key is expired'))
43
            return redirect(request, 'registration_register')
44
        except signing.BadSignature:
45
            messages.warning(request, _('Activation failed'))
46
            return redirect(request, 'registration_register')
47
        return method(request, *args, **kwargs)
48
    return f
49

  
50

  
51
class BaseRegistrationView(FormView):
52
    form_class = import_module_or_class(app_settings.A2_REGISTRATION_FORM_CLASS)
53
    template_name = 'registration/registration_form.html'
54
    title = _('Registration')
55

  
56
    def dispatch(self, request, *args, **kwargs):
57
        if not getattr(settings, 'REGISTRATION_OPEN', True):
58
            raise Http404('Registration is not open.')
59
        self.token = {}
60
        self.ou = get_default_ou()
61
        # load pre-filled values
62
        if request.GET.get('token'):
63
            try:
64
                self.token = signing.loads(
65
                    request.GET.get('token'),
66
                    max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24)
67
            except (TypeError, ValueError, signing.BadSignature) as e:
68
                logger.warning(u'registration_view: invalid token: %s', e)
69
                return HttpResponseBadRequest('invalid token', content_type='text/plain')
70
            if 'ou' in self.token:
71
                self.ou = OrganizationalUnit.objects.get(pk=self.token['ou'])
72
        self.next_url = self.token.pop(REDIRECT_FIELD_NAME, utils.select_next_url(request, None))
73
        return super(BaseRegistrationView, self).dispatch(request, *args, **kwargs)
74

  
75
    def form_valid(self, form):
76
        email = form.cleaned_data.pop('email')
77
        for field in form.cleaned_data:
78
            self.token[field] = form.cleaned_data[field]
79

  
80
        # propagate service to the registration completion view
81
        if constants.SERVICE_FIELD_NAME in self.request.GET:
82
            self.token[constants.SERVICE_FIELD_NAME] = \
83
                self.request.GET[constants.SERVICE_FIELD_NAME]
84

  
85
        self.token.pop(REDIRECT_FIELD_NAME, None)
86
        self.token.pop('email', None)
87

  
88
        utils.send_registration_mail(self.request, email, next_url=self.next_url,
89
                                     ou=self.ou, **self.token)
90
        self.request.session['registered_email'] = email
91
        return redirect(self.request, 'registration_complete', params={REDIRECT_FIELD_NAME: self.next_url})
92

  
93
    def get_context_data(self, **kwargs):
94
        context = super(BaseRegistrationView, self).get_context_data(**kwargs)
95
        parameters = {'request': self.request,
96
                      'context': context}
97
        blocks = [utils.get_authenticator_method(authenticator, 'registration', parameters)
98
                  for authenticator in utils.get_backends('AUTH_FRONTENDS')]
99
        context['frontends'] = collections.OrderedDict((block['id'], block)
100
                                                       for block in blocks if block)
101
        return context
102

  
103

  
104
class RegistrationView(cbv.ValidateCSRFMixin, BaseRegistrationView):
105
    pass
106

  
107

  
108
class RegistrationCompletionView(CreateView):
109
    model = get_user_model()
110
    success_url = 'auth_homepage'
111

  
112
    def get_template_names(self):
113
        if self.users and not 'create' in self.request.GET:
114
            return ['registration/registration_completion_choose.html']
115
        else:
116
            return ['registration/registration_completion_form.html']
117

  
118
    def get_success_url(self):
119
        try:
120
            redirect_url, next_field = app_settings.A2_REGISTRATION_REDIRECT
121
        except Exception:
122
            redirect_url = app_settings.A2_REGISTRATION_REDIRECT
123
            next_field = REDIRECT_FIELD_NAME
124

  
125
        if self.token and self.token.get(REDIRECT_FIELD_NAME):
126
            url = self.token[REDIRECT_FIELD_NAME]
127
            if redirect_url:
128
                url = make_url(redirect_url, params={next_field: url})
129
        else:
130
            if redirect_url:
131
                url = redirect_url
132
            else:
133
                url = make_url(self.success_url)
134
        return url
135

  
136
    def dispatch(self, request, *args, **kwargs):
137
        self.token = request.token
138
        self.authentication_method = self.token.get('authentication_method', 'email')
139
        self.email = request.token['email']
140
        if 'ou' in self.token:
141
            self.ou = OrganizationalUnit.objects.get(pk=self.token['ou'])
142
        else:
143
            self.ou = get_default_ou()
144
        self.users = User.objects.filter(email__iexact=self.email) \
145
            .order_by('date_joined')
146
        if self.ou:
147
            self.users = self.users.filter(ou=self.ou)
148
        self.email_is_unique = app_settings.A2_EMAIL_IS_UNIQUE \
149
            or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE
150
        if self.ou:
151
            self.email_is_unique |= self.ou.email_is_unique
152
        self.init_fields_labels_and_help_texts()
153
        # if registration is done during an SSO add the service to the registration event
154
        self.service = self.token.get(constants.SERVICE_FIELD_NAME)
155
        return super(RegistrationCompletionView, self) \
156
            .dispatch(request, *args, **kwargs)
157

  
158
    def init_fields_labels_and_help_texts(self):
159
        attributes = models.Attribute.objects.filter(
160
            asked_on_registration=True)
161
        default_fields = attributes.values_list('name', flat=True)
162
        required_fields = models.Attribute.objects.filter(required=True) \
163
            .values_list('name', flat=True)
164
        fields, labels = get_fields_and_labels(
165
            app_settings.A2_REGISTRATION_FIELDS,
166
            default_fields,
167
            app_settings.A2_REGISTRATION_REQUIRED_FIELDS,
168
            app_settings.A2_REQUIRED_FIELDS,
169
            models.Attribute.objects.filter(required=True).values_list('name', flat=True))
170
        help_texts = {}
171
        if app_settings.A2_REGISTRATION_FORM_USERNAME_LABEL:
172
            labels['username'] = app_settings.A2_REGISTRATION_FORM_USERNAME_LABEL
173
        if app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT:
174
            help_texts['username'] = \
175
                app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT
176
        required = list(app_settings.A2_REGISTRATION_REQUIRED_FIELDS) + \
177
            list(required_fields)
178
        if 'email' in fields:
179
            fields.remove('email')
180
        for field in self.token.get('skip_fields') or []:
181
            if field in fields:
182
                fields.remove(field)
183
        self.fields = fields
184
        self.labels = labels
185
        self.required = required
186
        self.help_texts = help_texts
187

  
188
    def get_form_class(self):
189
        if not self.token.get('valid_email', True):
190
            self.fields.append('email')
191
            self.required.append('email')
192
        form_class = RegistrationCompletionForm
193
        if self.token.get('no_password', False):
194
            form_class = RegistrationCompletionFormNoPassword
195
        form_class = forms.modelform_factory(self.model,
196
                                             form=form_class,
197
                                             fields=self.fields,
198
                                             labels=self.labels,
199
                                             required=self.required,
200
                                             help_texts=self.help_texts)
201
        if 'username' in self.fields and app_settings.A2_REGISTRATION_FORM_USERNAME_REGEX:
202
            # Keep existing field label and help_text
203
            old_field = form_class.base_fields['username']
204
            field = CharField(
205
                max_length=256,
206
                label=old_field.label,
207
                help_text=old_field.help_text,
208
                validators=[validators.UsernameValidator()])
209
            form_class = type('RegistrationForm', (form_class,), {'username': field})
210
        return form_class
211

  
212
    def get_form_kwargs(self, **kwargs):
213
        '''Initialize mail from token'''
214
        kwargs = super(RegistrationCompletionView, self).get_form_kwargs(**kwargs)
215
        if 'ou' in self.token:
216
            OU = get_ou_model()
217
            ou = get_object_or_404(OU, id=self.token['ou'])
218
        else:
219
            ou = get_default_ou()
220

  
221
        attributes = {'email': self.email, 'ou': ou}
222
        for key in self.token:
223
            if key in app_settings.A2_PRE_REGISTRATION_FIELDS:
224
                attributes[key] = self.token[key]
225
        logger.debug(u'attributes %s', attributes)
226

  
227
        prefilling_list = utils.accumulate_from_backends(self.request, 'registration_form_prefill')
228
        logger.debug(u'prefilling_list %s', prefilling_list)
229
        # Build a single meaningful prefilling with sets of values
230
        prefilling = {}
231
        for p in prefilling_list:
232
            for name, values in p.items():
233
                if name in self.fields:
234
                    prefilling.setdefault(name, set()).update(values)
235
        logger.debug(u'prefilling %s', prefilling)
236

  
237
        for name, values in prefilling.items():
238
            attributes[name] = ' '.join(values)
239
        logger.debug(u'attributes with prefilling %s', attributes)
240

  
241
        if self.token.get('user_id'):
242
            kwargs['instance'] = User.objects.get(id=self.token.get('user_id'))
243
        else:
244
            init_kwargs = {}
245
            for key in ('email', 'first_name', 'last_name', 'ou'):
246
                if key in attributes:
247
                    init_kwargs[key] = attributes[key]
248
            kwargs['instance'] = get_user_model()(**init_kwargs)
249

  
250
        return kwargs
251

  
252
    def get_form(self, form_class=None):
253
        form = super(RegistrationCompletionView, self).get_form(form_class=form_class)
254
        hooks.call_hooks('front_modify_form', self, form)
255
        return form
256

  
257
    def get_context_data(self, **kwargs):
258
        ctx = super(RegistrationCompletionView, self).get_context_data(**kwargs)
259
        ctx['token'] = self.token
260
        ctx['users'] = self.users
261
        ctx['email'] = self.email
262
        ctx['email_is_unique'] = self.email_is_unique
263
        ctx['create'] = 'create' in self.request.GET
264
        return ctx
265

  
266
    def get(self, request, *args, **kwargs):
267
        if len(self.users) == 1 and self.email_is_unique:
268
            # Found one user, EMAIL is unique, log her in
269
            simulate_authentication(request, self.users[0],
270
                                    method=self.authentication_method,
271
                                    service_slug=self.service)
272
            return redirect(request, self.get_success_url())
273
        confirm_data = self.token.get('confirm_data', False)
274

  
275
        if confirm_data == 'required':
276
            fields_to_confirm = self.required
277
        else:
278
            fields_to_confirm = self.fields
279
        if (all(field in self.token for field in fields_to_confirm)
280
                and (not confirm_data or confirm_data == 'required')):
281
            # We already have every fields
282
            form_kwargs = self.get_form_kwargs()
283
            form_class = self.get_form_class()
284
            data = self.token
285
            if 'password' in data:
286
                data['password1'] = data['password']
287
                data['password2'] = data['password']
288
                del data['password']
289
            form_kwargs['data'] = data
290
            form = form_class(**form_kwargs)
291
            if form.is_valid():
292
                user = form.save()
293
                return self.registration_success(request, user, form)
294
            self.get_form = lambda *args, **kwargs: form
295
        return super(RegistrationCompletionView, self).get(request, *args, **kwargs)
296

  
297
    def post(self, request, *args, **kwargs):
298
        if self.users and self.email_is_unique:
299
            # email is unique, users already exist, creating a new one is forbidden !
300
            return redirect(request, request.resolver_match.view_name, args=self.args,
301
                            kwargs=self.kwargs)
302
        if 'uid' in request.POST:
303
            uid = request.POST['uid']
304
            for user in self.users:
305
                if str(user.id) == uid:
306
                    simulate_authentication(request, user,
307
                                            method=self.authentication_method,
308
                                            service_slug=self.service)
309
                    return redirect(request, self.get_success_url())
310
        return super(RegistrationCompletionView, self).post(request, *args, **kwargs)
311

  
312
    def form_valid(self, form):
313

  
314
        # remove verified fields from form, this allows an authentication
315
        # method to provide verified data fields and to present it to the user,
316
        # while preventing the user to modify them.
317
        for av in models.AttributeValue.objects.with_owner(form.instance):
318
            if av.verified and av.attribute.name in form.fields:
319
                del form.fields[av.attribute.name]
320

  
321
        if ('email' in self.request.POST
322
                and (not 'email' in self.token or self.request.POST['email'] != self.token['email'])
323
                and not self.token.get('skip_email_check')):
324
            # If an email is submitted it must be validated or be the same as in the token
325
            data = form.cleaned_data
326
            data['no_password'] = self.token.get('no_password', False)
327
            utils.send_registration_mail(
328
                self.request,
329
                ou=self.ou,
330
                next_url=self.get_success_url(),
331
                **data)
332
            self.request.session['registered_email'] = form.cleaned_data['email']
333
            return redirect(self.request, 'registration_complete')
334
        super(RegistrationCompletionView, self).form_valid(form)
335
        return self.registration_success(self.request, form.instance, form)
336

  
337
    def registration_success(self, request, user, form):
338
        hooks.call_hooks('event', name='registration', user=user, form=form, view=self,
339
                         authentication_method=self.authentication_method,
340
                         token=request.token, service=self.service)
341
        simulate_authentication(request, user, method=self.authentication_method,
342
                                service_slug=self.service)
343
        messages.info(self.request, _('You have just created an account.'))
344
        self.send_registration_success_email(user)
345
        return redirect(request, self.get_success_url())
346

  
347
    def send_registration_success_email(self, user):
348
        if not user.email:
349
            return
350

  
351
        template_names = [
352
            'authentic2/registration_success'
353
        ]
354
        login_url = self.request.build_absolute_uri(settings.LOGIN_URL)
355
        utils.send_templated_mail(user, template_names=template_names,
356
                                  context={
357
                                      'user': user,
358
                                      'email': user.email,
359
                                      'site': self.request.get_host(),
360
                                      'login_url': login_url,
361
                                  },
362
                                  request=self.request)
363

  
364

  
365
class DeleteView(FormView):
366
    template_name = 'authentic2/accounts_delete.html'
367
    success_url = reverse_lazy('auth_logout')
368
    title = _('Delete account')
369

  
370
    def dispatch(self, request, *args, **kwargs):
371
        if not app_settings.A2_REGISTRATION_CAN_DELETE_ACCOUNT:
372
            return redirect(request, '..')
373
        return super(DeleteView, self).dispatch(request, *args, **kwargs)
374

  
375
    def post(self, request, *args, **kwargs):
376
        if 'cancel' in request.POST:
377
            return redirect(request, 'account_management')
378
        return super(DeleteView, self).post(request, *args, **kwargs)
379

  
380
    def get_form_class(self):
381
        if self.request.user.has_usable_password():
382
            return DeleteAccountForm
383
        return Form
384

  
385
    def get_form_kwargs(self, **kwargs):
386
        kwargs = super(DeleteView, self).get_form_kwargs(**kwargs)
387
        if self.request.user.has_usable_password():
388
            kwargs['user'] = self.request.user
389
        return kwargs
390

  
391
    def form_valid(self, form):
392
        utils.send_account_deletion_mail(self.request, self.request.user)
393
        models.DeletedUser.objects.delete_user(self.request.user)
394
        self.request.user.email += '#%d' % random.randint(1, 10000000)
395
        self.request.user.email_verified = False
396
        self.request.user.save(update_fields=['email', 'email_verified'])
397
        logger.info(u'deletion of account %s requested', self.request.user)
398
        hooks.call_hooks('event', name='delete-account', user=self.request.user)
399
        messages.info(self.request,
400
                      _('Your account has been scheduled for deletion. You cannot use it anymore.'))
401
        return super(DeleteView, self).form_valid(form)
402

  
403
registration_completion = valid_token(RegistrationCompletionView.as_view())
404

  
405

  
406
class RegistrationCompleteView(TemplateView):
407
    template_name = 'registration/registration_complete.html'
408

  
409
    def get_context_data(self, **kwargs):
410
        kwargs['next_url'] = utils.select_next_url(self.request, settings.LOGIN_REDIRECT_URL)
411
        return super(RegistrationCompleteView, self).get_context_data(
412
            account_activation_days=settings.ACCOUNT_ACTIVATION_DAYS,
413
            **kwargs)
414

  
415

  
416
registration_complete = RegistrationCompleteView.as_view()
src/authentic2/urls.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
1 17
from django.conf.urls import url, include
2 18
from django.conf import settings
3 19
from django.contrib import admin
20
from django.contrib.auth.decorators import login_required
21
from django.contrib.auth import views as dj_auth_views
4 22
from django.contrib.staticfiles.views import serve
23
from django.views.generic.base import TemplateView
5 24
from django.views.static import serve as media_serve
6 25

  
7
from . import app_settings, plugins, views
26
from . import plugins, views
8 27

  
9 28
admin.autodiscover()
10 29

  
11 30
handler500 = 'authentic2.views.server_error'
12 31

  
13
urlpatterns = [
14
    url(r'^$', views.homepage, name='auth_homepage'),
15
    url(r'test_redirect/$', views.test_redirect)
32
accounts_urlpatterns = [
33
    url(r'^activate/(?P<registration_token>[\w: -]+)/$',
34
        views.registration_completion, name='registration_activate'),
35
    url(r'^register/$',
36
        views.RegistrationView.as_view(),
37
        name='registration_register'),
38
    url(r'^register/complete/$',
39
        views.registration_complete,
40
        name='registration_complete'),
41
    url(r'^register/closed/$',
42
        TemplateView.as_view(template_name='registration/registration_closed.html'),
43
        name='registration_disallowed'),
44
    url(r'^delete/$',
45
        login_required(views.DeleteView.as_view()),
46
        name='delete_account'),
47
    url(r'^logged-in/$',
48
        views.logged_in,
49
        name='logged-in'),
50
    url(r'^edit/$',
51
        views.edit_profile,
52
        name='profile_edit'),
53
    url(r'^edit/(?P<scope>[-\w]+)/$',
54
        views.edit_profile,
55
        name='profile_edit_with_scope'),
56
    url(r'^change-email/$',
57
        views.email_change,
58
        name='email-change'),
59
    url(r'^change-email/verify/$',
60
        views.email_change_verify,
61
        name='email-change-verify'),
62
    url(r'^$',
63
        views.profile,
64
        name='account_management'),
65

  
66
    # Password change
67
    url(r'^password/change/$',
68
        views.password_change,
69
        name='password_change'),
70
    url(r'^password/change/done/$',
71
        dj_auth_views.password_change_done,
72
        name='password_change_done'),
73

  
74
    # Password reset
75
    url(r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
76
        views.password_reset_confirm,
77
        name='password_reset_confirm'),
78
    url(r'^password/reset/$',
79
        views.password_reset,
80
        name='password_reset'),
81

  
82
    url(r'^switch-back/$',
83
        views.switch_back,
84
        name='a2-switch-back'),
85

  
86
    # Legacy, only there to provide old view names to resolver
87
    url(r'^password/change/$',
88
        views.notimplemented_view,
89
        name='auth_password_change'),
90
    url(r'^password/change/done/$',
91
        views.notimplemented_view,
92
        name='auth_password_change_done'),
93

  
94
    url(r'^password/reset/confirm/(?P<uidb36>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
95
        views.notimplemented_view,
96
        name='auth_password_reset_confirm'),
97
    url(r'^password/reset/$',
98
        views.notimplemented_view,
99
        name='auth_password_reset'),
100
    url(r'^password/reset/complete/$',
101
        views.notimplemented_view,
102
        name='auth_password_reset_complete'),
103
    url(r'^password/reset/done/$',
104
        views.notimplemented_view,
105
        name='auth_password_reset_done'),
16 106
]
17 107

  
18
not_homepage_patterns = [
108
urlpatterns = [
109
    url(r'^$', views.homepage, name='auth_homepage'),
19 110
    url(r'^login/$', views.login, name='auth_login'),
20 111
    url(r'^logout/$', views.logout, name='auth_logout'),
21 112
    url(r'^redirect/(.*)', views.redirect, name='auth_redirect'),
22
    url(r'^accounts/', include('authentic2.profile_urls'))
23
]
24

  
25
not_homepage_patterns += [
26
    url(r'^accounts/', include(app_settings.A2_REGISTRATION_URLCONF)),
113
    url(r'^accounts/', include(accounts_urlpatterns)),
27 114
    url(r'^admin/', include(admin.site.urls)),
28 115
    url(r'^idp/', include('authentic2.idp.urls')),
29 116
    url(r'^manage/', include('authentic2.manager.urls')),
30
    url(r'^api/', include('authentic2.api_urls'))
117
    url(r'^api/', include('authentic2.api_urls')),
118
    url(r'^test_redirect/$', views.test_redirect),
31 119
]
32 120

  
33

  
34
urlpatterns += not_homepage_patterns
35

  
36 121
try:
37 122
    if getattr(settings, 'DISCO_SERVICE', False):
38 123
        urlpatterns += [
39 124
            (r'^disco_service/', include('disco_service.disco_responder')),
40 125
        ]
41
except:
126
except Exception:
42 127
    pass
43 128

  
44 129
if settings.DEBUG:
......
46 131
        url(r'^static/(?P<path>.*)$', serve)
47 132
    ]
48 133
    urlpatterns += [
49
        url(r'^media/(?P<path>.*)$', media_serve, {
50
        'document_root': settings.MEDIA_ROOT})
134
        url(r'^media/(?P<path>.*)$', media_serve,
135
            {
136
                'document_root': settings.MEDIA_ROOT
137
            })
51 138
    ]
52 139

  
53 140
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
src/authentic2/views.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import collections
1 18
import logging
2
from authentic2.compat_lasso import lasso
3
import requests
19
import random
4 20
import re
5
import collections
6

  
7 21

  
8 22
from django.conf import settings
9
from django.shortcuts import render_to_response, render
10
from django.template.loader import render_to_string, select_template
23
from django.shortcuts import render_to_response, render, get_object_or_404
24
from django.template.loader import render_to_string
11 25
from django.views.generic.edit import UpdateView, FormView
12
from django.views.generic import RedirectView, TemplateView
26
from django.views.generic import TemplateView
13 27
from django.views.generic.base import View
14 28
from django.contrib.auth import SESSION_KEY
15 29
from django import http, shortcuts
16
from django.core import mail, signing
30
from django.core import signing
17 31
from django.core.urlresolvers import reverse
18 32
from django.core.exceptions import ValidationError
19 33
from django.contrib import messages
......
21 35
from django.utils.translation import ugettext as _
22 36
from django.contrib.auth import logout as auth_logout
23 37
from django.contrib.auth import REDIRECT_FIELD_NAME
24
from django.http import (HttpResponseRedirect, HttpResponseForbidden,
25
    HttpResponse)
26
from django.core.exceptions import PermissionDenied
38
from django.contrib.auth.views import password_change as dj_password_change
39
from django.http import (HttpResponseRedirect, HttpResponseForbidden, HttpResponse)
27 40
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
28 41
from django.views.decorators.cache import never_cache
42
from django.views.decorators.debug import sensitive_post_parameters
29 43
from django.contrib.auth.decorators import login_required
30 44
from django.db.models.fields import FieldDoesNotExist
31 45
from django.db.models.query import Q
32

  
33
# FIXME: this decorator has nothing to do with an idp, should be moved in the
34
# a2 package
35
# FIXME: this constant should be moved in the a2 package
36

  
37

  
38
from . import (utils, app_settings, forms, compat, decorators, constants, models, cbv, hooks)
46
from django.contrib.auth import get_user_model, authenticate
47
from django.http import Http404
48
from django.utils.http import urlsafe_base64_decode
49
from django.views.generic.edit import CreateView
50
from django.forms import CharField, Form
51
from django.core.urlresolvers import reverse_lazy
52
from django.http import HttpResponseBadRequest
53

  
54
from . import (utils, app_settings, forms, compat, decorators, constants,
55
               models, cbv, hooks, validators)
56
from .a2_rbac.utils import get_default_ou
57
from .a2_rbac.models import OrganizationalUnit as OU
58
from .forms import (
59
    passwords as passwords_forms,
60
    registration as registration_forms,
61
    profile as profile_forms)
39 62

  
40 63

  
41 64
logger = logging.getLogger(__name__)
......
47 70
    if not next.startswith('http'):
48 71
        next = '/%s%s' % (request.get_host(), next)
49 72
    logging.info('Redirect to %r' % next)
50
    return render_to_response(template_name, { 'next': next })
73
    return render_to_response(template_name, {'next': next})
51 74

  
52 75

  
53 76
def server_error(request, template_name='500.html'):
......
100 123
        else:
101 124
            default_fields = list(attributes.values_list('name', flat=True))
102 125
        fields, labels = utils.get_fields_and_labels(
103
                editable_profile_fields,
104
                default_fields)
126
            editable_profile_fields, default_fields)
105 127
        if scopes:
106 128
            # restrict fields to those in the scopes
107 129
            fields = [field for field in fields if field in default_fields]
......
115 137
        fields, labels = self.get_fields(scopes=scopes)
116 138
        # Email must be edited through the change email view, as it needs validation
117 139
        fields = [field for field in fields if field != 'email']
118
        return forms.modelform_factory(compat.get_user_model(), fields=fields,
119
                                       labels=labels,
120
                                       form=forms.EditProfileForm)
140
        return profile_forms.modelform_factory(
141
            compat.get_user_model(), fields=fields,
142
            labels=labels,
143
            form=profile_forms.EditProfileForm)
121 144

  
122 145
    def get_object(self):
123 146
        return self.request.user
......
173 196

  
174 197
    def get_form_class(self):
175 198
        if self.request.user.has_usable_password():
176
            return forms.EmailChangeForm
177
        return forms.EmailChangeFormNoPassword
199
            return profile_forms.EmailChangeForm
200
        return profile_forms.EmailChangeFormNoPassword
178 201

  
179 202
    def get_form_kwargs(self):
180 203
        kwargs = super(EmailChangeView, self).get_form_kwargs()
......
225 248
                user.email = email
226 249
                user.email_verified = True
227 250
                user.save()
228
                messages.info(request, _('your request for changing your email for {0} '
229
                    'is successful').format(email))
230
                logging.getLogger(__name__).info('user %s changed its email '
231
                                                 'from %s to %s', user,
232
                                                 old_email, email)
251
                messages.info(request,
252
                              _('your request for changing your email for {0} is successful').format(email))
253
                logging.getLogger(__name__).info(
254
                    'user %s changed its email from %s to %s', user, old_email, email)
233 255
                hooks.call_hooks('event', name='change-email-confirm', user=user, email=email)
234 256
            except signing.SignatureExpired:
235
                messages.error(request, _('your request for changing your email is too '
236
                    'old, try again'))
257
                messages.error(request,
258
                               _('your request for changing your email is too old, try again'))
237 259
            except signing.BadSignature:
238
                messages.error(request, _('your request for changing your email is '
239
                    'invalid, try again'))
260
                messages.error(request,
261
                               _('your request for changing your email is invalid, try again'))
240 262
            except ValueError:
241
                messages.error(request, _('your request for changing your email was not '
242
                    'on this site, try again'))
263
                messages.error(request,
264
                               _('your request for changing your email was not on this site, try again'))
243 265
            except User.DoesNotExist:
244
                messages.error(request, _('your request for changing your email is for '
245
                    'an unknown user, try again'))
266
                messages.error(request,
267
                               _('your request for changing your email is for an unknown user, try again'))
246 268
            except ValidationError as e:
247 269
                messages.error(request, e.message)
248 270
            else:
......
264 286

  
265 287
    # redirect user to homepage if already connected, if setting
266 288
    # A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE is True
267
    if (request.user.is_authenticated() and
268
            app_settings.A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE):
289
    if (request.user.is_authenticated()
290
            and app_settings.A2_LOGIN_REDIRECT_AUTHENTICATED_USERS_TO_HOMEPAGE):
269 291
        return utils.redirect(request, 'auth_homepage')
270 292

  
271 293
    redirect_to = request.GET.get(redirect_field_name)
......
308 330
            form_class = authenticator.form()
309 331
            submit_name = 'submit-%s' % fid
310 332
            block = {
311
                    'id': fid,
312
                    'name': name,
313
                    'authenticator': authenticator
333
                'id': fid,
334
                'name': name,
335
                'authenticator': authenticator
314 336
            }
315 337
            if request.method == 'POST' and submit_name in request.POST:
316 338
                form = form_class(data=request.POST)
......
322 344
            else:
323 345
                block['form'] = form_class()
324 346
            blocks.append(block)
325
        else: # New frontends API
347
        else:  # New frontends API
326 348
            parameters = {'request': request,
327 349
                          'context': context}
328 350
            block = utils.get_authenticator_method(authenticator, 'login', parameters)
......
337 359
        else:
338 360
            blocks[-1]['is_hidden'] = False
339 361

  
340

  
341 362
    # Old frontends API
342 363
    for block in blocks:
343 364
        fid = block['id']
344
        if not 'form' in block:
365
        if 'form' not in block:
345 366
            continue
346 367
        authenticator = block['authenticator']
347 368
        context.update({
348
                'submit_name': 'submit-%s' % fid,
349
                redirect_field_name: redirect_to,
350
                'form': block['form']
369
            'submit_name': 'submit-%s' % fid,
370
            redirect_field_name: redirect_to,
371
            'form': block['form']
351 372
        })
352 373
        if hasattr(authenticator, 'get_context'):
353 374
            context.update(authenticator.get_context())
354 375
        sub_template_name = authenticator.template()
355
        block['content'] = render_to_string(
356
                sub_template_name, context,
357
                request=request)
376
        block['content'] = render_to_string(sub_template_name, context, request=request)
358 377

  
359 378
    request.session.set_test_cookie()
360 379

  
......
423 442
            for field_name in getattr(request.user, 'USER_PROFILE', []):
424 443
                if field_name not in field_names:
425 444
                    field_names.append(field_name)
426
            qs = models.Attribute.objects.filter(Q(user_editable=True)|Q(user_visible=True))
445
            qs = models.Attribute.objects.filter(Q(user_editable=True) | Q(user_visible=True))
427 446
            qs = qs.values_list('name', flat=True)
428 447
            for field_name in qs:
429 448
                if field_name not in field_names:
......
479 498
        # Credentials management
480 499
        parameters = {'request': request,
481 500
                      'context': context}
482
        profiles = [utils.get_authenticator_method(frontend, 'profile', parameters)
483
                            for frontend in frontends]
501
        profiles = [utils.get_authenticator_method(frontend, 'profile', parameters) for frontend in frontends]
484 502
        # Old frontends data structure for templates
485 503
        blocks = [block['content'] for block in profiles if block]
486 504
        # New frontends data structure for templates
......
510 528

  
511 529
profile = login_required(ProfileView.as_view())
512 530

  
531

  
513 532
def logout_list(request):
514 533
    '''Return logout links from idp backends'''
515 534
    return utils.accumulate_from_backends(request, 'logout_list')
516 535

  
536

  
517 537
def redirect_logout_list(request):
518 538
    '''Return redirect logout links from idp backends'''
519 539
    return utils.accumulate_from_backends(request, 'redirect_logout_list')
520 540

  
521
def logout(request, next_url=None, default_next_url='auth_homepage',
522
        redirect_field_name=REDIRECT_FIELD_NAME,
523
        template='authentic2/logout.html', do_local=True, check_referer=True):
541

  
542
def logout(request,
543
           next_url=None,
544
           default_next_url='auth_homepage',
545
           redirect_field_name=REDIRECT_FIELD_NAME,
546
           template='authentic2/logout.html',
547
           do_local=True,
548
           check_referer=True):
524 549
    '''Logout first check if a logout request is authorized, i.e.
525 550
       that logout was done using a POST with CSRF token or with a GET
526 551
       from the same site.
......
530 555
    '''
531 556
    logger = logging.getLogger(__name__)
532 557
    default_next_url = utils.make_url(default_next_url)
533
    next_url = next_url or request.GET.get(redirect_field_name,
534
            default_next_url)
558
    next_url = next_url or request.GET.get(redirect_field_name, default_next_url)
535 559
    ctx = {}
536 560
    ctx['next_url'] = next_url
537 561
    ctx['redir_timeout'] = 60
......
541 565
            return render(request, 'authentic2/logout_confirm.html', ctx)
542 566
        do_local = do_local and 'local' in request.GET
543 567
        if not do_local:
544
            l = logout_list(request)
545
            if l:
568
            fragments = logout_list(request)
569
            if fragments:
546 570
                # Full logout with iframes
547 571
                next_url = utils.make_url('auth_logout', params={
548 572
                    'local': 'ok',
549 573
                    REDIRECT_FIELD_NAME: next_url})
550 574
                ctx['next_url'] = next_url
551
                ctx['logout_list'] = l
575
                ctx['logout_list'] = fragments
552 576
                ctx['message'] = _('Logging out from all your services')
553 577
                return render(request, template, ctx)
554 578
        # Get redirection targets for full logout with redirections
......
613 637

  
614 638
logged_in = never_cache(LoggedInView.as_view())
615 639

  
640

  
616 641
def csrf_failure_view(request, reason=""):
617 642
    messages.warning(request, _('The page is out of date, it was reloaded for you'))
618 643
    return HttpResponseRedirect(request.get_full_path())
619 644

  
645

  
620 646
def test_redirect(request):
621 647
    next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL)
622 648
    messages.info(request, 'Une info')
623 649
    messages.warning(request, 'Un warning')
624 650
    messages.error(request, 'Une erreur')
625 651
    return HttpResponseRedirect(next_url)
652

  
653

  
654
class PasswordResetView(cbv.NextURLViewMixin, FormView):
655
    '''Ask for an email and send a password reset link by mail'''
656
    form_class = passwords_forms.PasswordResetForm
657
    title = _('Password Reset')
658

  
659
    def get_template_names(self):
660
        return [
661
            'authentic2/password_reset_form.html',
662
            'registration/password_reset_form.html',
663
        ]
664

  
665
    def get_form_kwargs(self, **kwargs):
666
        kwargs = super(PasswordResetView, self).get_form_kwargs(**kwargs)
667
        initial = kwargs.setdefault('initial', {})
668
        initial['next_url'] = self.request.GET.get(REDIRECT_FIELD_NAME, '')
669
        return kwargs
670

  
671
    def get_context_data(self, **kwargs):
672
        ctx = super(PasswordResetView, self).get_context_data(**kwargs)
673
        if app_settings.A2_USER_CAN_RESET_PASSWORD is False:
674
            raise Http404('Password reset is not allowed.')
675
        ctx['title'] = _('Password reset')
676
        return ctx
677

  
678
    def form_valid(self, form):
679
        form.save()
680
        # return to next URL
681
        messages.info(self.request, _('If your email address exists in our '
682
                                      'database, you will receive an email '
683
                                      'containing instructions to reset '
684
                                      'your password'))
685
        return super(PasswordResetView, self).form_valid(form)
686

  
687
password_reset = PasswordResetView.as_view()
688

  
689

  
690
class PasswordResetConfirmView(cbv.RedirectToNextURLViewMixin, FormView):
691
    '''Validate password reset link, show a set password form and login
692
       the user.
693
    '''
694
    form_class = passwords_forms.SetPasswordForm
695
    title = _('Password Reset')
696

  
697
    def get_template_names(self):
698
        return [
699
            'registration/password_reset_confirm.html',
700
            'authentic2/password_reset_confirm.html',
701
        ]
702

  
703
    def dispatch(self, request, *args, **kwargs):
704
        validlink = True
705
        uidb64 = kwargs['uidb64']
706
        self.token = token = kwargs['token']
707

  
708
        UserModel = get_user_model()
709
        # checked by URLconf
710
        assert uidb64 is not None and token is not None
711
        try:
712
            uid = urlsafe_base64_decode(uidb64)
713
            # use authenticate to eventually get an LDAPUser
714
            self.user = authenticate(user=UserModel._default_manager.get(pk=uid))
715
        except (TypeError, ValueError, OverflowError,
716
                UserModel.DoesNotExist):
717
            validlink = False
718
            messages.warning(request, _('User not found'))
719

  
720
        if validlink and not compat.default_token_generator.check_token(self.user, token):
721
            validlink = False
722
            messages.warning(request, _('You reset password link is invalid or has expired'))
723
        if not validlink:
724
            return utils.redirect(request, self.get_success_url())
725
        can_reset_password = utils.get_user_flag(user=self.user,
726
                                                 name='can_reset_password',
727
                                                 default=self.user.has_usable_password())
728
        if not can_reset_password:
729
            messages.warning(
730
                request,
731
                _('It\'s not possible to reset your password. Please contact an administrator.'))
732
            return utils.redirect(request, self.get_success_url())
733
        return super(PasswordResetConfirmView, self).dispatch(request, *args,
734
                                                              **kwargs)
735

  
736
    def get_context_data(self, **kwargs):
737
        ctx = super(PasswordResetConfirmView, self).get_context_data(**kwargs)
738
        # compatibility with existing templates !
739
        ctx['title'] = _('Enter new password')
740
        ctx['validlink'] = True
741
        return ctx
742

  
743
    def get_form_kwargs(self):
744
        kwargs = super(PasswordResetConfirmView, self).get_form_kwargs()
745
        kwargs['user'] = self.user
746
        return kwargs
747

  
748
    def form_valid(self, form):
749
        # Changing password by mail validate the email
750
        form.user.email_verified = True
751
        form.save()
752
        hooks.call_hooks('event', name='password-reset-confirm', user=form.user, token=self.token,
753
                         form=form)
754
        logger.info(u'user %s resetted its password with token %r...',
755
                    self.user, self.token[:9])
756
        return self.finish()
757

  
758
    def finish(self):
759
        return utils.simulate_authentication(self.request, self.user, 'email')
760

  
761
password_reset_confirm = PasswordResetConfirmView.as_view()
762

  
763

  
764
def switch_back(request):
765
    return utils.switch_back(request)
766

  
767
logger = logging.getLogger(__name__)
768

  
769
User = compat.get_user_model()
770

  
771

  
772
def valid_token(method):
773
    def f(request, *args, **kwargs):
774
        try:
775
            request.token = signing.loads(kwargs['registration_token'].replace(' ', ''),
776
                                          max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24)
777
        except signing.SignatureExpired:
778
            messages.warning(request, _('Your activation key is expired'))
779
            return utils.redirect(request, 'registration_register')
780
        except signing.BadSignature:
781
            messages.warning(request, _('Activation failed'))
782
            return utils.redirect(request, 'registration_register')
783
        return method(request, *args, **kwargs)
784
    return f
785

  
786

  
787
class BaseRegistrationView(FormView):
788
    form_class = registration_forms.RegistrationForm
789
    template_name = 'registration/registration_form.html'
790
    title = _('Registration')
791

  
792
    def dispatch(self, request, *args, **kwargs):
793
        if not getattr(settings, 'REGISTRATION_OPEN', True):
794
            raise Http404('Registration is not open.')
795
        self.token = {}
796
        self.ou = get_default_ou()
797
        # load pre-filled values
798
        if request.GET.get('token'):
799
            try:
800
                self.token = signing.loads(
801
                    request.GET.get('token'),
802
                    max_age=settings.ACCOUNT_ACTIVATION_DAYS * 3600 * 24)
803
            except (TypeError, ValueError, signing.BadSignature) as e:
804
                logger.warning(u'registration_view: invalid token: %s', e)
805
                return HttpResponseBadRequest('invalid token', content_type='text/plain')
806
            if 'ou' in self.token:
807
                self.ou = OU.objects.get(pk=self.token['ou'])
808
        self.next_url = self.token.pop(REDIRECT_FIELD_NAME, utils.select_next_url(request, None))
809
        return super(BaseRegistrationView, self).dispatch(request, *args, **kwargs)
810

  
811
    def form_valid(self, form):
812
        email = form.cleaned_data.pop('email')
813
        for field in form.cleaned_data:
814
            self.token[field] = form.cleaned_data[field]
815

  
816
        # propagate service to the registration completion view
817
        if constants.SERVICE_FIELD_NAME in self.request.GET:
818
            self.token[constants.SERVICE_FIELD_NAME] = \
819
                self.request.GET[constants.SERVICE_FIELD_NAME]
820

  
821
        self.token.pop(REDIRECT_FIELD_NAME, None)
822
        self.token.pop('email', None)
823

  
824
        utils.send_registration_mail(self.request, email, next_url=self.next_url,
825
                                     ou=self.ou, **self.token)
826
        self.request.session['registered_email'] = email
827
        return utils.redirect(self.request, 'registration_complete', params={REDIRECT_FIELD_NAME: self.next_url})
828

  
829
    def get_context_data(self, **kwargs):
830
        context = super(BaseRegistrationView, self).get_context_data(**kwargs)
831
        parameters = {'request': self.request,
832
                      'context': context}
833
        blocks = [utils.get_authenticator_method(authenticator, 'registration', parameters)
834
                  for authenticator in utils.get_backends('AUTH_FRONTENDS')]
835
        context['frontends'] = collections.OrderedDict((block['id'], block)
836
                                                       for block in blocks if block)
837
        return context
838

  
839

  
840
class RegistrationView(cbv.ValidateCSRFMixin, BaseRegistrationView):
841
    pass
842

  
843

  
844
class RegistrationCompletionView(CreateView):
845
    model = get_user_model()
846
    success_url = 'auth_homepage'
847

  
848
    def get_template_names(self):
849
        if self.users and 'create' not in self.request.GET:
850
            return ['registration/registration_completion_choose.html']
851
        else:
852
            return ['registration/registration_completion_form.html']
853

  
854
    def get_success_url(self):
855
        try:
856
            redirect_url, next_field = app_settings.A2_REGISTRATION_REDIRECT
857
        except Exception:
858
            redirect_url = app_settings.A2_REGISTRATION_REDIRECT
859
            next_field = REDIRECT_FIELD_NAME
860

  
861
        if self.token and self.token.get(REDIRECT_FIELD_NAME):
862
            url = self.token[REDIRECT_FIELD_NAME]
863
            if redirect_url:
864
                url = utils.make_url(redirect_url, params={next_field: url})
865
        else:
866
            if redirect_url:
867
                url = redirect_url
868
            else:
869
                url = utils.make_url(self.success_url)
870
        return url
871

  
872
    def dispatch(self, request, *args, **kwargs):
873
        self.token = request.token
874
        self.authentication_method = self.token.get('authentication_method', 'email')
875
        self.email = request.token['email']
876
        if 'ou' in self.token:
877
            self.ou = OU.objects.get(pk=self.token['ou'])
878
        else:
879
            self.ou = get_default_ou()
880
        self.users = User.objects.filter(email__iexact=self.email) \
881
            .order_by('date_joined')
882
        if self.ou:
883
            self.users = self.users.filter(ou=self.ou)
884
        self.email_is_unique = app_settings.A2_EMAIL_IS_UNIQUE \
885
            or app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE
886
        if self.ou:
887
            self.email_is_unique |= self.ou.email_is_unique
888
        self.init_fields_labels_and_help_texts()
889
        # if registration is done during an SSO add the service to the registration event
890
        self.service = self.token.get(constants.SERVICE_FIELD_NAME)
891
        return super(RegistrationCompletionView, self) \
892
            .dispatch(request, *args, **kwargs)
893

  
894
    def init_fields_labels_and_help_texts(self):
895
        attributes = models.Attribute.objects.filter(
896
            asked_on_registration=True)
897
        default_fields = attributes.values_list('name', flat=True)
898
        required_fields = models.Attribute.objects.filter(required=True) \
899
            .values_list('name', flat=True)
900
        fields, labels = utils.get_fields_and_labels(
901
            app_settings.A2_REGISTRATION_FIELDS,
902
            default_fields,
903
            app_settings.A2_REGISTRATION_REQUIRED_FIELDS,
904
            app_settings.A2_REQUIRED_FIELDS,
905
            models.Attribute.objects.filter(required=True).values_list('name', flat=True))
906
        help_texts = {}
907
        if app_settings.A2_REGISTRATION_FORM_USERNAME_LABEL:
908
            labels['username'] = app_settings.A2_REGISTRATION_FORM_USERNAME_LABEL
909
        if app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT:
910
            help_texts['username'] = \
911
                app_settings.A2_REGISTRATION_FORM_USERNAME_HELP_TEXT
912
        required = list(app_settings.A2_REGISTRATION_REQUIRED_FIELDS) + \
913
            list(required_fields)
914
        if 'email' in fields:
915
            fields.remove('email')
916
        for field in self.token.get('skip_fields') or []:
917
            if field in fields:
918
                fields.remove(field)
919
        self.fields = fields
920
        self.labels = labels
921
        self.required = required
922
        self.help_texts = help_texts
923

  
924
    def get_form_class(self):
925
        if not self.token.get('valid_email', True):
926
            self.fields.append('email')
927
            self.required.append('email')
928
        form_class = registration_forms.RegistrationCompletionForm
929
        if self.token.get('no_password', False):
930
            form_class = registration_forms.RegistrationCompletionFormNoPassword
931
        form_class = profile_forms.modelform_factory(
932
            self.model,
933
            form=form_class,
934
            fields=self.fields,
935
            labels=self.labels,
936
            required=self.required,
937
            help_texts=self.help_texts)
938
        if 'username' in self.fields and app_settings.A2_REGISTRATION_FORM_USERNAME_REGEX:
939
            # Keep existing field label and help_text
940
            old_field = form_class.base_fields['username']
941
            field = CharField(
942
                max_length=256,
943
                label=old_field.label,
944
                help_text=old_field.help_text,
945
                validators=[validators.UsernameValidator()])
946
            form_class = type('RegistrationForm', (form_class,), {'username': field})
947
        return form_class
948

  
949
    def get_form_kwargs(self, **kwargs):
950
        '''Initialize mail from token'''
951
        kwargs = super(RegistrationCompletionView, self).get_form_kwargs(**kwargs)
952
        if 'ou' in self.token:
953
            ou = get_object_or_404(OU, id=self.token['ou'])
954
        else:
955
            ou = get_default_ou()
956

  
957
        attributes = {'email': self.email, 'ou': ou}
958
        for key in self.token:
959
            if key in app_settings.A2_PRE_REGISTRATION_FIELDS:
960
                attributes[key] = self.token[key]
961
        logger.debug(u'attributes %s', attributes)
962

  
963
        prefilling_list = utils.accumulate_from_backends(self.request, 'registration_form_prefill')
964
        logger.debug(u'prefilling_list %s', prefilling_list)
965
        # Build a single meaningful prefilling with sets of values
966
        prefilling = {}
967
        for p in prefilling_list:
968
            for name, values in p.items():
969
                if name in self.fields:
970
                    prefilling.setdefault(name, set()).update(values)
971
        logger.debug(u'prefilling %s', prefilling)
972

  
973
        for name, values in prefilling.items():
974
            attributes[name] = ' '.join(values)
975
        logger.debug(u'attributes with prefilling %s', attributes)
976

  
977
        if self.token.get('user_id'):
978
            kwargs['instance'] = User.objects.get(id=self.token.get('user_id'))
979
        else:
980
            init_kwargs = {}
981
            for key in ('email', 'first_name', 'last_name', 'ou'):
982
                if key in attributes:
983
                    init_kwargs[key] = attributes[key]
984
            kwargs['instance'] = get_user_model()(**init_kwargs)
985

  
986
        return kwargs
987

  
988
    def get_form(self, form_class=None):
989
        form = super(RegistrationCompletionView, self).get_form(form_class=form_class)
990
        hooks.call_hooks('front_modify_form', self, form)
991
        return form
992

  
993
    def get_context_data(self, **kwargs):
994
        ctx = super(RegistrationCompletionView, self).get_context_data(**kwargs)
995
        ctx['token'] = self.token
996
        ctx['users'] = self.users
997
        ctx['email'] = self.email
998
        ctx['email_is_unique'] = self.email_is_unique
999
        ctx['create'] = 'create' in self.request.GET
1000
        return ctx
1001

  
1002
    def get(self, request, *args, **kwargs):
1003
        if len(self.users) == 1 and self.email_is_unique:
1004
            # Found one user, EMAIL is unique, log her in
1005
            utils.simulate_authentication(
1006
                request, self.users[0],
1007
                method=self.authentication_method,
1008
                service_slug=self.service)
1009
            return utils.redirect(request, self.get_success_url())
1010
        confirm_data = self.token.get('confirm_data', False)
1011

  
1012
        if confirm_data == 'required':
1013
            fields_to_confirm = self.required
1014
        else:
1015
            fields_to_confirm = self.fields
1016
        if (all(field in self.token for field in fields_to_confirm)
1017
                and (not confirm_data or confirm_data == 'required')):
1018
            # We already have every fields
1019
            form_kwargs = self.get_form_kwargs()
1020
            form_class = self.get_form_class()
1021
            data = self.token
1022
            if 'password' in data:
1023
                data['password1'] = data['password']
1024
                data['password2'] = data['password']
1025
                del data['password']
1026
            form_kwargs['data'] = data
1027
            form = form_class(**form_kwargs)
1028
            if form.is_valid():
1029
                user = form.save()
1030
                return self.registration_success(request, user, form)
1031
            self.get_form = lambda *args, **kwargs: form
1032
        return super(RegistrationCompletionView, self).get(request, *args, **kwargs)
1033

  
1034
    def post(self, request, *args, **kwargs):
1035
        if self.users and self.email_is_unique:
1036
            # email is unique, users already exist, creating a new one is forbidden !
1037
            return utils.redirect(
1038
                request, request.resolver_match.view_name, args=self.args,
1039
                kwargs=self.kwargs)
1040
        if 'uid' in request.POST:
1041
            uid = request.POST['uid']
1042
            for user in self.users:
1043
                if str(user.id) == uid:
1044
                    utils.simulate_authentication(
1045
                        request, user,
1046
                        method=self.authentication_method,
1047
                        service_slug=self.service)
1048
                    return utils.redirect(request, self.get_success_url())
1049
        return super(RegistrationCompletionView, self).post(request, *args, **kwargs)
1050

  
1051
    def form_valid(self, form):
1052

  
1053
        # remove verified fields from form, this allows an authentication
1054
        # method to provide verified data fields and to present it to the user,
1055
        # while preventing the user to modify them.
1056
        for av in models.AttributeValue.objects.with_owner(form.instance):
1057
            if av.verified and av.attribute.name in form.fields:
1058
                del form.fields[av.attribute.name]
1059

  
1060
        if ('email' in self.request.POST
1061
                and ('email' not in self.token or self.request.POST['email'] != self.token['email'])
1062
                and not self.token.get('skip_email_check')):
1063
            # If an email is submitted it must be validated or be the same as in the token
1064
            data = form.cleaned_data
1065
            data['no_password'] = self.token.get('no_password', False)
1066
            utils.send_registration_mail(
1067
                self.request,
1068
                ou=self.ou,
1069
                next_url=self.get_success_url(),
1070
                **data)
1071
            self.request.session['registered_email'] = form.cleaned_data['email']
1072
            return utils.redirect(self.request, 'registration_complete')
1073
        super(RegistrationCompletionView, self).form_valid(form)
1074
        return self.registration_success(self.request, form.instance, form)
1075

  
1076
    def registration_success(self, request, user, form):
1077
        hooks.call_hooks('event', name='registration', user=user, form=form, view=self,
1078
                         authentication_method=self.authentication_method,
1079
                         token=request.token, service=self.service)
1080
        utils.simulate_authentication(
1081
            request, user,
1082
            method=self.authentication_method,
1083
            service_slug=self.service)
1084
        messages.info(self.request, _('You have just created an account.'))
1085
        self.send_registration_success_email(user)
1086
        return utils.redirect(request, self.get_success_url())
1087

  
1088
    def send_registration_success_email(self, user):
1089
        if not user.email:
1090
            return
1091

  
1092
        template_names = [
1093
            'authentic2/registration_success'
1094
        ]
1095
        login_url = self.request.build_absolute_uri(settings.LOGIN_URL)
1096
        utils.send_templated_mail(user, template_names=template_names,
1097
                                  context={
1098
                                      'user': user,
1099
                                      'email': user.email,
1100
                                      'site': self.request.get_host(),
1101
                                      'login_url': login_url,
1102
                                  },
1103
                                  request=self.request)
1104

  
1105

  
1106
class DeleteView(FormView):
1107
    template_name = 'authentic2/accounts_delete.html'
1108
    success_url = reverse_lazy('auth_logout')
1109
    title = _('Delete account')
1110

  
1111
    def dispatch(self, request, *args, **kwargs):
1112
        if not app_settings.A2_REGISTRATION_CAN_DELETE_ACCOUNT:
1113
            return utils.redirect(request, '..')
1114
        return super(DeleteView, self).dispatch(request, *args, **kwargs)
1115

  
1116
    def post(self, request, *args, **kwargs):
1117
        if 'cancel' in request.POST:
1118
            return utils.redirect(request, 'account_management')
1119
        return super(DeleteView, self).post(request, *args, **kwargs)
1120

  
1121
    def get_form_class(self):
1122
        if self.request.user.has_usable_password():
1123
            return profile_forms.DeleteAccountForm
1124
        return Form
1125

  
1126
    def get_form_kwargs(self, **kwargs):
1127
        kwargs = super(DeleteView, self).get_form_kwargs(**kwargs)
1128
        if self.request.user.has_usable_password():
1129
            kwargs['user'] = self.request.user
1130
        return kwargs
1131

  
1132
    def form_valid(self, form):
1133
        utils.send_account_deletion_mail(self.request, self.request.user)
1134
        models.DeletedUser.objects.delete_user(self.request.user)
1135
        self.request.user.email += '#%d' % random.randint(1, 10000000)
1136
        self.request.user.email_verified = False
1137
        self.request.user.save(update_fields=['email', 'email_verified'])
1138
        logger.info(u'deletion of account %s requested', self.request.user)
1139
        hooks.call_hooks('event', name='delete-account', user=self.request.user)
1140
        messages.info(self.request,
1141
                      _('Your account has been scheduled for deletion. You cannot use it anymore.'))
1142
        return super(DeleteView, self).form_valid(form)
1143

  
1144
registration_completion = valid_token(RegistrationCompletionView.as_view())
1145

  
1146

  
1147
class RegistrationCompleteView(TemplateView):
1148
    template_name = 'registration/registration_complete.html'
1149

  
1150
    def get_context_data(self, **kwargs):
1151
        kwargs['next_url'] = utils.select_next_url(self.request, settings.LOGIN_REDIRECT_URL)
1152
        return super(RegistrationCompleteView, self).get_context_data(
1153
            account_activation_days=settings.ACCOUNT_ACTIVATION_DAYS,
1154
            **kwargs)
1155

  
1156

  
1157
registration_complete = RegistrationCompleteView.as_view()
1158

  
1159

  
1160
@sensitive_post_parameters()
1161
@login_required
1162
@decorators.setting_enabled('A2_REGISTRATION_CAN_CHANGE_PASSWORD')
1163
def password_change(request, *args, **kwargs):
1164
    kwargs['password_change_form'] = passwords_forms.PasswordChangeForm
1165
    post_change_redirect = kwargs.pop('post_change_redirect', None)
1166
    if 'next_url' in request.POST and request.POST['next_url']:
1167
        post_change_redirect = request.POST['next_url']
1168
    elif REDIRECT_FIELD_NAME in request.GET:
1169
        post_change_redirect = request.GET[REDIRECT_FIELD_NAME]
1170
    elif post_change_redirect is None:
1171
        post_change_redirect = reverse('account_management')
1172
    if not utils.user_can_change_password(request=request):
1173
        messages.warning(request, _('Password change is forbidden'))
1174
        return utils.redirect(request, post_change_redirect)
1175
    if 'cancel' in request.POST:
1176
        return utils.redirect(request, post_change_redirect)
1177
    kwargs['post_change_redirect'] = post_change_redirect
1178
    extra_context = kwargs.setdefault('extra_context', {})
1179
    extra_context['view'] = password_change
1180
    extra_context[REDIRECT_FIELD_NAME] = post_change_redirect
1181
    if not request.user.has_usable_password():
1182
        kwargs['password_change_form'] = passwords_forms.SetPasswordForm
1183
    response = dj_password_change(request, *args, **kwargs)
1184
    if isinstance(response, HttpResponseRedirect):
1185
        hooks.call_hooks('event', name='change-password', user=request.user, request=request)
1186
        messages.info(request, _('Password changed'))
1187
    return response
1188
password_change.title = _('Password Change')
1189
password_change.do_not_call_in_templates = True
1190

  
1191

  
1192
def notimplemented_view(request):
1193
    raise NotImplementedError
626
-