0001-manager-add-OpenID-service-handling-20696.patch
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 |
- |