Projet

Général

Profil

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

Nicolas Roche, 17 juillet 2020 17:21

Télécharger (10,8 ko)

Voir les différences:

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

 src/authentic2/views.py                       |  2 +
 .../authorized_oauth_apps.html                | 49 +++++++++++++++++++
 src/authentic2_idp_oidc/urls.py               |  3 ++
 src/authentic2_idp_oidc/views.py              | 31 +++++++++++-
 tests/test_idp_oidc.py                        | 43 ++++++++++++++++
 5 files changed, 127 insertions(+), 1 deletion(-)
 create mode 100644 src/authentic2_idp_oidc/templates/authentic2_idp_oidc/authorized_oauth_apps.html
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
            'oidc_authorizations': OIDCAuthorization.objects.filter(user=self.request.user),
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())
src/authentic2_idp_oidc/templates/authentic2_idp_oidc/authorized_oauth_apps.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
{% block oidc-authorized-oauth-apps-pre %}{% endblock %}
6
<div id="oidc-authorized-oauth-apps" class="authorized-oauth-apps">
7
  {% block oidc-authorized-oauth-apps-top %}{% endblock %}
8
  {% if authorized_oauth_apps|length_is:0 %}
9
  {% trans "You have no granted application access to your account." %}
10
  {% else %}
11
  <form method="post">
12
    {% blocktrans with len=authorized_oauth_apps|length %}
13
    You have granted {{ len }} applications access to your account.
14
    {% endblocktrans %}
15
    {% csrf_token %}
16
    <input type="hidden" id="auth-id" name="auth_id" value="">
17
    <input type="submit" value="{% trans 'Revoke all' %}">
18
  </form>
19
  <table>
20
    <thead>
21
      <tr>
22
        <td>{% trans "Application" %}</td>
23
        <td>{% trans "Attributs" %}</td>
24
        <td>{% trans "From" %}</td>
25
        <td>{% trans "To" %}</td>
26
        <td></td>
27
      </tr>
28
    </thead>
29
    {% for auth in authorized_oauth_apps %}
30
    <tr>
31
      <td>{{ auth.client }}</td>
32
      <td>{{ auth.scopes }}</td>
33
      <td>{{ auth.created }}</td>
34
      <td>{{ auth.expired }}</td>
35
      <td>
36
        <form method="post">
37
          {% csrf_token %}
38
          <input type="hidden" id="auth-id" name="auth_id" value="{{ auth.id }}">
39
          <input type="submit" value="{% trans 'Revoke' %}">
40
        </form>
41
      </td>
42
    </tr>
43
    {% endfor %}
44
  </table>
45
  {% endif %}
46
  {% block oidc-authorized-oauth-apps-bottom %}{% endblock %}
47
</div>
48
{% block oidc-authorized-oauth-apps-post %}{% endblock %}
49
{% endblock %}
src/authentic2_idp_oidc/urls.py
24 24
        views.openid_configuration,
25 25
        name='oidc-openid-configuration'),
26 26
    url(r'^idp/oidc/certs/$',
27 27
        views.certs,
28 28
        name='oidc-certs'),
29 29
    url(r'^idp/oidc/authorize/$',
30 30
        views.authorize,
31 31
        name='oidc-authorize'),
32
    url(r'^idp/oidc/authorized_oauth_apps/$',
33
        views.authorized_oauth_apps,
34
        name='oidc-authorized-oauth-apps'),
32 35
    url(r'^idp/oidc/token/$',
33 36
        views.token,
34 37
        name='oidc-token'),
35 38
    url(r'^idp/oidc/user_info/$',
36 39
        views.user_info,
37 40
        name='oidc-user-info'),
38 41
    url(r'^idp/oidc/logout/$',
39 42
        views.logout,
src/authentic2_idp_oidc/views.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import logging
18 18
import math
19 19
import datetime
20 20
import base64
21 21
import time
22 22

  
23
from django.http import (HttpResponse, HttpResponseNotAllowed, JsonResponse)
23
from django.http import (HttpResponse, HttpResponseNotAllowed, HttpResponseRedirect, JsonResponse)
24 24
from django.urls import reverse
25 25
from django.utils import six
26 26
from django.utils.encoding import force_text
27 27
from django.utils.timezone import now, utc
28 28
from django.utils.http import urlencode
29 29
from django.shortcuts import render
30 30
from django.views.decorators.csrf import csrf_exempt
31
from django.views.generic import TemplateView
31 32
from django.contrib import messages
32 33
from django.contrib.auth import authenticate
34
from django.contrib.auth.decorators import login_required
33 35
from django.conf import settings
34 36
from django.utils.translation import ugettext as _
35 37
from ratelimit.utils import is_ratelimited
36 38

  
37 39
from authentic2 import app_settings as a2_app_settings
38 40
from authentic2.compat.misc import Base64Error
39 41
from authentic2.decorators import setting_enabled
40 42
from authentic2.exponential_retry_timeout import ExponentialRetryTimeout
......
361 363
            })
362 364
        # query is transfered through the hashtag
363 365
        response = redirect(request, redirect_uri + '#%s' % urlencode(params), resolve=False)
364 366
    hooks.call_hooks('event', name='sso-success', idp='oidc', service=client, user=request.user)
365 367
    utils.add_oidc_session(request, client)
366 368
    return response
367 369

  
368 370

  
371
class AuthorizedOauthAppsView(TemplateView):
372
    template_name = 'authentic2_idp_oidc/authorized_oauth_apps.html'
373
    title = _('Granted Applications')
374

  
375
    def dispatch(self, request, *args, **kwargs):
376
        return login_required(
377
            super(AuthorizedOauthAppsView, self).dispatch)(request, *args, **kwargs)
378

  
379
    def get_context_data(self, **kwargs):
380
        context = super(AuthorizedOauthAppsView, self).get_context_data(**kwargs)
381
        context['authorized_oauth_apps'] = models.OIDCAuthorization.objects.filter(
382
            user=self.request.user)
383
        return context
384

  
385
    def post(self, request, *args, **kwargs):
386
        if request.user:
387
            qs = models.OIDCAuthorization.objects.filter(user=request.user)
388
            auth_id = request.POST.get('auth_id')
389
            if auth_id:
390
                qs = qs.filter(id=auth_id)
391
            qs.delete()
392
        return HttpResponseRedirect(reverse('oidc-authorized-oauth-apps'))
393

  
394

  
395
authorized_oauth_apps = AuthorizedOauthAppsView.as_view()
396

  
397

  
369 398
def authenticate_client(request, client=None):
370 399
    '''Authenticate client on the token endpoint'''
371 400

  
372 401
    if 'HTTP_AUTHORIZATION' in request.META:
373 402
        authorization = request.META['HTTP_AUTHORIZATION'].split()
374 403
        if authorization[0] != 'Basic' or len(authorization) != 2:
375 404
            return None
376 405
        try:
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_apps_views(app, oidc_client, simple_user):
1610
    url = make_url('oidc-authorized-oauth-apps')
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 no granted application access to your account." 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 applications access to your account." in response.text
1630
    assert len(response.html.find_all('form')) == 4
1631

  
1632
    revoke_all_form = response.html.find_all('form')[0]
1633
    assert revoke_all_form.find('input', {"type": "submit"}).attrs['value'] == 'Revoke all'
1634
    assert revoke_all_form.find('input', {"name": "auth_id"}).attrs['value'] == ""
1635

  
1636
    revoke_one_forms = response.html.find_all('form')[1:]
1637
    assert [x.find('input', {"type": "submit"}).attrs['value']
1638
            for x in revoke_one_forms] == ['Revoke'] * 3
1639

  
1640
    # revoke one
1641
    response = response.forms[1].submit()
1642
    response = response.follow()
1643
    assert "You have granted 2 applications access to your account." in response.text
1644
    assert len(response.html.find_all('form')) == 3
1645

  
1646
    # revoke all
1647
    response = response.forms[0].submit()
1648
    response = response.follow()
1649
    assert "You have no granted application access to your account." in response.text
1607
-