0003-misc-block-user-without-required_on_login-attributes.patch
src/authentic2/custom_user/models.py | ||
---|---|---|
400 | 400 |
deleted_user.save() |
401 | 401 |
return super().delete(**kwargs) |
402 | 402 | |
403 |
def get_missing_required_on_login_attributes(self): |
|
404 |
attributes = Attribute.objects.filter(required_on_login=True, disabled=False).order_by( |
|
405 |
'order', 'label' |
|
406 |
) |
|
407 | ||
408 |
missing = [] |
|
409 |
for attribute in attributes: |
|
410 |
value = getattr(self.attributes, attribute.name, None) |
|
411 |
if not value: |
|
412 |
missing.append(attribute) |
|
413 |
return missing |
|
414 | ||
403 | 415 | |
404 | 416 |
class DeletedUser(models.Model): |
405 | 417 |
deleted = models.DateTimeField(verbose_name=_('Deletion date'), auto_now_add=True) |
src/authentic2/idp/saml/saml2_endpoints.py | ||
---|---|---|
1126 | 1126 |
return return_saml2_response(request, logout, title=_('You are being redirected to "%s"') % provider.name) |
1127 | 1127 | |
1128 | 1128 | |
1129 |
finish_slo.no_view_restriction = True |
|
1130 | ||
1131 | ||
1129 | 1132 |
def return_logout_error(request, logout, error): |
1130 | 1133 |
logout.buildResponseMsg() |
1131 | 1134 |
set_saml2_response_responder_status_code(logout.response, error) |
... | ... | |
1445 | 1448 |
return a2_views.logout(request, next_url=next_url, do_local=False, check_referer=False) |
1446 | 1449 | |
1447 | 1450 | |
1451 |
slo.no_view_restriction = True |
|
1452 | ||
1453 | ||
1448 | 1454 |
def icon_url(name): |
1449 | 1455 |
return '%s/authentic2/images/%s.png' % (settings.STATIC_URL, name) |
1450 | 1456 | |
... | ... | |
1529 | 1535 |
return HttpResponseRedirect(logout.msgUrl) |
1530 | 1536 | |
1531 | 1537 | |
1538 |
idp_slo.no_view_restriction = True |
|
1539 | ||
1540 | ||
1532 | 1541 |
def process_logout_response(request, logout, soap_response, next): |
1533 | 1542 |
logger.debug('logout response is %r', soap_response) |
1534 | 1543 |
try: |
... | ... | |
1570 | 1579 |
return process_logout_response(request, logout, get_saml2_query_request(request), next) |
1571 | 1580 | |
1572 | 1581 | |
1582 |
slo_return.no_view_restriction = True |
|
1583 | ||
1584 | ||
1573 | 1585 |
# Helpers |
1574 | 1586 | |
1575 | 1587 |
# Mapping to generate the metadata file, must be kept in sync with the url |
src/authentic2/middleware.py | ||
---|---|---|
126 | 126 |
if view: |
127 | 127 |
return view |
128 | 128 | |
129 |
view = self.check_required_on_login_attribute_restriction(request, user) |
|
130 |
if view: |
|
131 |
return view |
|
132 | ||
129 | 133 |
for plugin in plugins.get_plugins(): |
130 | 134 |
if hasattr(plugin, 'check_view_restrictions'): |
131 | 135 |
view = plugin.check_view_restrictions(request, user) |
... | ... | |
136 | 140 |
request.session['last_password_reset_check'] = now |
137 | 141 |
return None |
138 | 142 | |
143 |
def check_required_on_login_attribute_restriction(self, request, user): |
|
144 |
# do not bother superuser with this |
|
145 |
if user.is_superuser: |
|
146 |
return None |
|
147 | ||
148 |
missing = user.get_missing_required_on_login_attributes() |
|
149 |
if missing: |
|
150 |
return 'profile_required_edit' |
|
151 |
return None |
|
152 | ||
139 | 153 |
def check_password_reset_view_restriction(self, request, user): |
140 | 154 |
# If user is authenticated and a password_reset_flag is set, force |
141 | 155 |
# redirect to password change and show a message. |
... | ... | |
161 | 175 |
if url_name == view: |
162 | 176 |
return |
163 | 177 | |
164 |
# prevent blocking people when they logout
|
|
165 |
if url_name == 'auth_logout':
|
|
178 |
# prevent blocking some views, like logout views
|
|
179 |
if getattr(request.resolver_match.func, 'no_view_restriction', False):
|
|
166 | 180 |
return |
167 | 181 |
return utils_misc.redirect_and_come_back(request, view) |
168 | 182 |
src/authentic2/templates/authentic2/accounts_edit_required.html | ||
---|---|---|
1 |
{% extends "authentic2/accounts_edit.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block content %} |
|
5 |
{% block required-attributes-message %} |
|
6 |
<div class="infonotice">{% trans "The following informations are required if you want to use this service:"%} {% for attribute in view.missing_attributes %}{{ attribute.label }}{% if not forloop.last %}, {% endif %}{% endfor %} |
|
7 |
</div> |
|
8 |
{% endblock %} |
|
9 |
{{ block.super }} |
|
10 |
{% endblock %} |
src/authentic2/urls.py | ||
---|---|---|
58 | 58 |
), |
59 | 59 |
url(r'^logged-in/$', views.logged_in, name='logged-in'), |
60 | 60 |
url(r'^edit/$', views.edit_profile, name='profile_edit'), |
61 |
url(r'^edit/required/$', views.edit_required_profile, name='profile_required_edit'), |
|
61 | 62 |
url(r'^edit/(?P<scope>[-\w]+)/$', views.edit_profile, name='profile_edit_with_scope'), |
62 | 63 |
url(r'^change-email/$', views.email_change, name='email-change'), |
63 | 64 |
url(r'^change-email/verify/$', views.email_change_verify, name='email-change-verify'), |
src/authentic2/views.py | ||
---|---|---|
157 | 157 |
) |
158 | 158 | |
159 | 159 | |
160 |
class EditRequired(EditProfile): |
|
161 |
template_names = ['authentic2/accounts_edit_required.html'] |
|
162 | ||
163 |
def dispatch(self, request, *args, **kwargs): |
|
164 |
self.missing_attributes = request.user.get_missing_required_on_login_attributes() |
|
165 |
if not self.missing_attributes: |
|
166 |
return utils_misc.redirect(request, self.get_success_url()) |
|
167 |
return super().dispatch(request, *args, **kwargs) |
|
168 | ||
169 |
@classmethod |
|
170 |
def get_fields(cls, scopes=None): |
|
171 |
# only show the required fields |
|
172 |
attribute_names = models.Attribute.objects.filter(required_on_login=True, disabled=False).values_list( |
|
173 |
'name', flat=True |
|
174 |
) |
|
175 | ||
176 |
fields, labels = utils_misc.get_fields_and_labels(attribute_names) |
|
177 |
return fields, labels |
|
178 | ||
179 | ||
180 |
edit_required_profile = login_required(EditRequired.as_view()) |
|
181 | ||
182 | ||
160 | 183 |
class EmailChangeView(cbv.TemplateNamesMixin, FormView): |
161 | 184 |
template_names = ['profiles/email_change.html', 'authentic2/change_email.html'] |
162 | 185 |
title = _('Email Change') |
... | ... | |
607 | 630 |
return response |
608 | 631 | |
609 | 632 | |
633 |
logout.no_view_restriction = True |
|
634 | ||
635 | ||
610 | 636 |
def login_password_profile(request, *args, **kwargs): |
611 | 637 |
context = kwargs.pop('context', {}) |
612 | 638 |
can_change_password = utils_misc.user_can_change_password(request=request) |
src/authentic2_auth_fc/views.py | ||
---|---|---|
572 | 572 | |
573 | 573 | |
574 | 574 |
logout = LogoutReturnView.as_view() |
575 |
logout.no_view_restriction = True |
src/authentic2_idp_cas/views.py | ||
---|---|---|
469 | 469 | |
470 | 470 |
login = LoginView.as_view() |
471 | 471 |
logout = LogoutView.as_view() |
472 |
logout.no_view_restriction = True |
|
472 | 473 |
_continue = ContinueView.as_view() |
473 | 474 |
validate = ValidateView.as_view() |
474 | 475 |
service_validate = ServiceValidateView.as_view() |
src/authentic2_idp_oidc/views.py | ||
---|---|---|
811 | 811 |
# FIXME: do something with id_token_hint |
812 | 812 |
id_token_hint = request.GET.get('id_token_hint') |
813 | 813 |
return a2_logout(request, next_url=post_logout_redirect_uri, do_local=False, check_referer=False) |
814 | ||
815 | ||
816 |
logout.no_view_restriction = True |
tests/middlewares/test_required_on_login_restriction.py | ||
---|---|---|
1 |
# authentic2 - versatile identity manager |
|
2 |
# Copyright (C) 2021 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 authentic2.models import Attribute |
|
18 | ||
19 |
from ..utils import login |
|
20 | ||
21 | ||
22 |
def test_simple(app, db, simple_user): |
|
23 |
Attribute.objects.create( |
|
24 |
name='cgu_2021', |
|
25 |
label='J\'accepte les conditions générales d\'utilisation', |
|
26 |
kind='boolean', |
|
27 |
required_on_login=True, |
|
28 |
user_visible=True, |
|
29 |
) |
|
30 |
resp = login(app, simple_user, path='/accounts/') |
|
31 |
assert resp.location == '/accounts/edit/required/?next=/accounts/' |
|
32 |
resp = resp.follow() |
|
33 |
resp.form.set('cgu_2021', True) |
|
34 |
resp = resp.form.submit() |
|
35 |
assert resp.location == '/accounts/' |
|
36 |
resp = resp.follow() |
|
37 |
assert 'les conditions générales d\'utilisation\xa0:\nTrue' in resp.pyquery.text() |
tests/test_user_manager.py | ||
---|---|---|
338 | 338 |
# overspending memory for the queryset cache, 4 queries by batches |
339 | 339 |
num_queries = int(4 + 4 * (user_count / DEFAULT_BATCH_SIZE + bool(user_count % DEFAULT_BATCH_SIZE))) |
340 | 340 |
# export task also perform one query to set trigram an another to get users count |
341 |
num_queries += 2
|
|
341 |
num_queries += 3
|
|
342 | 342 |
with django_assert_num_queries(num_queries): |
343 | 343 |
response = response.click('CSV') |
344 | 344 | |
345 |
- |