This commit is contained in:
Dave Dash 2010-08-27 10:07:16 -07:00
Родитель 998d8542ee
Коммит 08d501cd3a
12 изменённых файлов: 398 добавлений и 1 удалений

30
apps/api/handlers.py Normal file
Просмотреть файл

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

37
migrations/75-piston.sql Normal file
Просмотреть файл

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

Просмотреть файл

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