wcs: implement and use a generic way to do signed requests (#10492)
This commit is contained in:
parent
b663799b49
commit
47f9f7c8ca
|
@ -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'})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue