0001-reorganize-views-and-forms-32934.patch
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 |
- |