Limit CORS on internal API and allow cookies (fixes #2519) (#2527)

This commit is contained in:
Mark Striemer 2016-05-06 16:13:12 -05:00
Родитель f23466819a
Коммит 2daad31407
7 изменённых файлов: 84 добавлений и 4 удалений

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

@ -38,7 +38,7 @@ django-aesfield==0.1.2 --hash=sha256:b77932c67dd42f01a119133de1fa88effb2c4d97ab9
--hash=sha256:ce07311572aa05f3eaabe1207fb684c2e01b1b4c792cacae1a804c102e8945ae
django-cache-nuggets==0.1.1 --hash=sha256:5215ab7ae1d7446a649b27384317afe23e77bf459893d99096ae662538bb547d \
--hash=sha256:5376279b9c46288e1c4bae197e84a16f9c35ef21ce93e3ceb74ae9b51acbbb25
django-cors-headers==1.1.0 --hash=sha256:fcd96e2be47c8eef34c650e007a6d546e19e7ee61041b89edbbbbe7619aa3987
django-cors-headers-multi==1.2.0 --hash=sha256:c40f17823aa59df3c064234cdc890c56667b5db1ea6aac0172c949dc5c42ed53
django-cronjobs==0.2.3 --hash=sha256:177295b1442400c92cdb67e8e18f9ff5946fb442f85813b9d0837823722ea08d \
--hash=sha256:41fedd899af96b7057bafcad68ff42aefbb4dfe2b0e8a9de34cb32ac8892aff7
django_csp==2.0.3 --hash=sha256:149b219d801314cac1dbce9259f485636eeafee48f7d0ca9def4b12986428347 \

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

@ -256,6 +256,13 @@ FXA_CONFIG = {
},
}
INTERNAL_DOMAINS = [
'addons-admin.dev.mozaws.net',
'localhost:3000',
]
for regex, overrides in CORS_ENDPOINTS:
overrides['CORS_ORIGIN_WHITELIST'] = INTERNAL_DOMAINS
READ_ONLY = env.bool('READ_ONLY', default=False)
RAVEN_DSN = (

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

@ -221,6 +221,10 @@ FXA_CONFIG = {
},
}
INTERNAL_DOMAINS = ['addons-admin.prod.mozaws.net']
for regex, overrides in CORS_ENDPOINTS:
overrides['CORS_ORIGIN_WHITELIST'] = INTERNAL_DOMAINS
VALIDATOR_TIMEOUT = 360
ES_DEFAULT_NUM_SHARDS = 10

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

@ -250,6 +250,10 @@ FXA_CONFIG = {
},
}
INTERNAL_DOMAINS = ['addons-admin.stage.mozaws.net']
for regex, overrides in CORS_ENDPOINTS:
overrides['CORS_ORIGIN_WHITELIST'] = INTERNAL_DOMAINS
READ_ONLY = env.bool('READ_ONLY', default=False)
RAVEN_DSN = (

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

@ -3,10 +3,12 @@ import base64
import json
import urlparse
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from django.test import override_settings
import mock
from rest_framework.test import APIClient
from rest_framework_jwt.serializers import VerifyJSONWebTokenSerializer
from olympia.accounts import verify, views
@ -196,6 +198,7 @@ class TestLoginStartView(TestCase):
scheme=url.scheme, netloc=url.netloc, path=url.path)
assert redirect == 'https://accounts.firefox.com/v1/authorization'
assert urlparse.parse_qs(url.query) == {
'action': ['signin'],
'client_id': ['999abc111'],
'redirect_url': ['https://addons-frontend/fxa-authenticate'],
'scope': ['profile'],
@ -236,12 +239,31 @@ class TestLoginStartView(TestCase):
assert ':' not in query['state'][0]
@override_settings(FXA_CONFIG={'internal': FXA_CONFIG})
def has_cors_headers(response, origin='https://addons-frontend'):
return (
response['Access-Control-Allow-Origin'] == origin and
response['Access-Control-Allow-Credentials'] == 'true')
def update_domains(overrides):
overrides = overrides.copy()
overrides['CORS_ORIGIN_WHITELIST'] = ['addons-frontend', 'localhost:3000']
return overrides
endpoint_overrides = [
(regex, update_domains(overrides))
for regex, overrides in settings.CORS_ENDPOINT_OVERRIDES]
@override_settings(
FXA_CONFIG={'internal': FXA_CONFIG},
CORS_ENDPOINT_OVERRIDES=endpoint_overrides)
class TestLoginView(BaseAuthenticationView):
view_name = 'internal-login'
def setUp(self):
super(TestLoginView, self).setUp()
self.client.defaults['HTTP_ORIGIN'] = 'https://addons-frontend'
self.state = 'stateaosidoiajsdaagdsasi'
self.initialize_session({'fxa_state': self.state})
self.code = 'codeaosidjoiajsdioasjdoa'
@ -253,17 +275,22 @@ class TestLoginView(BaseAuthenticationView):
kwargs.setdefault('code', self.code)
return self.client.post(self.url, kwargs)
def options(self, url, origin):
return APIClient(HTTP_ORIGIN=origin).options(url)
def test_no_code_provided(self):
response = self.post(code='')
assert response.status_code == 422
assert response.data['error'] == views.ERROR_NO_CODE
assert not self.update_user.called
assert has_cors_headers(response)
def test_wrong_state(self):
response = self.post(state='a-different-state')
assert response.status_code == 400
assert response.data['error'] == views.ERROR_STATE_MISMATCH
assert not self.update_user.called
assert has_cors_headers(response)
def test_no_fxa_profile(self):
self.fxa_identify.side_effect = verify.IdentificationError
@ -272,6 +299,7 @@ class TestLoginView(BaseAuthenticationView):
assert response.data['error'] == views.ERROR_NO_PROFILE
self.fxa_identify.assert_called_with(self.code, config=FXA_CONFIG)
assert not self.update_user.called
assert has_cors_headers(response)
def test_no_amo_account_cant_login(self):
self.fxa_identify.return_value = {'email': 'me@yeahoo.com', 'uid': '5'}
@ -280,6 +308,7 @@ class TestLoginView(BaseAuthenticationView):
assert response.data['error'] == views.ERROR_NO_USER
self.fxa_identify.assert_called_with(self.code, config=FXA_CONFIG)
assert not self.update_user.called
assert has_cors_headers(response)
def test_login_success(self):
user = UserProfile.objects.create(
@ -293,6 +322,7 @@ class TestLoginView(BaseAuthenticationView):
verify = VerifyJSONWebTokenSerializer().validate(response.data)
assert verify['user'] == user
self.update_user.assert_called_with(user, identity)
assert has_cors_headers(response)
def test_account_exists_migrated_multiple(self):
"""Test that login fails if the user is logged in but the fxa_id is
@ -303,5 +333,24 @@ class TestLoginView(BaseAuthenticationView):
self.fxa_identify.return_value = {'email': 'real@yeahoo.com',
'uid': '9005'}
with self.assertRaises(UserProfile.MultipleObjectsReturned):
self.post()
response = self.post()
assert has_cors_headers(response)
assert not self.update_user.called
def test_cors_addons_frontend(self):
response = self.options(self.url, origin='https://addons-frontend')
assert has_cors_headers(response, origin='https://addons-frontend')
assert response.status_code == 200
def test_cors_localhost(self):
response = self.options(self.url, origin='http://localhost:3000')
assert has_cors_headers(response, origin='http://localhost:3000')
assert response.status_code == 200
def test_cors_other(self):
response = self.options(self.url, origin='https://attacker.com')
assert 'Access-Control-Allow-Origin' not in response
assert 'Access-Control-Allow-Methods' not in response
assert 'Access-Control-Allow-Headers' not in response
assert 'Access-Control-Allow-Credentials' not in response
assert response.status_code == 200

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

@ -47,6 +47,7 @@ class LoginStart(APIView):
if next_path and is_safe_url(next_path):
state += ':' + urlsafe_b64encode(next_path).rstrip('=')
query = {
'action': 'signin',
'client_id': config['client_id'],
'redirect_url': config['redirect_url'],
'scope': config['scope'],
@ -69,3 +70,6 @@ class LoginView(APIView):
add_api_token_to_response(response, user, set_cookie=False)
log.info('Logging in user {} from FxA'.format(user))
return response
def options(self, request):
return Response()

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

@ -74,6 +74,18 @@ NOBODY_EMAIL = 'nobody@mozilla.org'
# django-cors-headers.
CORS_ORIGIN_ALLOW_ALL = True
CORS_URLS_REGEX = r'^/api/v3/.*$'
INTERNAL_DOMAINS = ['localhost:3000']
CORS_ENDPOINT_OVERRIDES = [
(r'^/api/v3/internal/accounts/login/?$', {
'CORS_ORIGIN_ALLOW_ALL': False,
'CORS_ORIGIN_WHITELIST': INTERNAL_DOMAINS,
'CORS_ALLOW_CREDENTIALS': True,
}),
(r'^/api/v3/internal/.*$', {
'CORS_ORIGIN_ALLOW_ALL': False,
'CORS_ORIGIN_WHITELIST': INTERNAL_DOMAINS,
}),
]
DATABASE_URL = os.environ.get('DATABASE_URL',
'mysql://root:@localhost/olympia')