Add piston app.
This commit is contained in:
Родитель
998d8542ee
Коммит
08d501cd3a
|
@ -0,0 +1,30 @@
|
|||
import jingo
|
||||
from piston.handler import BaseHandler
|
||||
from piston.utils import rc, throttle
|
||||
from piston.authentication import OAuthAuthentication
|
||||
|
||||
|
||||
from users.models import UserProfile
|
||||
|
||||
|
||||
# Monkeypatch to render Piston's oauth page.
|
||||
def challenge(self, request):
|
||||
response = jingo.render(request, 'oauth/challenge.html', status=401)
|
||||
response['WWW-Authenticate'] = 'OAuth realm="API"'
|
||||
return response
|
||||
|
||||
|
||||
OAuthAuthentication.challenge = challenge
|
||||
|
||||
|
||||
class UserHandler(BaseHandler):
|
||||
allowed_methods = ('GET',)
|
||||
fields = ('email',)
|
||||
model = UserProfile
|
||||
|
||||
def read(self, request):
|
||||
try:
|
||||
user = UserProfile.objects.get(user=request.user)
|
||||
return user
|
||||
except UserProfile.DoesNotExist:
|
||||
return None
|
|
@ -0,0 +1,20 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ _('OAuth required') }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<h2>{{ _('OAuth required') }}</h2>
|
||||
|
||||
{# TODO(davedash): Get better copy, and link to the oauth consumer reg page.
|
||||
No need to trans until then. #}
|
||||
|
||||
<p>
|
||||
Dear courageous user.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You have stumbled upon our mighty API. It, however, requires that you use
|
||||
OAuth to authenticate yourself.
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,32 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ _('Authorize access to your data') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set consumer = token.consumer.name or _('Anonymous Party') %}
|
||||
<h2>
|
||||
{# L10n: consumer is the Oauth Consumer, e.g. FlightDeck #}
|
||||
{{ _('Authorize Token for {0}')|f(consumer) }}
|
||||
</h2>
|
||||
{% trans %}
|
||||
<p>
|
||||
{{ consumer }} wants to be authorized to act on your behalf. This may
|
||||
include:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Uploading, changing or removing add-ons on your behalf.</li>
|
||||
<li>Uploading, changing or removing versions of addons on your behalf.</li>
|
||||
<li>
|
||||
Learning personal information about you, such as your username and
|
||||
email address (but not your password).
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endtrans %}
|
||||
<form action="{{ url('oauth.authorize') }}" method="POST">
|
||||
{{ csrf() }}
|
||||
{# TODO(potch): A little help? #}
|
||||
{{ form.as_table()|safe }}
|
||||
<button type="submit">{{ _('Confirm') }}</button>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,40 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% set consumer = token.consumer.name or _('Anonymous Party') %}
|
||||
|
||||
{% block title %}
|
||||
|
||||
{% if token.is_approved %}
|
||||
{{ _('Access Granted') }}
|
||||
{% else %}
|
||||
{{ _('Access Not Granted') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if token.is_approved %}
|
||||
|
||||
<h2>{{ _('Access Granted') }}</h2>
|
||||
|
||||
<p>
|
||||
{% trans %}
|
||||
You have chosen to give access to {{ consumer }}. They will be
|
||||
able to act on your behalf or retrieve information about you.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
|
||||
{% else %}
|
||||
|
||||
<h2>{{ _('Access Not Granted') }}</h2>
|
||||
|
||||
<p>
|
||||
{% trans %}
|
||||
You have chosen to not give access to {{ consumer }}. They will not be
|
||||
able to act on your behalf or retrieve information about you.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
"""
|
||||
Verifies basic OAUTH functionality in AMO.
|
||||
|
||||
Sample request_token query:
|
||||
/en-US/firefox/oauth/request_token/?
|
||||
oauth_consumer_key=GYKEp7m5fJpj9j8Vjz&
|
||||
oauth_nonce=A7A79B47-B571-4D70-AA6C-592A0555E94B&
|
||||
oauth_signature_method=HMAC-SHA1&
|
||||
oauth_timestamp=1282950712&
|
||||
oauth_version=1.0
|
||||
|
||||
With headers:
|
||||
|
||||
Authorization: OAuth realm="",
|
||||
oauth_consumer_key="GYKEp7m5fJpj9j8Vjz",
|
||||
oauth_signature_method="HMAC-SHA1",
|
||||
oauth_signature="JBCA4ah%2FOQC0lLWV8aChGAC+15s%3D",
|
||||
oauth_timestamp="1282950995",
|
||||
oauth_nonce="1008F707-37E6-4ABF-8322-C6B658771D88",
|
||||
oauth_version="1.0"
|
||||
"""
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
import urlparse
|
||||
from hashlib import md5
|
||||
|
||||
from django.test.client import Client
|
||||
|
||||
import oauth2 as oauth
|
||||
from mock import Mock
|
||||
from nose.tools import eq_
|
||||
from test_utils import TestCase
|
||||
from piston.models import Consumer, Token
|
||||
|
||||
from amo.urlresolvers import reverse
|
||||
|
||||
|
||||
def _get_args(consumer, token=None, callback=False, verifier=None):
|
||||
d = dict(
|
||||
oauth_consumer_key=consumer.key,
|
||||
oauth_nonce=oauth.generate_nonce(),
|
||||
oauth_signature_method='HMAC-SHA1',
|
||||
oauth_timestamp=int(time.time()),
|
||||
oauth_version='1.0',
|
||||
)
|
||||
|
||||
if token:
|
||||
d['oauth_token'] = token
|
||||
|
||||
if callback:
|
||||
d['oauth_callback'] = 'http://testserver/foo'
|
||||
|
||||
if verifier:
|
||||
d['oauth_verifier'] = verifier
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def get_absolute_url(url):
|
||||
return 'http://%s%s' % ('api', reverse(url))
|
||||
|
||||
|
||||
class OAuthClient(Client):
|
||||
"""OauthClient can make magically signed requests."""
|
||||
def get(self, url, consumer=None, token=None, callback=False,
|
||||
verifier=None):
|
||||
url = get_absolute_url(url)
|
||||
req = oauth.Request(method="GET", url=url,
|
||||
parameters=_get_args(consumer, callback=callback,
|
||||
verifier=verifier))
|
||||
signature_method = oauth.SignatureMethod_HMAC_SHA1()
|
||||
req.sign_request(signature_method, consumer, token)
|
||||
return super(OAuthClient, self).get(req.to_url(), HTTP_HOST='api',
|
||||
**req)
|
||||
|
||||
|
||||
client = OAuthClient()
|
||||
token_keys = ('oauth_token_secret', 'oauth_token',)
|
||||
|
||||
|
||||
def get_token_from_response(response):
|
||||
data = urlparse.parse_qs(response.content)
|
||||
|
||||
for key in token_keys:
|
||||
assert key in data.keys(), '%s not in %s' % (key, data.keys())
|
||||
|
||||
return oauth.Token(key=data['oauth_token'][0],
|
||||
secret=data['oauth_token_secret'][0])
|
||||
|
||||
|
||||
def get_request_token(consumer, callback=False):
|
||||
r = client.get('oauth.request_token', consumer, callback=callback)
|
||||
return get_token_from_response(r)
|
||||
|
||||
|
||||
def get_access_token(consumer, token, authorize=True, verifier=None):
|
||||
r = client.get('oauth.access_token', consumer, token, verifier=verifier)
|
||||
|
||||
if authorize:
|
||||
return get_token_from_response(r)
|
||||
else:
|
||||
eq_(r.status_code, 401)
|
||||
|
||||
|
||||
class TestOauth(TestCase):
|
||||
fixtures = ('base/users',)
|
||||
|
||||
def setUp(self):
|
||||
consumers = []
|
||||
for status in ('accepted', 'pending', 'canceled', ):
|
||||
c = Consumer(name='a', status=status)
|
||||
c.generate_random_codes()
|
||||
c.save()
|
||||
consumers.append(c)
|
||||
self.accepted_consumer = consumers[0]
|
||||
self.pending_consumer = consumers[1]
|
||||
self.canceled_consumer = consumers[2]
|
||||
|
||||
def _login(self):
|
||||
self.client.login(username='admin@mozilla.com', password='password')
|
||||
|
||||
def _oauth_flow(self, consumer, authorize=True, callback=False):
|
||||
"""
|
||||
1. Get Request Token.
|
||||
2. Request Authorization.
|
||||
3. Get Access Token.
|
||||
4. Get to protected resource.
|
||||
"""
|
||||
token = get_request_token(consumer, callback)
|
||||
|
||||
self._login()
|
||||
url = (reverse('oauth.authorize') + '?oauth_token=' + token.key)
|
||||
r = self.client.get(url)
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
d = dict(authorize_access='on', oauth_token=token.key)
|
||||
|
||||
if callback:
|
||||
d['oauth_callback'] = 'http://testserver/foo'
|
||||
|
||||
verifier = None
|
||||
|
||||
if authorize:
|
||||
r = self.client.post(url, d)
|
||||
|
||||
if callback:
|
||||
redir = r.get('location', None)
|
||||
qs = urlparse.urlsplit(redir).query
|
||||
data = urlparse.parse_qs(qs)
|
||||
verifier = data['oauth_verifier'][0]
|
||||
else:
|
||||
eq_(r.status_code, 200)
|
||||
|
||||
piston_token = Token.objects.get()
|
||||
assert piston_token.is_approved, "Token not saved."
|
||||
else:
|
||||
del d['authorize_access']
|
||||
r = self.client.post(url, d)
|
||||
piston_token = Token.objects.get()
|
||||
assert not piston_token.is_approved, "Token saved."
|
||||
|
||||
token = get_access_token(consumer, token, authorize, verifier)
|
||||
|
||||
r = client.get('api.user', consumer, token)
|
||||
|
||||
if authorize:
|
||||
data = json.loads(r.content)
|
||||
eq_(data['email'], 'admin@mozilla.com')
|
||||
else:
|
||||
eq_(r.status_code, 401)
|
||||
|
||||
def test_accepted(self):
|
||||
self._oauth_flow(self.accepted_consumer)
|
||||
|
||||
def test_accepted_callback(self):
|
||||
"""Same as above, just uses a callback."""
|
||||
self._oauth_flow(self.accepted_consumer, callback=True)
|
||||
|
||||
def test_unauthorized(self):
|
||||
self._oauth_flow(self.accepted_consumer, authorize=False)
|
||||
|
||||
def test_request_token_pending(self):
|
||||
get_request_token(self.pending_consumer)
|
||||
|
||||
def test_request_token_cancelled(self):
|
||||
get_request_token(self.canceled_consumer)
|
||||
|
||||
def test_request_token_fake(self):
|
||||
"""Try with a phony consumer key"""
|
||||
c = Mock()
|
||||
c.key = 'yer'
|
||||
c.secret = 'mom'
|
||||
r = client.get('oauth.request_token', c)
|
||||
eq_(r.content, 'Invalid consumer.')
|
|
@ -1,8 +1,12 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls.defaults import patterns, url, include
|
||||
|
||||
from . import views
|
||||
from piston.resource import Resource
|
||||
from piston import authentication
|
||||
|
||||
from api import handlers
|
||||
from api import views
|
||||
from zadmin import jinja_for_django
|
||||
|
||||
API_CACHE_TIMEOUT = getattr(settings, 'API_CACHE_TIMEOUT', 500)
|
||||
|
||||
|
@ -60,10 +64,24 @@ for regexp in list_regexps:
|
|||
api_patterns += patterns('',
|
||||
url(regexp + '/?$', class_view(views.ListView), name='api.list'))
|
||||
|
||||
|
||||
ad = dict(authentication=authentication.OAuthAuthentication())
|
||||
user_resource = Resource(handler=handlers.UserHandler, **ad)
|
||||
|
||||
jfd = lambda a, b, c: jinja_for_django(a, b, context_instance=c)
|
||||
authentication.render_to_response = jfd
|
||||
|
||||
piston_patterns = patterns('',
|
||||
url(r'^user/$', user_resource, name='api.user'),
|
||||
)
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# Redirect api requests without versions
|
||||
url('^((?:addon|search|list)/.*)$', views.redirect_view),
|
||||
|
||||
# Piston
|
||||
url(r'^2/', include(piston_patterns)),
|
||||
|
||||
# Append api_version to the real api views
|
||||
url(r'^(?P<api_version>\d+|\d+.\d+)/', include(api_patterns)),
|
||||
|
||||
|
|
|
@ -294,3 +294,9 @@ def redirect_view(request, url):
|
|||
dest = get_url_prefix().fix(dest)
|
||||
|
||||
return HttpResponsePermanentRedirect(dest)
|
||||
|
||||
|
||||
def request_token_ready(request, token):
|
||||
error = request.GET.get('error', '')
|
||||
ctx = {'error': error, 'token': token}
|
||||
return jingo.render(request, 'piston/request_token_ready.html', ctx)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
CREATE TABLE `piston_nonce` (
|
||||
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||
`token_key` varchar(18) NOT NULL,
|
||||
`consumer_key` varchar(18) NOT NULL,
|
||||
`key` varchar(255) NOT NULL
|
||||
)
|
||||
;
|
||||
CREATE TABLE `piston_consumer` (
|
||||
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`description` longtext NOT NULL,
|
||||
`key` varchar(18) NOT NULL,
|
||||
`secret` varchar(32) NOT NULL,
|
||||
`status` varchar(16) NOT NULL,
|
||||
`user_id` integer
|
||||
)
|
||||
;
|
||||
ALTER TABLE `piston_consumer` ADD CONSTRAINT `user_id_refs_id_aad30107` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`);
|
||||
CREATE TABLE `piston_token` (
|
||||
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||
`key` varchar(18) NOT NULL,
|
||||
`secret` varchar(32) NOT NULL,
|
||||
`verifier` varchar(10) NOT NULL,
|
||||
`token_type` integer NOT NULL,
|
||||
`timestamp` integer NOT NULL,
|
||||
`is_approved` bool NOT NULL,
|
||||
`user_id` integer,
|
||||
`consumer_id` integer NOT NULL,
|
||||
`callback` varchar(255),
|
||||
`callback_confirmed` bool NOT NULL
|
||||
)
|
||||
;
|
||||
ALTER TABLE `piston_token` ADD CONSTRAINT `user_id_refs_id_efc02d17` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`);
|
||||
ALTER TABLE `piston_token` ADD CONSTRAINT `consumer_id_refs_id_85f42355` FOREIGN KEY (`consumer_id`) REFERENCES `piston_consumer` (`id`);
|
||||
CREATE INDEX `piston_consumer_fbfc09f1` ON `piston_consumer` (`user_id`);
|
||||
CREATE INDEX `piston_token_fbfc09f1` ON `piston_token` (`user_id`);
|
||||
CREATE INDEX `piston_token_6565fc20` ON `piston_token` (`consumer_id`);
|
|
@ -28,3 +28,6 @@ translate-toolkit==1.6.0
|
|||
|
||||
pylint
|
||||
-e git://github.com/davedash/django-fixture-magic.git#egg=django_fixture_magic
|
||||
|
||||
# Oauth client for tests
|
||||
-e git://github.com/simplegeo/python-oauth2.git#egg=python_oauth2
|
||||
|
|
|
@ -35,3 +35,5 @@ importlib==1.0.2
|
|||
# Recaptcha
|
||||
-e git://github.com/mozilla/django-recaptcha.git#egg=django-recaptcha
|
||||
|
||||
# Django Piston
|
||||
-e git://github.com/mozilla/django-piston.git#egg=django-piston
|
||||
|
|
|
@ -105,6 +105,8 @@ SITE_URL = 'http://%s' % DOMAIN
|
|||
# Example: https://services.addons.mozilla.org
|
||||
SERVICES_URL = 'http://services.%s' % DOMAIN
|
||||
|
||||
OAUTH_CALLBACK_VIEW = 'api.views.request_token_ready'
|
||||
|
||||
# Absolute path to the directory that holds media.
|
||||
# Example: "/home/media/media.lawrence.com/"
|
||||
MEDIA_ROOT = path('media')
|
||||
|
@ -256,9 +258,13 @@ INSTALLED_APPS = (
|
|||
ROOT_PACKAGE,
|
||||
|
||||
'cake',
|
||||
|
||||
# Third party apps
|
||||
'celery',
|
||||
'django_nose',
|
||||
'piston',
|
||||
|
||||
# Django contrib apps
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
|
|
8
urls.py
8
urls.py
|
@ -116,6 +116,14 @@ urlpatterns = patterns('',
|
|||
|
||||
)
|
||||
|
||||
urlpatterns += patterns('piston.authentication',
|
||||
url(r'^oauth/request_token/$', 'oauth_request_token',
|
||||
name='oauth.request_token'),
|
||||
url(r'^oauth/authorize/$', 'oauth_user_auth', name='oauth.authorize'),
|
||||
url(r'^oauth/access_token/$', 'oauth_access_token',
|
||||
name='oauth.access_token'),
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
# Remove leading and trailing slashes so the regex matches.
|
||||
media_url = settings.MEDIA_URL.lstrip('/').rstrip('/')
|
||||
|
|
Загрузка…
Ссылка в новой задаче