Commit a99f3752 by Peter Fogg

Merge pull request #4 from edx/peter-fogg/worker-auth

Enable authentication via JWT for ecommerce workers.
parents 43acfc87 e1e0e593
import os
def get_overrides_filename(variable):
"""
Get the name of the file containing configuration overrides
from the provided environment variable.
"""
filename = os.environ.get(variable)
if filename is None:
msg = 'Please set the {} environment variable.'.format(variable)
raise EnvironmentError(msg)
return filename
...@@ -22,12 +22,16 @@ CELERYD_HIJACK_ROOT_LOGGER = False ...@@ -22,12 +22,16 @@ CELERYD_HIJACK_ROOT_LOGGER = False
# Absolute URL used to construct API calls against the ecommerce service. # Absolute URL used to construct API calls against the ecommerce service.
ECOMMERCE_API_ROOT = None ECOMMERCE_API_ROOT = None
# Long-lived access token used by Celery workers to authenticate against the ecommerce service.
WORKER_ACCESS_TOKEN = None
# Maximum number of retries before giving up on the fulfillment of an order. # Maximum number of retries before giving up on the fulfillment of an order.
# For reference, 11 retries with exponential backoff yields a maximum waiting # For reference, 11 retries with exponential backoff yields a maximum waiting
# time of 2047 seconds (about 30 minutes). Defaulting this to None could yield # time of 2047 seconds (about 30 minutes). Defaulting this to None could yield
# unwanted behavior: infinite retries. # unwanted behavior: infinite retries.
MAX_FULFILLMENT_RETRIES = 11 MAX_FULFILLMENT_RETRIES = 11
# END ORDER FULFILLMENT # END ORDER FULFILLMENT
# AUTHENTICATION
JWT_SECRET_KEY = None
JWT_ISSUER = None
ECOMMERCE_SERVICE_USERNAME = 'ecommerce_worker'
# END AUTHENTICATION
import logging
from logging.config import dictConfig
import yaml
from . import get_overrides_filename
from ecommerce_worker.configuration.base import *
from ecommerce_worker.configuration.logger import get_logger_config
logger = logging.getLogger(__name__)
# LOGGING
logger_config = get_logger_config(debug=True, dev_env=True, local_loglevel='DEBUG')
dictConfig(logger_config)
# END LOGGING
filename = get_overrides_filename('ECOMMERCE_WORKER_CFG')
with open(filename) as f:
config_from_yaml = yaml.load(f)
# Override base configuration with values from disk.
vars().update(config_from_yaml)
# Apply any developer-defined overrides.
try:
from ecommerce_worker.configuration.private import * # pylint: disable=import-error
except ImportError:
logger.warning('No developer-defined configuration overrides have been applied.')
pass
...@@ -16,16 +16,19 @@ BROKER_URL = 'amqp://' ...@@ -16,16 +16,19 @@ BROKER_URL = 'amqp://'
ECOMMERCE_API_ROOT = 'http://localhost:8002/api/v2/' ECOMMERCE_API_ROOT = 'http://localhost:8002/api/v2/'
# END ORDER FULFILLMENT # END ORDER FULFILLMENT
# AUTHENTICATION
JWT_SECRET_KEY = 'insecure-secret-key'
JWT_ISSUER = 'ecommerce_worker'
# END AUTHENTICATION
# LOGGING # LOGGING
logger_config = get_logger_config(debug=True, dev_env=True, local_loglevel='DEBUG') logger_config = get_logger_config(debug=True, dev_env=True, local_loglevel='DEBUG')
dictConfig(logger_config) dictConfig(logger_config)
# END LOGGING # END LOGGING
# Apply any developer-defined overrides. # Apply any developer-defined overrides.
try: try:
from .private import * # pylint: disable=import-error from ecommerce_worker.configuration.private import * # pylint: disable=import-error
except ImportError: except ImportError:
logger.warning('No developer-defined configuration overrides have been applied.') logger.warning('No developer-defined configuration overrides have been applied.')
pass pass
from logging.config import dictConfig from logging.config import dictConfig
import os
import yaml import yaml
from ecommerce_worker.configuration import get_overrides_filename
from ecommerce_worker.configuration.base import * from ecommerce_worker.configuration.base import *
from ecommerce_worker.configuration.logger import get_logger_config from ecommerce_worker.configuration.logger import get_logger_config
def get_overrides_filename(variable):
"""
Get the name of the file containing configuration overrides
from the provided environment variable.
"""
filename = os.environ.get(variable)
if filename is None:
msg = 'Please set the {} environment variable.'.format(variable)
raise EnvironmentError(msg)
return filename
# LOGGING # LOGGING
logger_config = get_logger_config() logger_config = get_logger_config()
dictConfig(logger_config) dictConfig(logger_config)
......
...@@ -19,3 +19,10 @@ WORKER_ACCESS_TOKEN = 'fake-access-token' ...@@ -19,3 +19,10 @@ WORKER_ACCESS_TOKEN = 'fake-access-token'
logger_config = get_logger_config(debug=True, dev_env=True, local_loglevel='DEBUG') logger_config = get_logger_config(debug=True, dev_env=True, local_loglevel='DEBUG')
dictConfig(logger_config) dictConfig(logger_config)
# END LOGGING # END LOGGING
# AUTHENTICATION
JWT_SECRET_KEY = 'test-key'
JWT_ISSUER = 'ecommerce_worker'
ECOMMERCE_SERVICE_USERNAME = 'service'
# END AUTHENTICATION
...@@ -21,14 +21,12 @@ def fulfill_order(self, order_number): ...@@ -21,14 +21,12 @@ def fulfill_order(self, order_number):
None None
""" """
ecommerce_api_root = get_configuration('ECOMMERCE_API_ROOT') ecommerce_api_root = get_configuration('ECOMMERCE_API_ROOT')
worker_access_token = get_configuration('WORKER_ACCESS_TOKEN')
max_fulfillment_retries = get_configuration('MAX_FULFILLMENT_RETRIES') max_fulfillment_retries = get_configuration('MAX_FULFILLMENT_RETRIES')
signing_key = get_configuration('JWT_SECRET_KEY')
issuer = get_configuration('JWT_ISSUER')
service_username = get_configuration('ECOMMERCE_SERVICE_USERNAME')
if not (ecommerce_api_root and worker_access_token and max_fulfillment_retries): api = EcommerceApiClient(ecommerce_api_root, signing_key=signing_key, issuer=issuer, username=service_username)
raise RuntimeError('Worker is improperly configured for order fulfillment.')
api = EcommerceApiClient(ecommerce_api_root, oauth_access_token=worker_access_token)
try: try:
logger.info('Requesting fulfillment of order [%s].', order_number) logger.info('Requesting fulfillment of order [%s].', order_number)
api.orders(order_number).fulfill.put() api.orders(order_number).fulfill.put()
......
...@@ -6,6 +6,7 @@ from celery.exceptions import Ignore ...@@ -6,6 +6,7 @@ from celery.exceptions import Ignore
import ddt import ddt
from ecommerce_api_client import exceptions from ecommerce_api_client import exceptions
import httpretty import httpretty
import jwt
import mock import mock
from ecommerce_worker.fulfillment.v1.tasks import fulfill_order from ecommerce_worker.fulfillment.v1.tasks import fulfill_order
...@@ -23,8 +24,10 @@ class OrderFulfillmentTaskTests(TestCase): ...@@ -23,8 +24,10 @@ class OrderFulfillmentTaskTests(TestCase):
@ddt.data( @ddt.data(
'ECOMMERCE_API_ROOT', 'ECOMMERCE_API_ROOT',
'WORKER_ACCESS_TOKEN',
'MAX_FULFILLMENT_RETRIES', 'MAX_FULFILLMENT_RETRIES',
'JWT_SECRET_KEY',
'JWT_ISSUER',
'ECOMMERCE_SERVICE_USERNAME'
) )
def test_requires_configuration(self, setting): def test_requires_configuration(self, setting):
"""Verify that the task refuses to run without the configuration it requires.""" """Verify that the task refuses to run without the configuration it requires."""
...@@ -42,8 +45,9 @@ class OrderFulfillmentTaskTests(TestCase): ...@@ -42,8 +45,9 @@ class OrderFulfillmentTaskTests(TestCase):
# Validate the value of the HTTP Authorization header. # Validate the value of the HTTP Authorization header.
last_request = httpretty.last_request() last_request = httpretty.last_request()
authorization = last_request.headers.get('authorization') token = last_request.headers.get('authorization').split()[1]
self.assertEqual(authorization, 'Bearer ' + get_configuration('WORKER_ACCESS_TOKEN')) payload = jwt.decode(token, get_configuration('JWT_SECRET_KEY'))
self.assertEqual(payload['username'], get_configuration('ECOMMERCE_SERVICE_USERNAME'))
@httpretty.activate @httpretty.activate
def test_fulfillment_not_possible(self): def test_fulfillment_not_possible(self):
......
...@@ -25,4 +25,7 @@ def get_configuration(variable): ...@@ -25,4 +25,7 @@ def get_configuration(variable):
__import__(name) __import__(name)
module = sys.modules[name] module = sys.modules[name]
return getattr(module, variable, None) value = getattr(module, variable, None)
if value is None:
raise RuntimeError('Worker is improperly configured: {} is unset in {}.'.format(variable, module))
return value
...@@ -6,7 +6,7 @@ with open('README.rst') as readme: ...@@ -6,7 +6,7 @@ with open('README.rst') as readme:
setup( setup(
name='edx-ecommerce-worker', name='edx-ecommerce-worker',
version='0.1.0', version='0.2.0',
description='Celery tasks supporting the operations of edX\'s ecommerce service', description='Celery tasks supporting the operations of edX\'s ecommerce service',
long_description=long_description, long_description=long_description,
classifiers=[ classifiers=[
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment