Projet

Général

Profil

0001-manager-add-OpenID-service-handling-20696.patch

Voir les différences:

Subject: [PATCH] manager: add OpenID service handling (#20696)

 src/authentic2/manager/forms.py               |  27 ++++
 src/authentic2/manager/service_views.py       | 118 +++++++++++++++++-
 .../manager/oidc_service_detail.html          |  31 +++++
 .../templates/authentic2/manager/service.html |  14 ++-
 .../authentic2/manager/services.html          |   9 ++
 src/authentic2/manager/urls.py                |  29 ++++-
 src/authentic2/manager/views.py               |   1 +
 src/authentic2_idp_oidc/app_settings.py       |  22 ++++
 tests/test_manager.py                         |  92 ++++++++++++++
 9 files changed, 337 insertions(+), 6 deletions(-)
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/oidc_service_detail.html
src/authentic2/manager/forms.py
32 32

  
33 33
from authentic2.a2_rbac.models import OrganizationalUnit, Permission, Role
34 34
from authentic2.a2_rbac.utils import generate_slug, get_default_ou
35
from authentic2.attributes_ng.engine import get_service_attributes
35 36
from authentic2.forms.fields import CheckPasswordField, NewPasswordField, ValidatedEmailField
36 37
from authentic2.forms.mixins import SlugMixin
37 38
from authentic2.forms.profile import BaseUserForm
39
from authentic2.forms.widgets import DatalistTextInput
38 40
from authentic2.models import PasswordReset
39 41
from authentic2.passwords import generate_password
40 42
from authentic2.utils.misc import (
......
43 45
    send_password_reset_mail,
44 46
    send_templated_mail,
45 47
)
48
from authentic2_idp_oidc import app_settings as oidc_app_settings
49
from authentic2_idp_oidc.models import OIDCClaim, OIDCClient
46 50
from django_rbac.backends import DjangoRBACBackend
47 51
from django_rbac.models import Operation
48 52

  
......
897 901

  
898 902
        perm = '%s.search_%s' % (User._meta.app_label, User._meta.model_name)
899 903
        return user.filter_by_perm(perm, qs)
904

  
905

  
906
class OIDCServiceForm(SlugMixin, forms.ModelForm):
907
    class Meta:
908
        model = OIDCClient
909
        fields = oidc_app_settings.MANAGER_FIELDS
910

  
911

  
912
class OIDCClaimForm(forms.ModelForm):
913
    class Meta:
914
        model = OIDCClaim
915
        fields = ('name', 'value', 'scopes')
916
        widgets = {
917
            'value': DatalistTextInput,
918
        }
919

  
920
    def __init__(self, *args, **kwargs):
921
        super().__init__(*args, **kwargs)
922
        data = dict(get_service_attributes(getattr(self.instance, 'client', None))).keys()
923
        widget = self.fields['value'].widget
924
        widget.data = data
925
        widget.name = 'list__oidcclaim-inline'
926
        widget.attrs.update({'list': 'list__oidcclaim-inline'})
src/authentic2/manager/service_views.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
from django.contrib import messages
18
from django.template.loader import render_to_string
19
from django.urls import reverse
18 20
from django.utils.translation import ugettext as _
19 21

  
20 22
from authentic2.models import Service
23
from authentic2_idp_oidc import app_settings as oidc_app_settings
24
from authentic2_idp_oidc.models import OIDCClaim, OIDCClient
21 25

  
22 26
from . import forms, role_views, tables, views
23 27

  
......
82 86
            messages.warning(self.request, _('You are not authorized'))
83 87
        return super().form_valid(form)
84 88

  
89
    def get_service_data(self, service, fields):
90
        for field in fields:
91
            field_value = getattr(service, field)
92
            if not field_value:
93
                continue
94
            if hasattr(service, 'get_%s_display' % field):
95
                field_value = getattr(service, 'get_%s_display' % field)()
96
            yield service._meta.get_field(field).verbose_name, field_value
97

  
98
    def get_oidc_service_data(self):
99
        if not hasattr(self.object, 'oidcclient'):
100
            return {}
101
        service = self.object.oidcclient
102
        # add client id and secret, but remove name because it's already displayed
103
        display_fields = ['client_id', 'client_secret'] + oidc_app_settings.MANAGER_FIELDS[1:]
104
        ctx = {
105
            'service_fields': self.get_service_data(service, display_fields),
106
            'claims': service.oidcclaim_set.all(),
107
            'service': service,
108
        }
109
        self.can_delete = True
110
        return {'service_block': render_to_string('authentic2/manager/oidc_service_detail.html', ctx)}
111

  
85 112
    def get_context_data(self, **kwargs):
86 113
        kwargs['form'] = self.get_form()
87 114
        ctx = super().get_context_data(**kwargs)
88 115
        ctx['roles_table'] = tables.RoleTable(self.object.roles.all())
116
        ctx.update(self.get_oidc_service_data())
89 117
        return ctx
90 118

  
91 119

  
92
roles = ServiceView.as_view()
120
service_detail = ServiceView.as_view()
93 121

  
94 122

  
95 123
class ServiceEditView(views.BaseEditView):
......
101 129
    fields = ['name', 'slug', 'ou', 'unauthorized_url']
102 130
    success_url = '..'
103 131

  
132
    def get_object(self, queryset=None):
133
        service = super().get_object(queryset)
134
        if hasattr(service, 'oidcclient'):
135
            self.model = OIDCClient
136
            return service.oidcclient
137
        return service
138

  
139
    def get_fields(self):
140
        fields = super().get_fields()
141
        if hasattr(self.object, 'oidcclient'):
142
            fields.extend(oidc_app_settings.MANAGER_FIELDS)
143
        return fields
144

  
145

  
146
edit_service = ServiceEditView.as_view()
147

  
148

  
149
class ServiceDeleteView(views.BaseDeleteView):
150
    model = Service
151
    pk_url_kwarg = 'service_pk'
152
    permissions = ['authentic2.delete_service']
153
    title = _('Delete OpenID Service')
154

  
155

  
156
delete_service = ServiceDeleteView.as_view()
157

  
158

  
159
class OIDCServiceAddView(views.ActionMixin, views.BaseAddView):
160
    form_class = forms.OIDCServiceForm
161
    model = OIDCClient
162
    title = _('Add OpenID service')
163
    permissions = ['authentic2.add_service']
164
    action = _('Add')
165

  
166
    def get_success_url(self):
167
        # add default claims mappings after creating service
168
        for mapping in oidc_app_settings.DEFAULT_MAPPINGS:
169
            OIDCClaim.objects.get_or_create(client=self.object, **mapping)
170
        return reverse('a2-manager-service', kwargs={'service_pk': self.object.pk})
171

  
172

  
173
add_oidc_service = OIDCServiceAddView.as_view()
174

  
175

  
176
class OIDCClaimAddView(views.ActionMixin, views.BaseAddView):
177
    form_class = forms.OIDCClaimForm
178
    model = OIDCClaim
179
    title = _('Add OpenID Claim')
180
    action = _('Add')
181

  
182
    def form_valid(self, form):
183
        obj = form.save(commit=False)
184
        obj.client = OIDCClient.objects.get(pk=self.kwargs['service_pk'])
185
        obj.save()
186
        return super().form_valid(form)
187

  
188
    def get_success_url(self):
189
        return reverse('a2-manager-service', kwargs={'service_pk': self.object.client.pk})
190

  
191

  
192
oidc_claim_add = OIDCClaimAddView.as_view()
193

  
194

  
195
class BaseClaimView:
196
    model = OIDCClaim
197
    pk_url_kwarg = 'claim_pk'
198

  
199
    def get_queryset(self):
200
        qs = super().get_queryset()
201
        return qs.filter(client__pk=self.kwargs['service_pk'])
202

  
203
    def get_success_url(self):
204
        return reverse('a2-manager-service', kwargs={'service_pk': self.object.client.pk})
205

  
206

  
207
class OIDCClaimEditView(BaseClaimView, views.BaseEditView):
208
    title = _('Edit OpenID Claim')
209
    form_class = forms.OIDCClaimForm
210

  
211

  
212
oidc_claim_edit = OIDCClaimEditView.as_view()
213

  
214

  
215
class OIDCClaimDeleteView(BaseClaimView, views.BaseDeleteView):
216
    title = _('Delete OpenID Claim')
217

  
104 218

  
105
edit = ServiceEditView.as_view()
219
oidc_claim_delete = OIDCClaimDeleteView.as_view()
src/authentic2/manager/templates/authentic2/manager/oidc_service_detail.html
1
{% load i18n %}
2
{% for field, value in service_fields %}
3
  <p>{{ field|capfirst }}{% trans ":" %}  {% if value == True %}{% trans "yes" %}
4
  {% elif value == False %}{% trans "no" %}
5
  {% else %}{{value}}
6
  {% endif %}</p>
7
{% endfor %}
8

  
9
<div class="section">
10
  <h3>{% trans "Claims" %}<a href="{% url "a2-manager-oidc-claim-add" service_pk=service.pk %}" class="button" rel="popup">Add claim</a></h3>
11
  {% if claims %}
12
  <table class="main plaintable objects-table" id="oidc-claims">
13
    <thead>
14
      <tr><th>{% trans "Name" %}</th><th>{% trans "Value" %}</th><th>{% trans "Scopes" %}</th><th></th></tr>
15
    </thead>
16
    <tbody>
17
    {% for claim in claims %}
18
    <tr>
19
      <td>{{ claim.name }}</td>
20
      <td>{{ claim.value }}</td>
21
      <td>{{ claim.scopes }}</td>
22
      <td class="actions">
23
        <a class="edit" href="{% url "a2-manager-oidc-claim-edit" service_pk=service.pk claim_pk=claim.pk %}" rel="popup" title="{% trans "Edit" %}">{% trans "Edit" %}</a>
24
        <a class="delete" href="{% url "a2-manager-oidc-claim-delete" service_pk=service.pk claim_pk=claim.pk %}" rel="popup" title="{% trans "Delete" %}">{% trans "Delete" %}</a>
25
      </td>
26
    </tr>
27
    {% endfor %}
28
    </tbody>
29
  </table>
30
  {% endif %}
31
</div>
src/authentic2/manager/templates/authentic2/manager/service.html
1
{% extends "authentic2/manager/services.html" %}
1
{% extends "authentic2/manager/base.html" %}
2 2
{% load i18n static django_tables2 %}
3 3

  
4 4
{% block page-title %}{% firstof manager_site_title site_title "Authentic2" %} - {{ object }}{% endblock %}
5 5

  
6 6
{% block breadcrumb %}
7
  {{ block.super }}
7
{{ block.super }}
8
  <a href="{% url 'a2-manager-services' %}">{% trans 'Services' %}</a>
8 9
  <a href="{% url 'a2-manager-service' service_pk=view.kwargs.service_pk %}">{{ view.service.name }}</a>
9 10
{% endblock %}
10 11

  
12
{% block buttons %}
13
{% endblock %}
14

  
11 15
{% block appbar %}
12 16
  {{ block.super }}
13 17
  <span class="actions">
18
  {% if view.can_delete %}
19
  <a rel="popup" href="{% url "a2-manager-service-delete" service_pk=view.kwargs.service_pk %}">{% trans "Delete" %}</a>
20
  {% endif %}
14 21
  {% if view.can_change %}
15 22
  <a rel="popup" href="{% url "a2-manager-service-edit" service_pk=view.kwargs.service_pk %}">{% trans "Edit" %}</a>
16 23
  {% endif %}
......
34 41
{% endblock %}
35 42

  
36 43
{% block main %}
44

  
45
{{ service_block|safe }}
46

  
37 47
<div class="section">
38 48
  <h3>{% trans "Roles of users allowed on this service" %}</h3>
39 49
  <div id="authorized-roles">
src/authentic2/manager/templates/authentic2/manager/services.html
8 8
  <a href="{% url 'a2-manager-services' %}">{% trans 'Services' %}</a>
9 9
{% endblock %}
10 10

  
11
{% block appbar %}
12
  {{ block.super }}
13
  <span class="actions">
14
    {% if view.can_add %}
15
    <a href="{% url "a2-manager-add-oidc-service" %}">{% trans "Add OpenID service" %}</a>
16
    {% endif %}
17
  </span>
18
{% endblock %}
19

  
11 20
{% block sidebar %}
12 21
  <aside id="sidebar">
13 22
    {% include "authentic2/manager/search_form.html" %}
src/authentic2/manager/urls.py
182 182
        url(r'^organizational-units/import/$', ou_views.ous_import, name='a2-manager-ous-import'),
183 183
        # Services
184 184
        url(r'^services/$', service_views.listing, name='a2-manager-services'),
185
        url(r'^services/(?P<service_pk>\d+)/$', service_views.roles, name='a2-manager-service'),
186
        url(r'^services/(?P<service_pk>\d+)/edit/$', service_views.edit, name='a2-manager-service-edit'),
185
        url(r'^services/(?P<service_pk>\d+)/$', service_views.service_detail, name='a2-manager-service'),
186
        url(r'^services/add-oidc/$', service_views.add_oidc_service, name='a2-manager-add-oidc-service'),
187
        url(
188
            r'^services/(?P<service_pk>\d+)/edit/$',
189
            service_views.edit_service,
190
            name='a2-manager-service-edit',
191
        ),
192
        url(
193
            r'^services/(?P<service_pk>\d+)/delete/$',
194
            service_views.delete_service,
195
            name='a2-manager-service-delete',
196
        ),
197
        url(
198
            r'^services/(?P<service_pk>\d+)/claim/add/$',
199
            service_views.oidc_claim_add,
200
            name='a2-manager-oidc-claim-add',
201
        ),
202
        url(
203
            r'^services/(?P<service_pk>\d+)/claim/(?P<claim_pk>\d+)/edit/$',
204
            service_views.oidc_claim_edit,
205
            name='a2-manager-oidc-claim-edit',
206
        ),
207
        url(
208
            r'^services/(?P<service_pk>\d+)/claim/(?P<claim_pk>\d+)/delete/$',
209
            service_views.oidc_claim_delete,
210
            name='a2-manager-oidc-claim-delete',
211
        ),
187 212
        # Journal
188 213
        url(r'^journal/$', journal_views.journal, name='a2-manager-journal'),
189 214
        url(
src/authentic2/manager/views.py
483 483

  
484 484
    fields = None
485 485
    form_class = None
486
    model = None
486 487

  
487 488
    def get_fields(self):
488 489
        return self.fields
src/authentic2_idp_oidc/app_settings.py
83 83
    def PROFILE_OVERRIDE_MAPPING(self):
84 84
        return self._setting('PROFILE_OVERRIDE_MAPPING', {'email': 'email'})
85 85

  
86
    @property
87
    def MANAGER_FIELDS(self):
88
        return self._setting(
89
            'MANAGER_FIELDS',
90
            [
91
                'name',
92
                'redirect_uris',
93
                'post_logout_redirect_uris',
94
                'sector_identifier_uri',
95
                'frontchannel_logout_uri',
96
                'ou',
97
                'identifier_policy',
98
                'idtoken_algo',
99
                'unauthorized_url',
100
                'authorization_mode',
101
                'authorization_flow',
102
                'home_url',
103
                'colour',
104
                'logo',
105
            ],
106
        )
107

  
86 108

  
87 109
app_settings = AppSettings('A2_IDP_OIDC_')
88 110
app_settings.__name__ = __name__
tests/test_manager.py
34 34
from authentic2.apps.journal.models import Event
35 35
from authentic2.models import Service
36 36
from authentic2.validators import EmailValidator
37
from authentic2_idp_oidc import app_settings as oidc_app_settings
38
from authentic2_idp_oidc.models import OIDCClaim, OIDCClient
37 39
from django_rbac.models import VIEW_OP
38 40
from django_rbac.utils import get_operation
39 41

  
......
1220 1222
    resp = resp.form.submit()
1221 1223
    assert 'Test Service' in resp.text
1222 1224
    assert 'Example Service' not in resp.text
1225

  
1226

  
1227
def test_manager_add_oidc_service(app, superuser):
1228
    resp = login(app, superuser, 'a2-manager-services')
1229
    assert 'Add OpenID service' in resp.text
1230
    assert OIDCClient.objects.count() == 0
1231
    assert OIDCClaim.objects.count() == 0
1232

  
1233
    resp = resp.click('Add OpenID service')
1234
    form = resp.form
1235
    for field in oidc_app_settings.MANAGER_FIELDS:
1236
        assert field in form.fields
1237

  
1238
    form['name'] = 'Test'
1239
    form['redirect_uris'] = 'http://example.com'
1240
    resp = form.submit()
1241

  
1242
    assert OIDCClient.objects.count() == 1
1243
    assert OIDCClaim.objects.count() == len(oidc_app_settings.DEFAULT_MAPPINGS)
1244
    assert resp.location == reverse('a2-manager-service', kwargs={'service_pk': OIDCClient.objects.get().pk})
1245

  
1246
    resp = resp.follow()
1247
    assert "<h3>Claims" in resp.text
1248
    assert "Add claim" in resp.text
1249
    assert resp.pyquery.remove_namespaces()('#oidc-claims tbody tr').length == len(
1250
        oidc_app_settings.DEFAULT_MAPPINGS
1251
    )
1252

  
1253

  
1254
def test_manager_edit_oidc_service(app, superuser):
1255
    OIDCClient.objects.create(name='Test', slug='test', redirect_uris='http://example.com')
1256
    resp = login(app, superuser, 'a2-manager-services')
1257
    resp = resp.click('Test')
1258
    resp = resp.click('Edit')
1259
    form = resp.form
1260
    form['name'] = 'New Test'
1261
    form['colour'] = '#ff00ff'
1262
    resp = form.submit()
1263
    assert resp.location == '..'
1264
    resp = resp.follow()
1265
    assert "New Test" in resp.text
1266
    assert "#ff00ff" in resp.text
1267

  
1268

  
1269
def test_manager_delete_oidc_service(app, superuser):
1270
    OIDCClient.objects.create(name='Test', slug='test', redirect_uris='http://example.com')
1271
    resp = login(app, superuser, 'a2-manager-services')
1272
    resp = resp.click('Test')
1273
    resp = resp.click('Delete')
1274
    resp = resp.form.submit().follow()
1275
    assert OIDCClient.objects.count() == 0
1276

  
1277

  
1278
def test_manager_add_oidc_claim(app, superuser):
1279
    client = OIDCClient.objects.create(name='Test', slug='test', redirect_uris='http://example.com')
1280
    resp = login(app, superuser, reverse('a2-manager-service', kwargs={'service_pk': client.pk}))
1281
    resp = resp.click('Add claim')
1282
    form = resp.form
1283
    form['name'] = 'claim'
1284
    form['value'] = 'value'
1285
    form['scopes'] = 'profile'
1286
    resp = form.submit()
1287
    assert resp.location == reverse('a2-manager-service', kwargs={'service_pk': client.pk})
1288
    assert OIDCClaim.objects.filter(client=client, name='claim', value='value', scopes='profile').exists()
1289

  
1290

  
1291
def test_manager_edit_oidc_claim(app, superuser):
1292
    client = OIDCClient.objects.create(name='Test', slug='test', redirect_uris='http://example.com')
1293
    OIDCClaim.objects.create(client=client, name='claim', value='value', scopes='profile')
1294
    resp = login(app, superuser, reverse('a2-manager-service', kwargs={'service_pk': client.pk}))
1295
    assert "claim" in resp.text
1296
    resp = resp.click('Edit', index=1)
1297
    form = resp.form
1298
    form['value'] = 'new value'
1299
    resp = form.submit()
1300
    assert resp.location == reverse('a2-manager-service', kwargs={'service_pk': client.pk})
1301
    assert not OIDCClaim.objects.filter(client=client, name='claim', value='value', scopes='profile').exists()
1302
    assert OIDCClaim.objects.filter(client=client, name='claim', value='new value', scopes='profile').exists()
1303

  
1304

  
1305
def test_manager_delete_oidc_claim(app, superuser):
1306
    client = OIDCClient.objects.create(name='Test', slug='test', redirect_uris='http://example.com')
1307
    OIDCClaim.objects.create(client=client, name='claim', value='value', scopes='profile')
1308
    resp = login(app, superuser, reverse('a2-manager-service', kwargs={'service_pk': client.pk}))
1309
    assert "claim" in resp.text
1310
    resp = resp.click('Delete', index=1)
1311
    form = resp.form
1312
    resp = form.submit()
1313
    assert resp.location == reverse('a2-manager-service', kwargs={'service_pk': client.pk})
1314
    assert not OIDCClaim.objects.filter(client=client, name='claim', value='value', scopes='profile').exists()
1223
-