Projet

Général

Profil

0001-idp_oidc-revoke-oidc-claims-authorization-45200.patch

Nicolas Roche, 27 juillet 2020 16:34

Télécharger (13,4 ko)

Voir les différences:

Subject: [PATCH] idp_oidc: revoke oidc claims authorization (#45200)

 src/authentic2/app_settings.py                |  3 +
 .../templates/authentic2/accounts.html        |  3 +
 .../accounts_authorized_oauth_services.html   | 67 +++++++++++++++++++
 src/authentic2/urls.py                        |  3 +
 src/authentic2/views.py                       | 25 +++++++
 tests/test_idp_oidc.py                        | 36 ++++++++++
 tests/test_profile.py                         | 21 ++++++
 7 files changed, 158 insertions(+)
 create mode 100644 src/authentic2/templates/authentic2/accounts_authorized_oauth_services.html
src/authentic2/app_settings.py
106 106
        default=None,
107 107
        definition='File containing certificate chains as PEM certificates'),
108 108
    A2_REGISTRATION_CAN_DELETE_ACCOUNT=Setting(
109 109
        default=True,
110 110
        definition='Can user self delete their account and all their data'),
111 111
    A2_REGISTRATION_CAN_CHANGE_PASSWORD=Setting(
112 112
        default=True,
113 113
        definition='Allow user to change its own password'),
114
    A2_REGISTRATION_CAN_MANAGE_SERVICE_AUTHORIZATIONS=Setting(
115
        default=True,
116
        definition='Allow user to revoke granted services access to its account profile data'),
114 117
    A2_REGISTRATION_EMAIL_BLACKLIST=Setting(
115 118
        default=[],
116 119
        definition='List of forbidden email wildcards, ex.: ^.*@ville.fr$'),
117 120
    A2_REGISTRATION_REDIRECT=Setting(
118 121
        default=None,
119 122
        definition='Forced redirection after each redirect, NEXT_URL substring is replaced'
120 123
        ' by the original next_url passed to /accounts/register/'),
121 124
    A2_PROFILE_CAN_CHANGE_EMAIL=Setting(
src/authentic2/templates/authentic2/accounts.html
32 32
        </dl>
33 33
      {% endif %}
34 34
      {% if allow_email_change %}
35 35
        <p><a href="{% url 'email-change' %}">{% trans "Change email" %}</a></p>
36 36
      {% endif %}
37 37
      {% if allow_profile_edit %}
38 38
        <p><a href="{% url 'profile_edit' %}">{% trans "Edit account data" %}</a></p>
39 39
      {% endif %}
40
      {% if allow_authorization_management %}
41
        <p><a href="{% url 'authorized-oauth-services' %}">{% trans "Manage service authorizations" %}</a></p>
42
      {% endif %}
40 43
      {% if allow_account_deletion %}
41 44
        <p><a href="{% url 'delete_account' %}">{% trans "Delete account" %}</a></p>
42 45
      {% endif %}
43 46
    </div>
44 47
    <div id="a2-credentials" class="a2-profile-block">
45 48
      <h3>{% trans "Credentials" %}</h3>
46 49
      {% for html_block in frontends_block %}
47 50
        {{ html_block|safe }}
src/authentic2/templates/authentic2/accounts_authorized_oauth_services.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block page-title %}
5
{{ block.super }} - {{ view.title }}
6
{% endblock %}
7

  
8
{% block breadcrumb %}
9
{{ block.super }}
10
<a href="..">{% trans "Your account" %}</a>
11
<a href="">{{ view.title }}</a>
12
{% endblock %}
13

  
14
{% block content %}
15
{% block oidc-authorized-oauth-services-pre %}{% endblock %}
16
<div class="authorized-oauth-services">
17
  {% block oidc-authorized-oauth-services-top %}
18
  <p class="authorized-oauth-services--top">
19
    {% if authorized_oauth_services|length_is:0 %}
20
    {% trans "You have not granted service access to your account profile data." %}
21
    {% else %}
22
    {% blocktrans count counter=authorized_oauth_services|length %}
23
    You have granted one service access to your account profile data.
24
    {% plural %}
25
    You have granted {{ counter }} services access to your account profile data.
26
    {% endblocktrans %}
27
    {% endif %}
28
  </p>
29
  {% endblock %}
30
  <ul class="authorized-oauth-services--list">
31
    {% for auth in authorized_oauth_services %}
32
    <li class="authorized-oauth-services--item">
33
      <form method="post" class="authorized-oauth-services--form">
34
        {% csrf_token %}
35
        {% block oidc-authorized-oauth-service %}
36
        <div class="authorized-oauth-services--infos">
37
          {% block oidc-authorized-oauth-service-top %}{% endblock %}
38
          <span class="authorized-oauth-services--client">
39
            {{ auth.client }}
40
          </span>
41
          <span class="authorized-oauth-services--dates">
42
            <span class="authorized-oauth-services-dates--since">
43
              <span class="label">{% trans "Allowed since:" %}</span>
44
              <span class="time">{{ auth.created }}</span>
45
            </span>
46
            <span class="authorized-oauth-services--separator">/</span>
47
            <span class="authorized-oauth-services--expired">
48
              <span class="label">{% trans "Expire on:" %}</span>
49
              <span class="time">{{ auth.expired }}</span>
50
            </span>
51
          </span>
52
        </div>
53
        <div class="authorized-oauth-services--actions">
54
          <input type="hidden" id="auth-id" name="auth_id" value="{{ auth.id }}">
55
          <button class="authorized-oauth-services--revoke-button">{% trans 'Revoke' %}</button>
56
        </div>
57
        {% block oidc-authorized-oauth-service-bottom %}{% endblock %}
58
        {% endblock %}
59
      </form>
60
    </li>
61
    {% endfor %}
62
  </ul>
63
</table>
64
{% block oidc-authorized-oauth-services-bottom %}{% endblock %}
65
</div>
66
{% block oidc-authorized-oauth-services-post %}{% endblock %}
67
{% endblock %}
src/authentic2/urls.py
64 64
        views.edit_profile,
65 65
        name='profile_edit_with_scope'),
66 66
    url(r'^change-email/$',
67 67
        views.email_change,
68 68
        name='email-change'),
69 69
    url(r'^change-email/verify/$',
70 70
        views.email_change_verify,
71 71
        name='email-change-verify'),
72
    url(r'^authorizations/$',
73
        login_required(views.authorized_oauth_services),
74
        name='authorized-oauth-services'),
72 75
    url(r'^$',
73 76
        views.profile,
74 77
        name='account_management'),
75 78

  
76 79
    # Password change
77 80
    url(r'^password/change/$',
78 81
        views.password_change,
79 82
        name='password_change'),
src/authentic2/views.py
50 50
from django.http import Http404
51 51
from django.utils.http import urlsafe_base64_decode
52 52
from django.views.generic.edit import CreateView
53 53
from django.forms import CharField
54 54
from django.http import HttpResponseBadRequest
55 55
from django.template import loader
56 56

  
57 57
from authentic2.compat.misc import default_token_generator
58
from authentic2_idp_oidc.models import OIDCAuthorization
58 59
from . import (utils, app_settings, decorators, constants,
59 60
               models, cbv, hooks, validators)
60 61
from .utils import switch_user
61 62
from .a2_rbac.utils import get_default_ou
62 63
from .a2_rbac.models import OrganizationalUnit as OU
63 64
from .forms import (
64 65
    passwords as passwords_forms,
65 66
    registration as registration_forms,
......
501 502
        context.update({
502 503
            'frontends_block': blocks,
503 504
            'frontends_block_by_id': blocks_by_id,
504 505
            'profile': profile,
505 506
            'attributes': attributes,
506 507
            'allow_account_deletion': app_settings.A2_REGISTRATION_CAN_DELETE_ACCOUNT,
507 508
            'allow_profile_edit': EditProfile.can_edit_profile(),
508 509
            'allow_email_change': app_settings.A2_PROFILE_CAN_CHANGE_EMAIL,
510
            'allow_authorization_management': app_settings.A2_REGISTRATION_CAN_MANAGE_SERVICE_AUTHORIZATIONS,
509 511
            # TODO: deprecated should be removed when publik-base-theme is updated
510 512
            'allow_password_change': utils.user_can_change_password(request=request),
511 513
            'federation_management': federation_management,
512 514
        })
513 515
        hooks.call_hooks('modify_context_data', self, context)
514 516
        return context
515 517

  
516 518
profile = login_required(ProfileView.as_view())
......
1264 1266
class SuView(View):
1265 1267
    def get(self, request, uuid):
1266 1268
        user = switch_user.resolve_token(uuid)
1267 1269
        if not user:
1268 1270
            raise Http404
1269 1271
        return utils.simulate_authentication(request, user, 'su')
1270 1272

  
1271 1273
su = SuView.as_view()
1274

  
1275

  
1276
class AuthorizedOauthServicesView(TemplateView):
1277
    template_name = 'authentic2/accounts_authorized_oauth_services.html'
1278
    title = _('Consent Management')
1279

  
1280
    def get_context_data(self, **kwargs):
1281
        context = super(AuthorizedOauthServicesView, self).get_context_data(**kwargs)
1282
        context['authorized_oauth_services'] = OIDCAuthorization.objects.filter(
1283
            user=self.request.user)
1284
        return context
1285

  
1286
    def post(self, request, *args, **kwargs):
1287
        if request.user:
1288
            qs = OIDCAuthorization.objects.filter(user=request.user)
1289
            auth_id = request.POST.get('auth_id')
1290
            if auth_id:
1291
                qs = qs.filter(id=auth_id)
1292
            qs.delete()
1293
        return HttpResponseRedirect(reverse('authorized-oauth-services'))
1294

  
1295

  
1296
authorized_oauth_services = AuthorizedOauthServicesView.as_view()
tests/test_idp_oidc.py
1599 1599
    with pytest.raises(ValidationError, match=r'same domain'):
1600 1600
        OIDCClient(
1601 1601
            redirect_uris='https://example.com/ https://example2.com/',
1602 1602
            identifier_policy=OIDCClient.POLICY_PAIRWISE).clean()
1603 1603

  
1604 1604
    OIDCClient(
1605 1605
        redirect_uris='https://example.com/ https://example2.com/',
1606 1606
        sector_identifier_uri='https://example.com/').clean()
1607

  
1608

  
1609
def test_oidc_authorized_oauth_services_view(app, oidc_client, simple_user):
1610
    url = make_url('authorized-oauth-services')
1611
    response = app.get(url, status=302)
1612
    assert '/login/' in response.location
1613

  
1614
    utils.login(app, simple_user)
1615
    response = app.get(url, status=200)
1616
    assert "You have not granted service access to your account profile data." in response.text
1617

  
1618
    OIDCAuthorization.objects.create(
1619
        client=oidc_client, user=simple_user, scopes='openid',
1620
        expired=now() + datetime.timedelta(days=2))
1621
    OIDCAuthorization.objects.create(
1622
        client=oidc_client, user=simple_user, scopes='openid profile',
1623
        expired=now() + datetime.timedelta(days=2))
1624
    OIDCAuthorization.objects.create(
1625
        client=oidc_client, user=simple_user, scopes='openid profile email',
1626
        expired=now() + datetime.timedelta(days=2))
1627

  
1628
    response = app.get(url, status=200)
1629
    assert "You have granted 3 services access to your account profile data."
1630
    assert len(response.html.find_all(
1631
        'button', {'class': 'authorized-oauth-services--revoke-button'})) == 3
1632

  
1633
    # revoke two
1634
    response = response.forms[0].submit()
1635
    response = response.follow()
1636
    assert len(response.html.find_all(
1637
        'button', {'class': 'authorized-oauth-services--revoke-button'})) == 2
1638
    response = response.forms[0].submit()
1639
    response = response.follow()
1640
    assert len(response.html.find_all(
1641
        'button', {'class': 'authorized-oauth-services--revoke-button'})) == 1
1642
    assert "You have granted one service access to your account profile data." in response.text
tests/test_profile.py
187 187
    assert len(response.pyquery('input[type="radio"][name="edit-profile-title"][readonly="true"]')) == 0
188 188
    assert len(response.pyquery('select[name="edit-profile-title"]')) == 0
189 189

  
190 190
    simple_user.verified_attributes.title = 'Monsieur'
191 191

  
192 192
    response = app.get(url, status=200)
193 193
    assert len(response.pyquery('input[type="radio"][name="edit-profile-title"]')) == 0
194 194
    assert len(response.pyquery('input[type="text"][name="edit-profile-title@disabled"][readonly]')) == 1
195

  
196

  
197
def test_acount_view(app, simple_user, settings):
198
    utils.login(app, simple_user)
199
    url = reverse('account_management')
200
    response = app.get(url, status=200)
201
    assert [x['href'] for x in response.html.find('div', {'id': 'a2-profile'}).find_all('a')] == [
202
        reverse('email-change'),
203
        reverse('profile_edit'),
204
        reverse('authorized-oauth-services'),
205
        reverse('delete_account')
206
    ]
207

  
208
    settings.A2_PROFILE_CAN_CHANGE_EMAIL = False
209
    settings.A2_REGISTRATION_CAN_MANAGE_SERVICE_AUTHORIZATIONS = False
210
    settings.A2_REGISTRATION_CAN_DELETE_ACCOUNT = False
211
    url = reverse('account_management')
212
    response = app.get(url, status=200)
213
    assert [x['href'] for x in response.html.find('div', {'id': 'a2-profile'}).find_all('a')] == [
214
        reverse('profile_edit'),
215
    ]
195
-