idp oidc: add extra attributes configuration (#21870)

This commit is contained in:
Josue Kouka 2018-04-13 09:17:19 +02:00
parent 87bcb45cbe
commit f06900ead4
7 changed files with 181 additions and 35 deletions

View File

@ -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'))])

View File

@ -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 = [

View File

@ -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):

View File

@ -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),
]

View File

@ -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(',')

View File

@ -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

View File

@ -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