idp oidc: add extra attributes configuration (#21870)
This commit is contained in:
parent
87bcb45cbe
commit
f06900ead4
|
@ -1,11 +1,13 @@
|
|||
import logging
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from ..decorators import to_list
|
||||
from ..decorators import to_iter, to_list
|
||||
from .. import app_settings, plugins, utils
|
||||
|
||||
__ALL__ = ['get_attribute_names', 'get_attributes']
|
||||
__ALL__ = ['get_attribute_names', 'get_attributes', 'get_service_attributes']
|
||||
|
||||
|
||||
class UnsortableError(Exception):
|
||||
'''
|
||||
|
@ -93,3 +95,10 @@ def get_attributes(ctx):
|
|||
for source, instance in source_and_instances:
|
||||
ctx.update(source.get_attributes(instance, ctx.copy()))
|
||||
return ctx
|
||||
|
||||
|
||||
@to_iter
|
||||
def get_service_attributes(service):
|
||||
ctx = {'request': None, 'user': None, 'service': service}
|
||||
return ([('', _('None'))] + get_attribute_names(ctx)
|
||||
+ [('@verified_attributes@', _('List of verified attributes'))])
|
||||
|
|
|
@ -18,7 +18,7 @@ from authentic2.saml.models import (LibertyProvider, LibertyServiceProvider,
|
|||
KeyValue, LibertySession, SAMLAttribute)
|
||||
|
||||
from authentic2.decorators import to_iter
|
||||
from authentic2.attributes_ng.engine import get_attribute_names
|
||||
from authentic2.attributes_ng.engine import get_service_attributes
|
||||
|
||||
from . import admin_views
|
||||
|
||||
|
@ -107,19 +107,10 @@ class SAMLAttributeInlineForm(forms.ModelForm):
|
|||
def __init__(self, *args, **kwargs):
|
||||
service = kwargs.pop('service', None)
|
||||
super(SAMLAttributeInlineForm, self).__init__(*args, **kwargs)
|
||||
choices = self.choices({
|
||||
'user': None,
|
||||
'request': None,
|
||||
'service': service,
|
||||
})
|
||||
choices = get_service_attributes(service)
|
||||
self.fields['attribute_name'].choices = choices
|
||||
self.fields['attribute_name'].widget = forms.Select(choices=choices)
|
||||
|
||||
@to_iter
|
||||
def choices(self, ctx):
|
||||
return ([('', _('None'))] + get_attribute_names(ctx)
|
||||
+ [('@verified_attributes@', _('List of verified attributes'))])
|
||||
|
||||
class Meta:
|
||||
model = SAMLAttribute
|
||||
fields = [
|
||||
|
|
|
@ -1,13 +1,55 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.utils.functional import curry
|
||||
|
||||
from authentic2.attributes_ng.engine import get_service_attributes
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class OIDCClaimInlineForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OIDCClaimInlineForm, self).__init__(*args, **kwargs)
|
||||
choices = get_service_attributes(self.instance.client_id)
|
||||
self.fields['value'].choices = choices
|
||||
self.fields['value'].widget = forms.Select(choices=choices)
|
||||
|
||||
class Meta:
|
||||
model = models.OIDCClaim
|
||||
fields = ['name', 'value', 'scopes']
|
||||
|
||||
|
||||
class OIDCClaimInlineAdmin(admin.TabularInline):
|
||||
|
||||
model = models.OIDCClaim
|
||||
form = OIDCClaimInlineForm
|
||||
extra = 0
|
||||
|
||||
def get_formset(self, request, obj=None, **kwargs):
|
||||
initial = []
|
||||
# formsets are only saved if formset.has_changed() is True, so only set initial
|
||||
# values on the GET (display of the creation form)
|
||||
if request.method == 'GET' and not obj:
|
||||
initial.extend([
|
||||
{'name': 'preferred_username', 'value': 'django_user_username', 'scopes': 'profile'},
|
||||
{'name': 'given_name', 'value': 'django_user_first_name', 'scopes': 'profile'},
|
||||
{'name': 'family_name', 'value': 'django_user_last_name', 'scopes': 'profile'},
|
||||
{'name': 'email', 'value': 'django_user_email', 'scopes': 'email'},
|
||||
{'name': 'email_verified', 'value': 'django_user_email_verified', 'scopes': 'email'},
|
||||
])
|
||||
self.extra = 5
|
||||
formset = super(OIDCClaimInlineAdmin, self).get_formset(request, obj=obj, **kwargs)
|
||||
formset.__init__ = curry(formset.__init__, initial=initial)
|
||||
return formset
|
||||
|
||||
|
||||
class OIDCClientAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'client_id', 'ou', 'identifier_policy', 'created', 'modified']
|
||||
list_filter = ['ou', 'identifier_policy']
|
||||
date_hierarchy = 'modified'
|
||||
readonly_fields = ['created', 'modified']
|
||||
inlines = [OIDCClaimInlineAdmin]
|
||||
|
||||
|
||||
class OIDCAuthorizationAdmin(admin.ModelAdmin):
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_odcclient_default_claims(apps, schema_editor):
|
||||
OIDCClient = apps.get_model('authentic2_idp_oidc', 'OIDCClient')
|
||||
OIDCClaim = apps.get_model('authentic2_idp_oidc', 'OIDCClaim')
|
||||
for oidcclient in OIDCClient.objects.all():
|
||||
OIDCClaim.objects.create(client=oidcclient, name='preferred_username', value='django_user_username', scopes='profile')
|
||||
OIDCClaim.objects.create(client=oidcclient, name='given_name', value='django_user_first_name', scopes='profile')
|
||||
OIDCClaim.objects.create(client=oidcclient, name='family_name', value='django_user_last_name', scopes='profile')
|
||||
OIDCClaim.objects.create(client=oidcclient, name='email', value='django_user_email', scopes='email')
|
||||
OIDCClaim.objects.create(client=oidcclient, name='email_verified', value='django_user_email_verified', scopes='email')
|
||||
|
||||
|
||||
def unset_odcclient_default_claims(apps, schema_editor):
|
||||
OIDCClient = apps.get_model('authentic2_idp_oidc', 'OIDCClient')
|
||||
for oidcclient in OIDCClient.objects.all():
|
||||
oidcclient.oidcclaim_set.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentic2_idp_oidc', '0009_auto_20180313_1156'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OIDCClaim',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(max_length=128, verbose_name='attribute name', blank=True)),
|
||||
('value', models.CharField(max_length=128, verbose_name='attribute value', blank=True)),
|
||||
('scopes', models.CharField(max_length=128, verbose_name='attribute scopes', blank=True)),
|
||||
('client', models.ForeignKey(verbose_name='client', to='authentic2_idp_oidc.OIDCClient')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(set_odcclient_default_claims, unset_odcclient_default_claims),
|
||||
]
|
|
@ -2,6 +2,7 @@ import uuid
|
|||
from importlib import import_module
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -9,6 +10,7 @@ from django.conf import settings
|
|||
from django.utils.timezone import now
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
|
||||
from authentic2.managers import GenericManager
|
||||
from authentic2.models import Service
|
||||
|
||||
from . import utils, managers
|
||||
|
@ -40,7 +42,7 @@ class OIDCClient(Service):
|
|||
POLICY_UUID = 1
|
||||
POLICY_PAIRWISE = 2
|
||||
POLICY_EMAIL = 3
|
||||
POLICY_PAIRWISE_REVERSIBLE = 4
|
||||
POLICY_PAIRWISE_REVERSIBLE = 4
|
||||
|
||||
IDENTIFIER_POLICIES = [
|
||||
(POLICY_UUID, _('uuid')),
|
||||
|
@ -139,6 +141,9 @@ class OIDCClient(Service):
|
|||
self.redirect_uris = strip_words(self.redirect_uris)
|
||||
self.post_logout_redirect_uris = strip_words(self.post_logout_redirect_uris)
|
||||
|
||||
def get_wanted_attributes(self):
|
||||
return self.oidcclaim_set.filter(name__isnull=False).values_list('value', flat=True)
|
||||
|
||||
def __repr__(self):
|
||||
return ('<OIDCClient name:%r client_id:%r identifier_policy:%r>' %
|
||||
(self.name, self.client_id, self.get_identifier_policy_display()))
|
||||
|
@ -293,3 +298,23 @@ GenericRelation('authentic2_idp_oidc.OIDCAuthorization',
|
|||
content_type_field='client_ct',
|
||||
object_id_field='client_id').contribute_to_class(
|
||||
OrganizationalUnit, 'oidc_authorizations')
|
||||
|
||||
|
||||
class OIDCClaim(models.Model):
|
||||
client = models.ForeignKey(
|
||||
to=OIDCClient, verbose_name=_('client'))
|
||||
name = models.CharField(
|
||||
max_length=128, blank=True,
|
||||
verbose_name=_('attribute name'))
|
||||
value = models.CharField(
|
||||
max_length=128, blank=True,
|
||||
verbose_name=_('attribute value'))
|
||||
scopes = models.CharField(
|
||||
max_length=128, blank=True,
|
||||
verbose_name=_('attribute scopes'))
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s - %s - %s' % (self.name, self.value, self.scopes)
|
||||
|
||||
def get_scopes(self):
|
||||
return self.scopes.strip().split(',')
|
||||
|
|
|
@ -12,6 +12,7 @@ from django.conf import settings
|
|||
from django.utils.encoding import smart_bytes
|
||||
|
||||
from authentic2 import hooks, crypto
|
||||
from authentic2.attributes_ng.engine import get_attributes
|
||||
|
||||
from . import app_settings
|
||||
|
||||
|
@ -149,29 +150,32 @@ def reverse_pairwise_sub(client, sub):
|
|||
return None
|
||||
|
||||
|
||||
def normalize_claim_values(values):
|
||||
values_list = []
|
||||
if isinstance(values, basestring) or not hasattr(values, '__iter__'):
|
||||
return values
|
||||
for value in values:
|
||||
if isinstance(value, bool):
|
||||
value = str(value).lower()
|
||||
values_list.append(value)
|
||||
return values_list
|
||||
|
||||
|
||||
def create_user_info(client, user, scope_set, id_token=False):
|
||||
'''Create user info dictionnary'''
|
||||
user_info = {
|
||||
'sub': make_sub(client, user)
|
||||
}
|
||||
if 'profile' in scope_set:
|
||||
user_info['family_name'] = user.last_name
|
||||
user_info['given_name'] = user.first_name
|
||||
if user.username:
|
||||
user_info['preferred_username'] = user.username.split('@', 1)[0]
|
||||
if 'email' in scope_set:
|
||||
user_info['email'] = user.email
|
||||
user_info['email_verified'] = True
|
||||
if 'roles' in scope_set:
|
||||
roles = user_info['roles'] = []
|
||||
for role in user.roles_and_parents().select_related('ou'):
|
||||
roles.append({
|
||||
'uuid': role.uuid,
|
||||
'name': role.name,
|
||||
'slug': role.slug,
|
||||
'ou__name': role.ou.name,
|
||||
'ou__slug': role.ou.slug
|
||||
})
|
||||
attributes = get_attributes({
|
||||
'user': user, 'request': None, 'service': client,
|
||||
'__wanted_attributes': client.get_wanted_attributes()})
|
||||
for claim in client.oidcclaim_set.filter(name__isnull=False):
|
||||
if not set(claim.get_scopes()).intersection(scope_set):
|
||||
continue
|
||||
user_info[claim.name] = normalize_claim_values(attributes[claim.value])
|
||||
# check if attribute is verified
|
||||
if claim.value + ':verified' in attributes:
|
||||
user_info[claim.value + '_verified'] = True
|
||||
hooks.call_hooks('idp_oidc_modify_user_info', client, user, scope_set, user_info)
|
||||
return user_info
|
||||
|
||||
|
|
|
@ -13,13 +13,15 @@ import utils
|
|||
from django.core.urlresolvers import reverse
|
||||
from django.utils.timezone import now
|
||||
|
||||
from authentic2_idp_oidc.models import OIDCClient, OIDCAuthorization, OIDCCode, OIDCAccessToken
|
||||
from authentic2_idp_oidc.models import OIDCClient, OIDCAuthorization, OIDCCode, OIDCAccessToken, OIDCClaim
|
||||
from authentic2_idp_oidc.utils import make_sub
|
||||
from authentic2.a2_rbac.utils import get_default_ou
|
||||
from authentic2.utils import make_url
|
||||
from authentic2_auth_oidc.utils import parse_timestamp
|
||||
from django_rbac.utils import get_role_model
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
JWKSET = {
|
||||
"keys": [
|
||||
{
|
||||
|
@ -231,7 +233,7 @@ def test_authorization_code_sso(login_first, oidc_settings, oidc_client, simple_
|
|||
assert claims['given_name'] == simple_user.first_name
|
||||
assert claims['family_name'] == simple_user.last_name
|
||||
assert claims['email'] == simple_user.email
|
||||
assert claims['email_verified'] is True
|
||||
assert claims['email_verified'] is False
|
||||
|
||||
user_info_url = make_url('oidc-user-info')
|
||||
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
|
||||
|
@ -240,7 +242,16 @@ def test_authorization_code_sso(login_first, oidc_settings, oidc_client, simple_
|
|||
assert response.json['given_name'] == simple_user.first_name
|
||||
assert response.json['family_name'] == simple_user.last_name
|
||||
assert response.json['email'] == simple_user.email
|
||||
assert response.json['email_verified'] is True
|
||||
assert response.json['email_verified'] is False
|
||||
|
||||
# when adding extra attributes
|
||||
OIDCClaim.objects.create(client=oidc_client, name='ou', value='django_user_ou_name', scopes='profile')
|
||||
OIDCClaim.objects.create(client=oidc_client, name='roles', value='a2_role_names', scopes='profile, role')
|
||||
simple_user.roles.add(get_role_model().objects.create(
|
||||
name='Whatever', slug='whatever', ou=get_default_ou()))
|
||||
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token))
|
||||
assert response.json['ou'] == simple_user.ou.name
|
||||
assert response.json['roles'][0] == 'Whatever'
|
||||
|
||||
# Now logout
|
||||
if oidc_client.post_logout_redirect_uris:
|
||||
|
@ -830,3 +841,25 @@ def test_registration_service_slug(oidc_settings, app, simple_oidc_client, simpl
|
|||
assert hooks.event[2]['kwargs']['name'] == 'login'
|
||||
assert hooks.event[2]['kwargs']['how'] == 'email'
|
||||
assert hooks.event[2]['kwargs']['service'] == 'client'
|
||||
|
||||
|
||||
def test_oidclient_claims_data_migration():
|
||||
from django.db import connection
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
|
||||
executor = MigrationExecutor(connection)
|
||||
app = 'authentic2_idp_oidc'
|
||||
migrate_from = [(app, '0009_auto_20180313_1156')]
|
||||
migrate_to = [(app, '0010_oidcclaim')]
|
||||
executor.migrate(migrate_from)
|
||||
executor.loader.build_graph()
|
||||
|
||||
old_apps = executor.loader.project_state(migrate_from).apps
|
||||
OIDCClient = old_apps.get_model('authentic2_idp_oidc', 'OIDCClient')
|
||||
client = OIDCClient(name='test', slug='test', redirect_uris='https://example.net/')
|
||||
client.save()
|
||||
|
||||
executor.migrate(migrate_to)
|
||||
executor.loader.build_graph()
|
||||
client = OIDCClient.objects.first()
|
||||
assert OIDCClaim.objects.filter(client=client.id).count() == 5
|
||||
|
|
Loading…
Reference in New Issue