Projet

Général

Profil

0003-misc-block-user-without-required_on_login-attributes.patch

Benjamin Dauvergne, 23 juillet 2021 15:48

Télécharger (11,8 ko)

Voir les différences:

Subject: [PATCH 3/4] misc: block user without required_on_login attributes
 (#24056)

Superuser are exempted from the restriction.
 src/authentic2/custom_user/models.py          | 12 ++++++
 src/authentic2/idp/saml/saml2_endpoints.py    | 12 ++++++
 src/authentic2/middleware.py                  | 18 ++++++++-
 .../authentic2/accounts_edit_required.html    | 10 +++++
 src/authentic2/urls.py                        |  1 +
 src/authentic2/views.py                       | 26 +++++++++++++
 src/authentic2_auth_fc/views.py               |  1 +
 src/authentic2_idp_cas/views.py               |  1 +
 src/authentic2_idp_oidc/views.py              |  3 ++
 tests/middlewares/__init__.py                 |  0
 .../test_required_on_login_restriction.py     | 37 +++++++++++++++++++
 tests/test_user_manager.py                    |  2 +-
 12 files changed, 120 insertions(+), 3 deletions(-)
 create mode 100644 src/authentic2/templates/authentic2/accounts_edit_required.html
 create mode 100644 tests/middlewares/__init__.py
 create mode 100644 tests/middlewares/test_required_on_login_restriction.py
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
-