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       | 120 +++++++++++++++++-
 .../manager/oidc_service_detail.html          |  31 +++++
 .../templates/authentic2/manager/service.html |  16 ++-
 .../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, 341 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.urls import reverse
18 19
from django.utils.translation import ugettext as _
19 20

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

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

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

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

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

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

  
91 121

  
92
roles = ServiceView.as_view()
122
service_detail = ServiceView.as_view()
93 123

  
94 124

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

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

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

  
147

  
148
edit_service = ServiceEditView.as_view()
149

  
150

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

  
157

  
158
delete_service = ServiceDeleteView.as_view()
159

  
160

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

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

  
174

  
175
add_oidc_service = OIDCServiceAddView.as_view()
176

  
177

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

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

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

  
193

  
194
oidc_claim_add = OIDCClaimAddView.as_view()
195

  
196

  
197
class BaseClaimView:
198
    model = OIDCClaim
199
    pk_url_kwarg = 'claim_pk'
200

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

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

  
208

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

  
213

  
214
oidc_claim_edit = OIDCClaimEditView.as_view()
215

  
216

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

  
104 220

  
105
edit = ServiceEditView.as_view()
221
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 "OIDC Claims" %}<a href="{% url "a2-manager-oidc-claim-add" service_pk=object.pk %}" class="button" rel="popup">{% trans "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=object.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=object.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
{% if extra_details_template %}
46
  {% include extra_details_template %}
47
{% endif %}
48

  
37 49
<div class="section">
38 50
  <h3>{% trans "Roles of users allowed on this service" %}</h3>
39 51
  <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 OIDC 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 OIDC service' in resp.text
1230
    assert OIDCClient.objects.count() == 0
1231
    assert OIDCClaim.objects.count() == 0
1232

  
1233
    resp = resp.click('Add OIDC 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>OIDC 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
-