0001-support-avatar-picture-in-user-profile-26022.patch
debian-wheezy/control | ||
---|---|---|
25 | 25 |
python-markdown (>= 2.1), |
26 | 26 |
python-ldap (>= 2.4), |
27 | 27 |
python-six (>= 1.0), |
28 |
python-django-filters (>= 1) |
|
28 |
python-django-filters (>= 1), |
|
29 |
python-pil |
|
29 | 30 |
Provides: ${python:Provides} |
30 | 31 |
Recommends: python-ldap |
31 | 32 |
Suggests: python-raven |
debian/control | ||
---|---|---|
27 | 27 |
python-jwcrypto (>= 0.3.1), |
28 | 28 |
python-cryptography (>= 1.3.4), |
29 | 29 |
python-django-filters (>= 1), |
30 |
python-django-filters (<< 2) |
|
30 |
python-django-filters (<< 2), |
|
31 |
python-pil |
|
31 | 32 |
Provides: ${python:Provides} |
32 | 33 |
Recommends: python-ldap |
33 | 34 |
Suggests: python-raven |
setup.py | ||
---|---|---|
131 | 131 |
'XStatic-jQuery', |
132 | 132 |
'XStatic-jquery-ui<1.12', |
133 | 133 |
'xstatic-select2', |
134 |
'pillow', |
|
134 | 135 |
], |
135 | 136 |
zip_safe=False, |
136 | 137 |
classifiers=[ |
src/authentic2/app_settings.py | ||
---|---|---|
145 | 145 |
A2_OPENED_SESSION_COOKIE_NAME=Setting(default='A2_OPENED_SESSION', definition='Authentic session open'), |
146 | 146 |
A2_OPENED_SESSION_COOKIE_DOMAIN=Setting(default=None), |
147 | 147 |
A2_ATTRIBUTE_KINDS=Setting(default=(), definition='List of other attribute kinds'), |
148 |
A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE=Setting(default=200, definition='Max width and height for a profile image'), |
|
148 | 149 |
A2_VALIDATE_EMAIL=Setting(default=False, definition='Validate user email server by doing an RCPT command'), |
149 | 150 |
A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'), |
150 | 151 |
A2_PASSWORD_POLICY_MIN_CLASSES=Setting(default=3, definition='Minimum number of characters classes to be present in passwords'), |
src/authentic2/attribute_kinds.py | ||
---|---|---|
1 | 1 |
import re |
2 | 2 |
import string |
3 | 3 |
import datetime |
4 |
import io |
|
5 |
import hashlib |
|
6 |
import os |
|
4 | 7 | |
5 | 8 |
from itertools import chain |
6 | 9 | |
... | ... | |
9 | 12 |
from django.core.validators import RegexValidator |
10 | 13 |
from django.utils.translation import ugettext_lazy as _, pgettext_lazy |
11 | 14 |
from django.utils.functional import allow_lazy |
15 |
from django.utils import html |
|
12 | 16 |
from django.template.defaultfilters import capfirst |
17 |
from django.core.files import File |
|
18 |
from django.core.files.storage import default_storage |
|
13 | 19 | |
14 | 20 |
from rest_framework import serializers |
15 | 21 | |
16 | 22 |
from .decorators import to_iter |
17 | 23 |
from .plugins import collect_from_plugins |
18 | 24 |
from . import app_settings |
19 |
from .forms import widgets |
|
25 |
from .forms import widgets, fields
|
|
20 | 26 | |
21 | 27 |
capfirst = allow_lazy(capfirst, unicode) |
22 | 28 | |
... | ... | |
100 | 106 |
default_validators = [validate_fr_postcode] |
101 | 107 | |
102 | 108 | |
109 |
class ProfileImageFile(object): |
|
110 |
def __init__(self, name): |
|
111 |
self.name = name |
|
112 | ||
113 |
@property |
|
114 |
def url(self): |
|
115 |
return default_storage.url(self.name) |
|
116 | ||
117 | ||
118 |
def profile_image_serialize(uploadedfile): |
|
119 |
if not uploadedfile: |
|
120 |
return '' |
|
121 |
if hasattr(uploadedfile, 'url'): |
|
122 |
return uploadedfile.name |
|
123 |
h_computation = hashlib.md5() |
|
124 |
for chunk in uploadedfile.chunks(): |
|
125 |
h_computation.update(chunk) |
|
126 |
hexdigest = h_computation.hexdigest() |
|
127 |
stored_file = default_storage.save( |
|
128 |
os.path.join('profile-image', hexdigest), |
|
129 |
uploadedfile) |
|
130 |
return stored_file |
|
131 | ||
132 | ||
133 |
def profile_image_deserialize(name): |
|
134 |
if name: |
|
135 |
return ProfileImageFile(name) |
|
136 |
return None |
|
137 | ||
138 | ||
139 |
def profile_image_html_value(attribute, value): |
|
140 |
if value: |
|
141 |
fragment = u'<a href="%s"><img class="%s" src="%s"/></a>' % ( |
|
142 |
value.url, attribute.name, value.url) |
|
143 |
return html.mark_safe(fragment) |
|
144 |
return '' |
|
145 | ||
146 | ||
147 |
def profile_attributes_ng_serialize(ctx, value): |
|
148 |
if value and getattr(value, 'url', None): |
|
149 |
request = ctx.get('request') |
|
150 |
if request: |
|
151 |
return request.build_absolute_uri(value.url) |
|
152 |
else: |
|
153 |
return value.url |
|
154 |
return None |
|
155 | ||
156 | ||
103 | 157 |
DEFAULT_ALLOW_BLANK = True |
104 | 158 |
DEFAULT_MAX_LENGTH = 256 |
105 | 159 | |
... | ... | |
160 | 214 |
'field_class': PhoneNumberField, |
161 | 215 |
'rest_framework_field_class': PhoneNumberDRFField, |
162 | 216 |
}, |
217 |
{ |
|
218 |
'label': _('profile image'), |
|
219 |
'name': 'profile_image', |
|
220 |
'field_class': fields.ProfileImageField, |
|
221 |
'serialize': profile_image_serialize, |
|
222 |
'deserialize': profile_image_deserialize, |
|
223 |
'rest_framework_field_class': serializers.FileField, |
|
224 |
'rest_framework_field_kwargs': { |
|
225 |
'read_only': True, |
|
226 |
'use_url': True, |
|
227 |
}, |
|
228 |
'html_value': profile_image_html_value, |
|
229 |
'attributes_ng_serialize': profile_attributes_ng_serialize, |
|
230 |
}, |
|
163 | 231 |
] |
164 | 232 | |
165 | 233 |
src/authentic2/attributes_ng/sources/django_user.py | ||
---|---|---|
49 | 49 | |
50 | 50 | |
51 | 51 |
def get_dependencies(instance, ctx): |
52 |
return ('user',) |
|
52 |
return ('user', 'request')
|
|
53 | 53 | |
54 | 54 | |
55 | 55 |
def get_attributes(instance, ctx): |
... | ... | |
68 | 68 |
if user.ou: |
69 | 69 |
for attr in ('uuid', 'slug', 'name'): |
70 | 70 |
ctx['django_user_ou_' + attr] = getattr(user.ou, attr) |
71 |
for av in AttributeValue.objects.with_owner(user): |
|
72 |
ctx['django_user_' + str(av.attribute.name)] = av.to_python() |
|
71 |
for av in AttributeValue.objects.with_owner(user).select_related('attribute'): |
|
72 |
serialize = av.attribute.get_kind().get('attributes_ng_serialize', lambda a, b: b) |
|
73 |
value = av.to_python() |
|
74 |
serialized = serialize(ctx, value) |
|
75 |
ctx['django_user_' + str(av.attribute.name)] = serialized |
|
73 | 76 |
ctx['django_user_' + str(av.attribute.name) + ':verified'] = av.verified |
74 | 77 |
ctx['django_user_groups'] = [group for group in user.groups.all()] |
75 | 78 |
ctx['django_user_group_names'] = [unicode(group) for group in user.groups.all()] |
src/authentic2/forms/fields.py | ||
---|---|---|
1 |
from django.forms import CharField |
|
1 |
import warnings |
|
2 |
import io |
|
3 | ||
4 |
from django.forms import CharField, FileField, ValidationError |
|
5 |
from django.forms.fields import FILE_INPUT_CONTRADICTION |
|
2 | 6 |
from django.utils.translation import ugettext_lazy as _ |
7 |
from django.core.files import File |
|
3 | 8 | |
9 |
from authentic2 import app_settings |
|
4 | 10 |
from authentic2.passwords import password_help_text, validate_password |
5 |
from authentic2.forms.widgets import PasswordInput, NewPasswordInput, CheckPasswordInput |
|
11 |
from authentic2.forms.widgets import (PasswordInput, NewPasswordInput, |
|
12 |
CheckPasswordInput, ProfileImageInput) |
|
13 | ||
14 |
import PIL.Image |
|
6 | 15 | |
7 | 16 | |
8 | 17 |
class PasswordField(CharField): |
... | ... | |
33 | 42 |
} |
34 | 43 |
super(CheckPasswordField, self).__init__(*args, **kwargs) |
35 | 44 | |
45 | ||
46 |
class ProfileImageField(FileField): |
|
47 |
widget = ProfileImageInput |
|
48 | ||
49 |
def __init__(self, *args, **kwargs): |
|
50 |
kwargs.setdefault( |
|
51 |
'help_text', |
|
52 |
_('Image must be JPG or PNG of size less ' |
|
53 |
'than {max_size}x{max_size} pixels').format(max_size=self.max_size)) |
|
54 |
super(ProfileImageField, self).__init__(*args, **kwargs) |
|
55 | ||
56 |
@property |
|
57 |
def max_size(self): |
|
58 |
return app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE |
|
59 | ||
60 |
def clean(self, data, initial=None): |
|
61 |
if data is FILE_INPUT_CONTRADICTION or data is False or data is None: |
|
62 |
return super(ProfileImageField, self).clean(data, initial=initial) |
|
63 |
# we have a file |
|
64 |
try: |
|
65 |
with warnings.catch_warnings(): |
|
66 |
image = PIL.Image.open(io.BytesIO(data.read())) |
|
67 |
except (IOError, PIL.Image.DecompressionBombWarning): |
|
68 |
raise ValidationError(_('The image is not valid')) |
|
69 |
width, height = image.size |
|
70 |
max_size = app_settings.A2_ATTRIBUTE_KIND_PROFILE_IMAGE_MAX_SIZE |
|
71 |
if width > max_size or height > max_size: |
|
72 |
raise ValidationError(_('The image is bigger than {max_size}x{max_size} pixels') |
|
73 |
.format(max_size=self.max_size)) |
|
74 |
new_data = self.file_from_image(image, data.name) |
|
75 |
return super(ProfileImageField, self).clean(new_data, initial=initial) |
|
76 | ||
77 |
def file_from_image(self, image, name=None): |
|
78 |
output = io.BytesIO() |
|
79 |
if image.mode != 'RGB': |
|
80 |
image = image.convert('RGB') |
|
81 |
image.save( |
|
82 |
output, |
|
83 |
format='JPEG', |
|
84 |
quality=99, |
|
85 |
optimize=1) |
|
86 |
output.seek(0) |
|
87 |
return File(output, name=name) |
src/authentic2/forms/widgets.py | ||
---|---|---|
11 | 11 |
import re |
12 | 12 |
import uuid |
13 | 13 | |
14 |
from django.forms.widgets import DateTimeInput, DateInput, TimeInput |
|
14 |
import django |
|
15 |
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, \ |
|
16 |
ClearableFileInput |
|
15 | 17 |
from django.forms.widgets import PasswordInput as BasePasswordInput |
16 | 18 |
from django.utils.formats import get_language, get_format |
17 | 19 |
from django.utils.safestring import mark_safe |
... | ... | |
246 | 248 |
json.dumps(_id), |
247 | 249 |
) |
248 | 250 |
return output |
251 | ||
252 | ||
253 |
class ProfileImageInput(ClearableFileInput): |
|
254 |
if django.VERSION < (1, 9): |
|
255 |
template_with_initial = ( |
|
256 |
'%(initial_text)s: <a href="%(initial_url)s"><img src="%(initial_url)s"/></a> ' |
|
257 |
'%(clear_template)s<br />%(input_text)s: %(input)s' |
|
258 |
) |
|
259 |
else: |
|
260 |
template_name = "authentic2/profile_image_input.html" |
src/authentic2/settings.py | ||
---|---|---|
22 | 22 |
DEBUG = False |
23 | 23 |
DEBUG_DB = False |
24 | 24 |
MEDIA = 'media' |
25 |
MEDIA_ROOT = 'media' |
|
26 |
MEDIA_URL = '/media/' |
|
25 | 27 | |
26 | 28 |
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts |
27 | 29 |
ALLOWED_HOSTS = [] |
src/authentic2/templates/authentic2/accounts_edit.html | ||
---|---|---|
12 | 12 |
{% endblock %} |
13 | 13 | |
14 | 14 |
{% block content %} |
15 |
<form method="post"> |
|
15 |
<form enctype="multipart/form-data" method="post"> |
|
16 | ||
16 | 17 |
{% csrf_token %} |
17 | 18 |
{{ form.as_p }} |
18 | 19 |
{% if form.instance and form.instance.id %} |
src/authentic2/templates/authentic2/profile_image_input.html | ||
---|---|---|
1 |
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}"><img src="{{ widget.value.url }}"/></a>{% if not widget.required %} |
|
2 |
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}" /> |
|
3 |
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br /> |
|
4 |
{{ widget.input_text }}:{% endif %} |
|
5 |
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} /> |
src/authentic2/templates/registration/registration_form.html | ||
---|---|---|
15 | 15 | |
16 | 16 |
<h2>{{ view.title }}</h2> |
17 | 17 | |
18 |
<form method="post"> |
|
18 |
<form enctype="multipart/form-data" method="post"> |
|
19 | ||
19 | 20 |
{% csrf_token %} |
20 | 21 |
{{ form.as_p }} |
21 | 22 |
<button class="submit-button">{% trans 'Submit' %}</button> |
src/authentic2/urls.py | ||
---|---|---|
2 | 2 |
from django.conf import settings |
3 | 3 |
from django.contrib import admin |
4 | 4 |
from django.contrib.staticfiles.views import serve |
5 |
from django.views.static import serve as media_serve |
|
5 | 6 | |
6 | 7 |
from . import app_settings, plugins, views |
7 | 8 | |
... | ... | |
44 | 45 |
urlpatterns += [ |
45 | 46 |
url(r'^static/(?P<path>.*)$', serve) |
46 | 47 |
] |
48 |
urlpatterns += [ |
|
49 |
url(r'^media/(?P<path>.*)$', media_serve, { |
|
50 |
'document_root': settings.MEDIA_ROOT}) |
|
51 |
] |
|
47 | 52 | |
48 | 53 |
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS: |
49 | 54 |
import debug_toolbar |
src/authentic2/views.py | ||
---|---|---|
444 | 444 |
if attribute: |
445 | 445 |
if not attribute.user_visible: |
446 | 446 |
continue |
447 |
html_value = attribute.get_kind().get('html_value', lambda a, b: b) |
|
447 | 448 |
qs = models.AttributeValue.objects.with_owner(request.user) |
448 | 449 |
qs = qs.filter(attribute=attribute) |
449 | 450 |
qs = qs.select_related() |
450 | 451 |
value = [at_value.to_python() for at_value in qs] |
451 | 452 |
value = filter(None, value) |
453 |
value = [html_value(attribute, at_value) for at_value in value] |
|
452 | 454 |
if not title: |
453 | 455 |
title = unicode(attribute) |
454 | 456 |
else: |
src/authentic2_idp_oidc/utils.py | ||
---|---|---|
161 | 161 |
return values_list |
162 | 162 | |
163 | 163 | |
164 |
def create_user_info(client, user, scope_set, id_token=False): |
|
164 |
def create_user_info(request, client, user, scope_set, id_token=False):
|
|
165 | 165 |
'''Create user info dictionnary''' |
166 | 166 |
user_info = { |
167 | 167 |
'sub': make_sub(client, user) |
168 | 168 |
} |
169 | 169 |
attributes = get_attributes({ |
170 |
'user': user, 'request': None, 'service': client, |
|
171 |
'__wanted_attributes': client.get_wanted_attributes()}) |
|
170 |
'user': user, |
|
171 |
'request': request, |
|
172 |
'service': client, |
|
173 |
'__wanted_attributes': client.get_wanted_attributes(), |
|
174 |
}) |
|
172 | 175 |
for claim in client.oidcclaim_set.filter(name__isnull=False): |
173 | 176 |
if not set(claim.get_scopes()).intersection(scope_set): |
174 | 177 |
continue |
src/authentic2_idp_oidc/views.py | ||
---|---|---|
279 | 279 |
acr = '0' |
280 | 280 |
if nonce is not None and last_auth.get('nonce') == nonce: |
281 | 281 |
acr = '1' |
282 |
id_token = utils.create_user_info(client, request.user, scopes, id_token=True) |
|
282 |
id_token = utils.create_user_info(request, |
|
283 |
client, |
|
284 |
request.user, |
|
285 |
scopes, |
|
286 |
id_token=True) |
|
283 | 287 |
id_token.update({ |
284 | 288 |
'iss': utils.get_issuer(request), |
285 | 289 |
'aud': client.client_id, |
... | ... | |
386 | 390 |
oidc_code.nonce): |
387 | 391 |
acr = '1' |
388 | 392 |
# prefill id_token with user info |
389 |
id_token = utils.create_user_info(client, oidc_code.user, oidc_code.scope_set(), id_token=True) |
|
393 |
id_token = utils.create_user_info( |
|
394 |
request, |
|
395 |
client, |
|
396 |
oidc_code.user, |
|
397 |
oidc_code.scope_set(), |
|
398 |
id_token=True) |
|
390 | 399 |
id_token.update({ |
391 | 400 |
'iss': utils.get_issuer(request), |
392 | 401 |
'sub': utils.make_sub(client, oidc_code.user), |
... | ... | |
430 | 439 |
access_token = authenticate_access_token(request) |
431 | 440 |
if access_token is None: |
432 | 441 |
return HttpResponse('unauthenticated', status=401) |
433 |
user_info = utils.create_user_info(access_token.client, access_token.user, |
|
442 |
user_info = utils.create_user_info(request, |
|
443 |
access_token.client, |
|
444 |
access_token.user, |
|
434 | 445 |
access_token.scope_set()) |
435 | 446 |
return HttpResponse(json.dumps(user_info), content_type='application/json') |
436 | 447 |
tests/conftest.py | ||
---|---|---|
339 | 339 |
activate('fr') |
340 | 340 |
yield |
341 | 341 |
deactivate() |
342 | ||
343 | ||
344 |
@pytest.fixture |
|
345 |
def media(settings, tmpdir): |
|
346 |
settings.MEDIA_ROOT = str(tmpdir.mkdir('media')) |
tests/test_attribute_kinds.py | ||
---|---|---|
5 | 5 |
from authentic2.models import Attribute |
6 | 6 | |
7 | 7 |
from utils import get_link_from_mail |
8 |
from webtest import Upload |
|
8 | 9 | |
9 | 10 | |
10 | 11 |
def test_string(db, app, admin, mailoutbox): |
... | ... | |
369 | 370 |
app.post_json('/api/users/', params=payload) |
370 | 371 |
assert qs.get().attributes.birthdate == datetime.date(1900, 1, 1) |
371 | 372 |
qs.delete() |
373 | ||
374 | ||
375 |
def test_profile_image(db, app, admin, mailoutbox, media): |
|
376 |
Attribute.objects.create(name='cityscape_image', label='cityscape', kind='profile_image', |
|
377 |
asked_on_registration=True, required=False, |
|
378 |
user_visible=True, user_editable=True) |
|
379 | ||
380 |
def john(): |
|
381 |
return User.objects.get(first_name='John') |
|
382 | ||
383 |
response = app.get('/accounts/register/') |
|
384 |
form = response.form |
|
385 |
form.set('email', 'john.doe@example.com') |
|
386 |
response = form.submit().follow() |
|
387 |
assert 'john.doe@example.com' in response |
|
388 |
url = get_link_from_mail(mailoutbox[0]) |
|
389 |
response = app.get(url) |
|
390 | ||
391 |
# verify empty file is refused |
|
392 |
form = response.form |
|
393 |
form.set('first_name', 'John') |
|
394 |
form.set('last_name', 'Doe') |
|
395 |
form.set('cityscape_image', Upload('/dev/null')) |
|
396 |
form.set('password1', '12345abcdA') |
|
397 |
form.set('password2', '12345abcdA') |
|
398 |
response = form.submit() |
|
399 |
assert response.pyquery.find('.form-field-error #id_cityscape_image') |
|
400 | ||
401 |
# verify 201x201 image is refused |
|
402 |
form = response.form |
|
403 |
form.set('cityscape_image', Upload('tests/201x201.jpg')) |
|
404 |
form.set('password1', '12345abcdA') |
|
405 |
form.set('password2', '12345abcdA') |
|
406 |
response = form.submit() |
|
407 |
assert response.pyquery.find('.form-field-error #id_cityscape_image') |
|
408 | ||
409 |
# verify 200x200 image is accepted |
|
410 |
form = response.form |
|
411 |
form.set('cityscape_image', Upload('tests/200x200.jpg')) |
|
412 |
form.set('password1', '12345abcdA') |
|
413 |
form.set('password2', '12345abcdA') |
|
414 |
response = form.submit() |
|
415 |
assert john().attributes.cityscape_image |
|
416 | ||
417 |
# verify API serves absolute URL for profile images |
|
418 |
app.authorization = ('Basic', (admin.username, admin.username)) |
|
419 |
response = app.get('/api/users/%s/' % john().uuid) |
|
420 |
assert response.json['cityscape_image'] == 'http://testserver/media/%s' % john().attributes.cityscape_image.name |
|
421 |
app.authorization = None |
|
422 | ||
423 |
# verify we can clear the image |
|
424 |
response = app.get('/accounts/edit/') |
|
425 |
form = response.form |
|
426 |
form.set('edit-profile-first_name', 'John') |
|
427 |
form.set('edit-profile-last_name', 'Doe') |
|
428 |
form.set('edit-profile-cityscape_image-clear', True) |
|
429 |
response = form.submit() |
|
430 |
assert john().attributes.cityscape_image == None |
|
431 | ||
432 |
# verify API serves absolute URL for profile images |
|
433 |
app.authorization = ('Basic', (admin.username, admin.username)) |
|
434 |
response = app.get('/api/users/%s/' % john().uuid) |
|
435 |
assert response.json['cityscape_image'] is None |
tests/test_idp_oidc.py | ||
---|---|---|
11 | 11 |
import utils |
12 | 12 | |
13 | 13 |
from django.core.urlresolvers import reverse |
14 |
from django.core.files import File |
|
14 | 15 |
from django.db import connection |
15 | 16 |
from django.db.migrations.executor import MigrationExecutor |
16 | 17 |
from django.utils.timezone import now |
... | ... | |
19 | 20 | |
20 | 21 |
User = get_user_model() |
21 | 22 | |
23 |
from authentic2.models import Attribute |
|
22 | 24 |
from authentic2_idp_oidc.models import OIDCClient, OIDCAuthorization, OIDCCode, OIDCAccessToken, OIDCClaim |
23 | 25 |
from authentic2_idp_oidc.utils import make_sub |
24 | 26 |
from authentic2.a2_rbac.utils import get_default_ou |
... | ... | |
98 | 100 | |
99 | 101 | |
100 | 102 |
@pytest.fixture(params=OIDC_CLIENT_PARAMS) |
101 |
def oidc_client(request, superuser, app): |
|
103 |
def oidc_client(request, superuser, app, simple_user, media): |
|
104 |
Attribute.objects.create( |
|
105 |
name='cityscape_image', |
|
106 |
label='cityscape', |
|
107 |
kind='profile_image', |
|
108 |
asked_on_registration=True, |
|
109 |
required=False, |
|
110 |
user_visible=True, |
|
111 |
user_editable=True) |
|
112 | ||
102 | 113 |
url = reverse('admin:authentic2_idp_oidc_oidcclient_add') |
103 | 114 |
assert OIDCClient.objects.count() == 0 |
104 | 115 |
response = utils.login(app, superuser, path=url) |
... | ... | |
256 | 267 |
# when adding extra attributes |
257 | 268 |
OIDCClaim.objects.create(client=oidc_client, name='ou', value='django_user_ou_name', scopes='profile') |
258 | 269 |
OIDCClaim.objects.create(client=oidc_client, name='roles', value='a2_role_names', scopes='profile, role') |
270 |
OIDCClaim.objects.create(client=oidc_client, |
|
271 |
name='cityscape_image', |
|
272 |
value='django_user_cityscape_image', |
|
273 |
scopes='profile') |
|
259 | 274 |
simple_user.roles.add(get_role_model().objects.create( |
260 | 275 |
name='Whatever', slug='whatever', ou=get_default_ou())) |
261 | 276 |
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token)) |
262 | 277 |
assert response.json['ou'] == simple_user.ou.name |
263 | 278 |
assert response.json['roles'][0] == 'Whatever' |
279 |
assert response.json.get('cityscape_image') is None |
|
280 |
simple_user.attributes.cityscape_image = File(open('tests/200x200.jpg')) |
|
281 |
response = app.get(user_info_url, headers=bearer_authentication_headers(access_token)) |
|
282 |
assert response.json['cityscape_image'].startswith('http://testserver/media/profile-image/') |
|
264 | 283 | |
265 | 284 |
# check against a user without username |
266 | 285 |
simple_user.username = None |
tests/test_idp_saml2.py | ||
---|---|---|
1 |
import re |
|
1 | 2 |
import datetime |
2 | 3 |
import base64 |
3 | 4 |
import unittest |
... | ... | |
9 | 10 |
from django.test.utils import override_settings |
10 | 11 |
from django.contrib.auth import get_user_model, REDIRECT_FIELD_NAME |
11 | 12 |
from django.core.urlresolvers import reverse |
13 |
from django.core.files import File |
|
12 | 14 |
from django.utils.translation import gettext as _ |
13 | 15 | |
14 | 16 |
from authentic2.saml import models as saml_models |
... | ... | |
85 | 87 |
self.code_attribute = Attribute.objects.create(kind='string', name='code', label='Code') |
86 | 88 |
self.mobile_attribute = Attribute.objects.create(kind='string', name='mobile', |
87 | 89 |
label='Mobile') |
90 |
self.avatar_attribute = Attribute.objects.create( |
|
91 |
kind='profile_image', |
|
92 |
name='avatar', |
|
93 |
label='Avatar') |
|
88 | 94 |
self.user = get_user_model().objects.create( |
89 | 95 |
email=self.email, |
90 | 96 |
username=self.username, |
... | ... | |
92 | 98 |
last_name=self.last_name) |
93 | 99 |
self.code_attribute.set_value(self.user, '1234', verified=True) |
94 | 100 |
self.mobile_attribute.set_value(self.user, '5678', verified=True) |
101 |
self.avatar_attribute.set_value(self.user, File(open('tests/200x200.jpg'))) |
|
95 | 102 |
self.user.set_password(self.password) |
96 | 103 |
self.user.save() |
97 | 104 |
self.default_ou = OrganizationalUnit.objects.get() |
... | ... | |
154 | 161 |
name='verified_attributes', |
155 | 162 |
friendly_name='Verified attributes', |
156 | 163 |
attribute_name='@verified_attributes@') |
164 |
self.liberty_provider.attributes.create( |
|
165 |
name_format='basic', |
|
166 |
name='avatar', |
|
167 |
friendly_name='Avatar', |
|
168 |
attribute_name='django_user_avatar') |
|
157 | 169 |
self.role_authorized = Role.objects.create(name='PC Delta', slug='pc-delta') |
158 | 170 |
self.liberty_provider.unauthorized_url = 'https://whatever.com/loser/' |
159 | 171 |
self.liberty_provider.save() |
... | ... | |
406 | 418 |
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='mobile']/" |
407 | 419 |
"saml:AttributeValue", '5678'), |
408 | 420 | |
421 |
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='avatar']/" |
|
422 |
"@NameFormat", lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC), |
|
423 |
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='avatar']/" |
|
424 |
"@FriendlyName", 'Avatar'), |
|
425 |
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='avatar']/" |
|
426 |
"saml:AttributeValue", re.compile('^http://testserver/media/profile-image/.*$')), |
|
427 | ||
409 | 428 |
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='verified_attributes']/" |
410 | 429 |
"@NameFormat", lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC), |
411 | 430 |
("/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='verified_attributes']/" |
tests/utils.py | ||
---|---|---|
121 | 121 |
self.assertEqual(set(values), content) |
122 | 122 |
elif isinstance(content, list): |
123 | 123 |
self.assertEqual(values, content) |
124 |
elif hasattr(content, 'pattern'): |
|
125 |
for value in values: |
|
126 |
self.assertRegexpMatches( |
|
127 |
value, content, |
|
128 |
msg='xpath %s does not match regexp %s' % (xpath, content.pattern)) |
|
124 | 129 |
else: |
125 | 130 |
raise NotImplementedError('comparing xpath result to type %s: %r is not ' |
126 | 131 |
'implemented' % (type(content), content)) |
127 |
- |