Skip to content

Commit ca172b4

Browse files
remeikaOmer Katz
authored andcommitted
result_backend setting supports rediss:// protocol URLs (celery#4696)
* Implements support for rediss protocol URLs in setting result_backend * Adds documentation for rediss:// urls in result_broker setting * Adds tests for rediss:// urls in result_backend * Urllib import compatible with Python 3 * Adds moar tests * Happify lint. * Fixes test input for rediss URLs
1 parent 696646d commit ca172b4

File tree

4 files changed

+132
-1
lines changed

4 files changed

+132
-1
lines changed

celery/app/backends.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
'rpc': 'celery.backends.rpc.RPCBackend',
2222
'cache': 'celery.backends.cache:CacheBackend',
2323
'redis': 'celery.backends.redis:RedisBackend',
24+
'rediss': 'celery.backends.redis:RedisBackend',
2425
'sentinel': 'celery.backends.redis:SentinelBackend',
2526
'mongodb': 'celery.backends.mongodb:MongoBackend',
2627
'db': 'celery.backends.database:DatabaseBackend',

celery/backends/redis.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import absolute_import, unicode_literals
44

55
from functools import partial
6+
from ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
67

78
from kombu.utils.functional import retry_over_time
89
from kombu.utils.objects import cached_property
@@ -20,6 +21,12 @@
2021

2122
from . import async, base
2223

24+
try:
25+
from urllib.parse import unquote
26+
except ImportError:
27+
# Python 2
28+
from urlparse import unquote
29+
2330
try:
2431
import redis
2532
from kombu.transport.redis import get_redis_error_classes
@@ -44,6 +51,23 @@
4451
sentinel in order to use the Redis result store backend.
4552
"""
4653

54+
W_REDIS_SSL_CERT_OPTIONAL = """
55+
Setting ssl_cert_reqs=CERT_OPTIONAL when connecting to redis means that \
56+
celery might not valdate the identity of the redis broker when connecting. \
57+
This leaves you vulnerable to man in the middle attacks.
58+
"""
59+
60+
W_REDIS_SSL_CERT_NONE = """
61+
Setting ssl_cert_reqs=CERT_NONE when connecting to redis means that celery \
62+
will not valdate the identity of the redis broker when connecting. This \
63+
leaves you vulnerable to man in the middle attacks.
64+
"""
65+
66+
E_REDIS_SSL_CERT_REQS_MISSING = """
67+
A rediss:// URL must have parameter ssl_cert_reqs be CERT_REQUIRED, \
68+
CERT_OPTIONAL, or CERT_NONE
69+
"""
70+
4771
E_LOST = 'Connection to Redis lost: Retry (%s/%s) %s.'
4872

4973
logger = get_logger(__name__)
@@ -197,6 +221,26 @@ def _params_from_url(self, url, defaults):
197221
else:
198222
connparams['db'] = path
199223

224+
if scheme == 'rediss':
225+
connparams['connection_class'] = redis.SSLConnection
226+
# The following parameters, if present in the URL, are encoded. We
227+
# must add the decoded values to connparams.
228+
for ssl_setting in ['ssl_ca_certs', 'ssl_certfile', 'ssl_keyfile']:
229+
ssl_val = query.pop(ssl_setting, None)
230+
if ssl_val:
231+
connparams[ssl_setting] = unquote(ssl_val)
232+
ssl_cert_reqs = query.pop('ssl_cert_reqs', 'MISSING')
233+
if ssl_cert_reqs == 'CERT_REQUIRED':
234+
connparams['ssl_cert_reqs'] = CERT_REQUIRED
235+
elif ssl_cert_reqs == 'CERT_OPTIONAL':
236+
logger.warn(W_REDIS_SSL_CERT_OPTIONAL)
237+
connparams['ssl_cert_reqs'] = CERT_OPTIONAL
238+
elif ssl_cert_reqs == 'CERT_NONE':
239+
logger.warn(W_REDIS_SSL_CERT_NONE)
240+
connparams['ssl_cert_reqs'] = CERT_NONE
241+
else:
242+
raise ValueError(E_REDIS_SSL_CERT_REQS_MISSING)
243+
200244
# db may be string and start with / like in kombu.
201245
db = connparams.get('db') or 0
202246
db = db.strip('/') if isinstance(db, string_t) else db

docs/userguide/configuration.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,10 +875,13 @@ Configuring the backend URL
875875
requirements.
876876

877877
This backend requires the :setting:`result_backend`
878-
setting to be set to a Redis URL::
878+
setting to be set to a Redis or `Redis over TLS`_ URL::
879879

880880
result_backend = 'redis://:password@host:port/db'
881881

882+
.. _`Redis over TLS`:
883+
https://www.iana.org/assignments/uri-schemes/prov/rediss
884+
882885
For example::
883886

884887
result_backend = 'redis://localhost/0'
@@ -887,6 +890,10 @@ is the same as::
887890

888891
result_backend = 'redis://'
889892

893+
Use the ``rediss://`` protocol to connect to redis over TLS::
894+
895+
result_backend = 'rediss://:password@host:port/db?ssl_cert_reqs=CERT_REQUIRED'
896+
890897
The fields of the URL are defined as follows:
891898

892899
#. ``password``
@@ -906,6 +913,17 @@ The fields of the URL are defined as follows:
906913
Database number to use. Default is 0.
907914
The db can include an optional leading slash.
908915

916+
When using a TLS connection (protocol is ``rediss://``), you may pass in all values in :setting:`broker_use_ssl` as query parameters. Paths to certificates must be URL encoded, and ``ssl_cert_reqs`` is required. Example:
917+
918+
.. code-block:: python
919+
920+
result_backend = 'rediss://:password@host:port/db?\
921+
ssl_cert_reqs=CERT_REQUIRED\
922+
&ssl_ca_certs=%2Fvar%2Fssl%2Fmyca.pem\ # /var/ssl/myca.pem
923+
&ssl_certfile=%2Fvar%2Fssl%2Fredis-server-cert.pem\ # /var/ssl/redis-server-cert.pem
924+
&ssl_keyfile=%2Fvar%2Fssl%2Fprivate%2Fworker-key.pem' # /var/ssl/private/worker-key.pem
925+
926+
909927
.. setting:: redis_backend_use_ssl
910928

911929
``redis_backend_use_ssl``

t/unit/backends/test_redis.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,74 @@ def test_backend_ssl(self):
270270
from redis.connection import SSLConnection
271271
assert x.connparams['connection_class'] is SSLConnection
272272

273+
@skip.unless_module('redis')
274+
def test_backend_ssl_url(self):
275+
self.app.conf.redis_socket_timeout = 30.0
276+
self.app.conf.redis_socket_connect_timeout = 100.0
277+
x = self.Backend(
278+
'rediss://:[email protected]:123//1?ssl_cert_reqs=CERT_REQUIRED',
279+
app=self.app,
280+
)
281+
assert x.connparams
282+
assert x.connparams['host'] == 'vandelay.com'
283+
assert x.connparams['db'] == 1
284+
assert x.connparams['port'] == 123
285+
assert x.connparams['password'] == 'bosco'
286+
assert x.connparams['socket_timeout'] == 30.0
287+
assert x.connparams['socket_connect_timeout'] == 100.0
288+
assert x.connparams['ssl_cert_reqs'] == ssl.CERT_REQUIRED
289+
290+
from redis.connection import SSLConnection
291+
assert x.connparams['connection_class'] is SSLConnection
292+
293+
@skip.unless_module('redis')
294+
def test_backend_ssl_url_options(self):
295+
x = self.Backend(
296+
(
297+
'rediss://:[email protected]:123//1?ssl_cert_reqs=CERT_NONE'
298+
'&ssl_ca_certs=%2Fvar%2Fssl%2Fmyca.pem'
299+
'&ssl_certfile=%2Fvar%2Fssl%2Fredis-server-cert.pem'
300+
'&ssl_keyfile=%2Fvar%2Fssl%2Fprivate%2Fworker-key.pem'
301+
),
302+
app=self.app,
303+
)
304+
assert x.connparams
305+
assert x.connparams['host'] == 'vandelay.com'
306+
assert x.connparams['db'] == 1
307+
assert x.connparams['port'] == 123
308+
assert x.connparams['password'] == 'bosco'
309+
assert x.connparams['ssl_cert_reqs'] == ssl.CERT_NONE
310+
assert x.connparams['ssl_ca_certs'] == '/var/ssl/myca.pem'
311+
assert x.connparams['ssl_certfile'] == '/var/ssl/redis-server-cert.pem'
312+
assert x.connparams['ssl_keyfile'] == '/var/ssl/private/worker-key.pem'
313+
314+
@skip.unless_module('redis')
315+
def test_backend_ssl_url_cert_none(self):
316+
x = self.Backend(
317+
'rediss://:[email protected]:123//1?ssl_cert_reqs=CERT_OPTIONAL',
318+
app=self.app,
319+
)
320+
assert x.connparams
321+
assert x.connparams['host'] == 'vandelay.com'
322+
assert x.connparams['db'] == 1
323+
assert x.connparams['port'] == 123
324+
assert x.connparams['ssl_cert_reqs'] == ssl.CERT_OPTIONAL
325+
326+
from redis.connection import SSLConnection
327+
assert x.connparams['connection_class'] is SSLConnection
328+
329+
@skip.unless_module('redis')
330+
@pytest.mark.parametrize("uri", [
331+
'rediss://:[email protected]:123//1?ssl_cert_reqs=CERT_KITTY_CATS',
332+
'rediss://:[email protected]:123//1'
333+
])
334+
def test_backend_ssl_url_invalid(self, uri):
335+
with pytest.raises(ValueError):
336+
self.Backend(
337+
uri,
338+
app=self.app,
339+
)
340+
273341
def test_compat_propertie(self):
274342
x = self.Backend(
275343
'redis://:[email protected]:123//1', app=self.app,

0 commit comments

Comments
 (0)