wcs: implement and use a generic way to do signed requests (#10492)

This commit is contained in:
Frédéric Péters 2016-03-30 19:05:24 +02:00
parent b663799b49
commit 47f9f7c8ca
5 changed files with 118 additions and 95 deletions

View File

@ -28,7 +28,7 @@ class WcsFormCellForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(WcsFormCellForm, self).__init__(*args, **kwargs)
formdef_references = get_wcs_options('json')
formdef_references = get_wcs_options('/api/formdefs/')
self.fields['formdef_reference'].widget = forms.Select(choices=formdef_references)
@ -39,7 +39,7 @@ class WcsCategoryCellForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(WcsCategoryCellForm, self).__init__(*args, **kwargs)
references = get_wcs_options('categories')
references = get_wcs_options('/api/categories/')
self.fields['category_reference'].widget = forms.Select(choices=references)
@ -85,8 +85,8 @@ class WcsFormsOfCategoryCellForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(WcsFormsOfCategoryCellForm, self).__init__(*args, **kwargs)
references = get_wcs_options('categories')
formdef_references = get_wcs_options('json', include_category_slug=True)
references = get_wcs_options('/api/categories/')
formdef_references = get_wcs_options('/api/formdefs/', include_category_slug=True)
self.fields['ordering'].widget = forms.Select(
choices=self.fields['ordering'].choices,
attrs={'class': 'ordering-select'})

View File

@ -14,15 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import collections
import hashlib
import json
import logging
import requests
import urllib
from django import template
from django.core.cache import cache
from django.db import models
from django.forms import models as model_forms
from django.forms import Select
@ -32,12 +24,10 @@ from jsonfield import JSONField
from combo.data.models import CellBase
from combo.data.library import register_cell_class
from combo.utils import NothingInCacheException, sign_url
from combo.utils import requests
from .utils import get_wcs_json, is_wcs_enabled, get_wcs_services
logger = logging.getLogger(__name__)
@register_cell_class
class WcsFormCell(CellBase):
formdef_reference = models.CharField(_('Form'), max_length=150)
@ -157,41 +147,14 @@ class WcsBlurpMixin(object):
url += '/'
wcs_site['base_url'] = url
url += self.api_url
if not '?' in url:
url += '?'
else:
url += '&'
url += 'format=json'
if wcs_site.get('orig') and wcs_site.get('secret'):
url += '&orig=%s' % wcs_site['orig']
if context.get('user') and context['user'].is_authenticated():
if context.get('request') and hasattr(context['request'], 'session') \
and context['request'].session.get('mellon_session'):
mellon = context['request'].session['mellon_session']
nameid = mellon['name_id_content']
url += '&NameID=' + urllib.quote(nameid)
elif hasattr(context['user'], 'email') and context['user'].email:
url += '&email=' + urllib.quote(context['user'].email)
else:
# add an empty user
url += '&NameID='
cache_key = hashlib.md5(url).hexdigest()
cache_content = cache.get(cache_key)
if not cache_content:
if not context.get('synchronous'):
raise NothingInCacheException()
source_response = requests.get(sign_url(
url, wcs_site['secret']),
verify=False)
if source_response.status_code == 200:
cache_content = json.loads(source_response.content)
cache.set(cache_key, cache_content, self.cache_duration)
else:
logger.error('failed to load %s', url)
cache_content = None
wcs_site['data'] = cache_content
response = requests.get(
self.api_url,
remote_service=wcs_site,
request=context.get('request'),
cache_duration=self.cache_duration,
raise_if_not_cached=not(context.get('synchronous')))
if response.status_code == 200:
wcs_site['data'] = response.json()
return wcs_sites
@ -234,7 +197,7 @@ class WcsUserDataBaseCell(WcsDataBaseCell):
@register_cell_class
class WcsCurrentFormsCell(WcsUserDataBaseCell):
api_url = 'api/user/forms'
api_url = '/api/user/forms'
variable_name = 'user_forms'
current_forms = models.BooleanField(_('Current Forms'), default=True)
@ -285,7 +248,7 @@ class WcsCurrentFormsCell(WcsUserDataBaseCell):
@register_cell_class
class WcsCurrentDraftsCell(WcsUserDataBaseCell):
api_url = 'myspace/drafts'
api_url = '/api/user/drafts'
variable_name = 'current_drafts'
template_name = 'combo/wcs/current_drafts.html'
@ -323,7 +286,7 @@ class WcsFormsOfCategoryCell(WcsCommonCategoryCell, WcsBlurpMixin):
@property
def api_url(self):
return self.category_reference.split(':')[1] + '/json'
return '/api/categories/%s/formdefs/' % self.category_reference.split(':')[1]
def is_relevant(self, user=None):
return bool(self.category_reference)
@ -368,7 +331,7 @@ class WcsFormsOfCategoryCell(WcsCommonCategoryCell, WcsBlurpMixin):
@register_cell_class
class CategoriesCell(WcsDataBaseCell):
api_url = 'api/categories/?full=on'
api_url = '/api/categories/?full=on'
variable_name = 'form_categories'
template_name = 'combo/wcs/form_categories.html'
cache_duration = 600

View File

@ -14,12 +14,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import requests
from django.conf import settings
from django.core.cache import cache
from combo.utils import sign_url
from combo.utils import requests
def is_wcs_enabled(cls):
return hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get('wcs')
@ -30,20 +27,8 @@ def get_wcs_services():
return settings.KNOWN_SERVICES.get('wcs')
def get_wcs_json(wcs_site, path):
wcs_url = wcs_site.get('url')
if not wcs_url.endswith('/'):
wcs_url += '/'
url = wcs_url + path
response_json = cache.get(url)
if response_json is None:
real_url = url
if wcs_site.get('orig') and wcs_site.get('secret'):
real_url += '?orig=%s' % wcs_site.get('orig')
real_url = sign_url(real_url, wcs_site.get('secret'))
response_json = requests.get(real_url,
headers={'accept': 'application/json'}).json()
cache.set(url, response_json)
return response_json
return requests.get(path, remote_service=wcs_site, user=None,
headers={'accept': 'application/json'}).json()
def get_wcs_options(url, include_category_slug=False):
references = []

View File

@ -19,9 +19,14 @@ import base64
import hmac
import hashlib
from HTMLParser import HTMLParser
import logging
import random
from StringIO import StringIO
import urlparse
from requests import Response, Session as RequestsSession
import requests
from django.core.cache import cache
from django.utils.html import strip_tags
from django.utils.http import urlencode, quote
@ -29,6 +34,72 @@ class NothingInCacheException(Exception):
pass
class Requests(RequestsSession):
AUTO_USER = object()
AUTO_USER_EMAIL = object()
AUTO_USER_NAMEID = object()
def request(self, method, url, **kwargs):
remote_service = kwargs.pop('remote_service', None)
cache_duration = kwargs.pop('cache_duration', 15)
user = kwargs.pop('user', self.AUTO_USER)
raise_if_not_cached = kwargs.pop('raise_if_not_cached', False)
current_request = kwargs.pop('request', None)
if remote_service:
query_params = {'orig': remote_service.get('orig')}
if user in (self.AUTO_USER, self.AUTO_USER_NAMEID, self.AUTO_USER_EMAIL):
if current_request.user and current_request.user.is_authenticated():
if current_request.session.get('mellon_session') and user is not self.AUTO_USER_EMAIL:
mellon = current_request.session['mellon_session']
query_params['NameID'] = mellon['name_id_content']
elif user is not self.AUTO_USER_NAMEID:
query_params['email'] = current_request.user.email
else:
query_params['NameID'] = ''
query_params['email'] = ''
elif user:
# user must then be a dictionary
query_params.update(user)
remote_service_base_url = remote_service.get('url')
scheme, netloc, old_path, params, old_query, fragment = urlparse.urlparse(
remote_service_base_url)
query = urlencode(query_params)
if '?' in url:
path, old_query = url.split('?')
query += '&' + old_query
else:
path = url
url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
if method == 'GET' and cache_duration:
# handle cache
cache_key = hashlib.md5(url).hexdigest()
cache_content = cache.get(cache_key)
if cache_content:
response = Response()
response.status_code = 200
response.raw = StringIO(cache_content)
return response
elif raise_if_not_cached:
raise NothingInCacheException()
if remote_service: # sign
url = sign_url(url, remote_service.get('secret'))
response = super(Requests, self).request(method, url, **kwargs)
if response.status_code != 200:
logging.error('failed to %s %s (%s)' % (method, url, response.status_code))
if method == 'GET' and cache_duration and response.status_code == 200:
cache.set(cache_key, response.content, cache_duration)
return response
requests = Requests()
# Simple signature scheme for query strings
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None):

View File

@ -10,6 +10,7 @@ import urlparse
import os
from django.conf import settings
from django.test.client import RequestFactory
from django.template import Context
from combo.data.models import Page
@ -190,6 +191,13 @@ def check_wcs_open(url):
assert resp.status_code == 200
@pytest.fixture
def context():
ctx = Context({'request': RequestFactory().get('/')})
ctx['request'].user = None
ctx['request'].session = {}
return ctx
@wcsctl_present
def test_form_cell_setup():
cell = WcsFormCell()
@ -261,27 +269,23 @@ def test_current_forms_cell_setup():
settings.KNOWN_SERVICES = temp_settings
@wcsctl_present
def test_current_forms_cell_render():
def test_current_forms_cell_render(context):
page = Page(title='xxx', slug='test_current_forms_cell_render', template_name='standard')
page.save()
cell = WcsCurrentFormsCell(page=page, placeholder='content', order=0)
cell.save()
# query should fail as nothing is cached
with pytest.raises(NothingInCacheException):
result = cell.render(Context())
context = Context()
context['synchronous'] = True # to get fresh content
result = cell.render(context)
assert not 'http://127.0.0.1:8999/form-title/' in result # no form
class MockUser(object):
email = 'foo@example.net'
def is_authenticated(self):
return True
context['user'] = MockUser()
context['request'].user = MockUser()
# query should fail as nothing is cached
with pytest.raises(NothingInCacheException):
result = cell.render(context)
context['synchronous'] = True # to get fresh content
# default is to get current forms from all wcs sites
result = cell.render(context)
@ -311,14 +315,15 @@ def test_forms_of_category_cell_setup():
(u'other:test-9', u'test2 : Test 9')]
@wcsctl_present
def test_forms_of_category_cell_render():
def test_forms_of_category_cell_render(context):
page = Page(title='xxx', slug='test_forms_of_category_cell_render', template_name='standard')
page.save()
cell = WcsFormsOfCategoryCell(page=page, placeholder='content', order=0)
cell.category_reference = 'default:test-9'
cell.ordering = 'alpha'
cell.save()
result = cell.render(Context({'synchronous': True}))
context['synchronous'] = True # to get fresh content
result = cell.render(context)
assert 'form title' in result and 'a second form title' in result
assert result.index('form title') > result.index('a second form title')
assert 'http://127.0.0.1:8999/form-title/tryauth' in result
@ -326,13 +331,13 @@ def test_forms_of_category_cell_render():
cell.ordering = 'popularity'
cell.save()
result = cell.render(Context({'synchronous': True}))
result = cell.render(context)
assert 'form title' in result and 'a second form title' in result
assert result.index('form title') < result.index('a second form title')
cell.ordering = 'manual'
cell.save()
result = cell.render(Context({'synchronous': True}))
result = cell.render(context)
# by default all forms should be present, in alphabetical order
assert result.index('form title') > result.index('a second form title')
@ -340,32 +345,31 @@ def test_forms_of_category_cell_render():
cell.manual_order = {'data': ['default:test-9:a-second-form-title',
'default:test-9:form-title']}
cell.save()
result = cell.render(Context({'synchronous': True}))
result = cell.render(context)
assert result.index('form title') > result.index('a second form title')
# make sure all forms are displayed even if the manual order only specify
# some.
cell.manual_order = {'data': ['default:test-9:a-second-form-title']}
cell.save()
result = cell.render(Context({'synchronous': True}))
result = cell.render(context)
assert result.index('form title') > result.index('a second form title')
assert 'form title' in result and 'a second form title' in result
@wcsctl_present
def test_current_drafts_cell_render():
def test_current_drafts_cell_render(context):
page = Page(title='xxx', slug='test_current_drafts_cell_render', template_name='standard')
page.save()
cell = WcsCurrentDraftsCell(page=page, placeholder='content', order=0)
cell.save()
result = cell.render(Context({'synchronous': True}))
context['synchronous'] = True # to get fresh content
result = cell.render(context)
assert not 'http://127.0.0.1:8999/third-form-title' in result # no form
context = Context({'synchronous': True})
class MockUser(object):
email = 'foo@example.net'
def is_authenticated(self):
return True
context['user'] = MockUser()
context['synchronous'] = True # to force fresh content
context['request'].user = MockUser()
# default is to get current forms from all wcs sites
result = cell.render(context)