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               |  14 +++
 src/authentic2/manager/service_views.py       | 116 +++++++++++++++++-
 .../manager/oidc_service_detail.html          |  32 +++++
 .../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, 323 insertions(+), 6 deletions(-)
 create mode 100644 src/authentic2/manager/templates/authentic2/manager/oidc_service_detail.html
src/authentic2/manager/forms.py
43 43
    send_password_reset_mail,
44 44
    send_templated_mail,
45 45
)
46
from authentic2_idp_oidc import app_settings as oidc_app_settings
47
from authentic2_idp_oidc.models import OIDCClaim, OIDCClient
46 48
from django_rbac.backends import DjangoRBACBackend
47 49
from django_rbac.models import Operation
48 50

  
......
897 899

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

  
903

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

  
909

  
910
class OIDCClaimForm(forms.ModelForm):
911
    class Meta:
912
        model = OIDCClaim
913
        fields = ('name', 'value', 'scopes')
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
        ctx = {
103
            'service_fields': self.get_service_data(service, oidc_app_settings.MANAGER_FIELDS),
104
            'claims': service.oidcclaim_set.all(),
105
            'service': service,
106
        }
107
        self.can_delete = True
108
        return {'service_block': render_to_string('authentic2/manager/oidc_service_detail.html', ctx)}
109

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

  
91 117

  
92
roles = ServiceView.as_view()
118
service_detail = ServiceView.as_view()
93 119

  
94 120

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

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

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

  
143

  
144
edit_service = ServiceEditView.as_view()
145

  
146

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

  
153

  
154
delete_service = ServiceDeleteView.as_view()
155

  
156

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

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

  
170

  
171
add_oidc_service = OIDCServiceAddView.as_view()
172

  
173

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

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

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

  
189

  
190
oidc_claim_add = OIDCClaimAddView.as_view()
191

  
192

  
193
class BaseClaimView:
194
    model = OIDCClaim
195
    pk_url_kwarg = 'claim_pk'
196

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

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

  
204

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

  
209

  
210
oidc_claim_edit = OIDCClaimEditView.as_view()
211

  
212

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

  
104 216

  
105
edit = ServiceEditView.as_view()
217
oidc_claim_delete = OIDCClaimDeleteView.as_view()
src/authentic2/manager/templates/authentic2/manager/oidc_service_detail.html
1
{% load i18n %}
2
{% if service_fields %}
3
<table class="main plaintable">
4
  {% for field_label, field_value in service_fields %}
5
  <tr>
6
    <td>{{ field_label|capfirst }}</td>
7
    <td>{{ field_value }}</td>
8
  </tr>
9
  {% endfor %}
10
</table>
11
{% endif %}
12

  
13
<div class="section">
14
  <h3>{% trans "Claims" %}<a href="{% url "a2-manager-oidc-claim-add" service_pk=service.pk %}" class="button" rel="popup">Add claim</a></h3>
15
  {% if claims %}
16
  <table class="main plaintable" id="oidc-claims">
17
    <thead>
18
      <tr><th>{% trans "Name" %}</th><th>{% trans "Value" %}</th><th>{% trans "Scopes" %}</th><th></th></tr>
19
    </thead>
20
    <tbody>
21
    {% for claim in claims %}
22
    <tr>
23
      <td>{{ claim.name }}</td>
24
      <td>{{ claim.value }}</td>
25
      <td>{{ claim.scopes }}</td>
26
      <td class="actions"><a class="edit" href="{% url "a2-manager-oidc-claim-edit" service_pk=service.pk claim_pk=claim.pk %}" rel="popup">{% trans "Edit" %}</a> <a class="delete" href="{% url "a2-manager-oidc-claim-delete" service_pk=service.pk claim_pk=claim.pk %}" rel="popup">{% trans "Delete" %}</a></td>
27
    </tr>
28
    {% endfor %}
29
    </tbody>
30
  </table>
31
  {% endif %}
32
</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 rel="popup" 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/oidc/add/$', 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
-