Repository: celery/celery

Issue:
Celery cannot connect to redis >6.0 using ACL user:password broker url
<!--
Please fill this template entirely and do not erase parts of it.
We reserve the right to close without a response
bug reports which are incomplete.
-->
# Checklist
<!--
To check an item on the list replace [ ] with [x].
-->
- [x] I have verified that the issue exists against the `master` branch of Celery.
- [ ] This has already been asked to the [discussion group](https://groups.google.com/forum/#!forum/celery-users) first.
- [x] I have read the relevant section in the
  [contribution guide](http://docs.celeryproject.org/en/latest/contributing.html#other-bugs)
  on reporting bugs.
- [x] I have checked the [issues list](https://github.com/celery/celery/issues?q=is%3Aissue+label%3A%22Issue+Type%3A+Bug+Report%22+-label%3A%22Category%3A+Documentation%22)
  for similar or identical bug reports.
- [x] I have checked the [pull requests list](https://github.com/celery/celery/pulls?q=is%3Apr+label%3A%22PR+Type%3A+Bugfix%22+-label%3A%22Category%3A+Documentation%22)
  for existing proposed fixes.
- [x] I have checked the [commit log](https://github.com/celery/celery/commits/master)
  to find out if the bug was already fixed in the master branch.
- [x] I have included all related issues and possible duplicate issues
  in this issue (If there are none, check this box anyway).

## Mandatory Debugging Information

- [ ] I have included the output of ``celery -A proj report`` in the issue.
    (if you are not able to do this, then at least specify the Celery
     version affected).
- [x] I have verified that the issue exists against the `master` branch of Celery.
- [ ] I have included the contents of ``pip freeze`` in the issue.
- [ ] I have included all the versions of all the external dependencies required
  to reproduce this bug.

## Related Issues and Possible Duplicates
<!--
Please make sure to search and mention any related issues
or possible duplicates to this issue as requested by the checklist above.

This may or may not include issues in other repositories that the Celery project
maintains or other repositories that are dependencies of Celery.

If you don't know how to mention issues, please refer to Github's documentation
on the subject: https://help.github.com/en/articles/autolinked-references-and-urls#issues-and-pull-requests
-->

#### Related Issues

- None

#### Possible Duplicates

- None

## Environment & Settings
<!-- Include the contents of celery --version below -->
**5.0.0**:
<!-- Include the output of celery -A proj report below -->

# Expected Behavior
<!-- Describe in detail what you expect to happen -->
I'm a new celery user, not quite sure if I should report this as a bug or feature request and I'm not quite sure how to best fix this myself, so I hope you'll bear with me :-)

Redis 6.0 or greater implements users in the form of ACL (see https://redis.io/topics/acl), however, celery throws an WRONGPASS error when using the following broker URL:

```redis://user:password@localhost:6379/0```

We would expect celery to connect to redis using the correct user:password combination and not throw a password error. Older redis versions did not implement ACL; looking at _params_from_url function on line 277 in celery/backend/redis.py, it seems like celery currently only implements a broker URL containing a password and no username, as indicated by line 278:

```scheme, host, port, _, password, path, query = _parse_url(url)```

I would expect celery to implement the user:password@host scheme, as well as the current password@host scheme in order to correctly handle the ACL introduced in redis >6.0

# Actual Behavior
<!--
Describe in detail what actually happened.
Please include a backtrace and surround it with triple backticks (```).
In addition, include the Celery daemon logs, the broker logs,
the result backend logs and system logs below if they will help us debug
the issue.
-->

Celery throws this error:
``` 
[2020-10-18 02:35:54,748: ERROR/MainProcess] consumer: Cannot connect to redis://user:**@localhost:6379/0: WRONGPASS invalid username-password pair.

Trying again in 18.00 seconds... (9/100)```


Discussion:
Hi @Insension. I am not sure if I understand. the URL is passed as `broker_url` or as `result_backend`?
> Hi @Insension. I am not sure if I understand. the URL is passed as `broker_url` or as `result_backend`?

Run into the same issue when using the string as broker_url.
> Hi @Insension. I am not sure if I understand. the URL is passed as `broker_url` or as `result_backend`?

Hi @matusvalo , sorry for the late reply; yes this issue arose when attempting to connect to redis as a message broker using 'user:password@localhost:6379/0' `broker_url` format, similar to what @gainskills is experiencing. When using this URL as `result_backend` string, the same issue also occurs.

I can see that my suggestion that this can possibly be traced back to /celery/backend/redis.py could lead to confusion. Is this not the script that handles connecting to redis for both `broker_url` and `result_backend` though?
> > Hi @Insension. I am not sure if I understand. the URL is passed as `broker_url` or as `result_backend`?
> 
> Hi @matusvalo , sorry for the late reply; yes this issue arose when attempting to connect to redis as a message broker using 'user:password@localhost:6379/0' `broker_url` format, similar to what @gainskills is experiencing. When using this URL as `result_backend` string, the same issue also occurs.
> 
> I can see that my suggestion that this can possibly be traced back to /celery/backend/redis.py could lead to confusion. Is this not the script that handles connecting to redis for both `broker_url` and `result_backend` though?

figured my issue out: it caused by a special character in the password string. It back to work after I removed it.
> > > Hi @Insension. I am not sure if I understand. the URL is passed as `broker_url` or as `result_backend`?
> > 
> > 
> > Hi @matusvalo , sorry for the late reply; yes this issue arose when attempting to connect to redis as a message broker using 'user:password@localhost:6379/0' `broker_url` format, similar to what @gainskills is experiencing. When using this URL as `result_backend` string, the same issue also occurs.
> > I can see that my suggestion that this can possibly be traced back to /celery/backend/redis.py could lead to confusion. Is this not the script that handles connecting to redis for both `broker_url` and `result_backend` though?
> 
> figured my issue out: it caused by a special character in the password string. It back to work after I removed it.

Care to share a test case with us?
I have absolutely the same issue as OP, I am trying to use Redis as broker and backend. I am sure that login, password, host, port are correct since  Flask app ( redis-py  | run in another terminal window) connects to Redis without any problems.

Should I open my own Issue or I can follow with OP Issue?

```
R_LOGIN = "myuserlogin"
R_PSWD = "xfjhdsfhsjdfhk123123sddsfdsf24fdsfr43"
R_HOST = "127.0.0.1"

celery_app = Celery(
    'tasks',
    broker=f"redis://{R_LOGIN}:{R_PSWD}@{R_HOST}:6379/0",
    backend=f"redis://{R_LOGIN}:{R_PSWD}@{R_HOST}:6379/1"
)
```

Command `celery -A  celery_app worker --loglevel=INFO`

Error: 

```

- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app:         tasks:0x2b48d8dd088
- ** ---------- .> transport:   redis://myuserlogin:**@127.0.0.1:6379/0
- ** ---------- .> results:     redis://myuserlogin:**@127.0.0.1:6379/1
- *** --- * --- .> concurrency: 12 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
 -------------- [queues]
                .> celery           exchange=celery(direct) key=celery


[tasks]
  . celery_app.add

[2020-11-02 17:09:56,047: ERROR/MainProcess] consumer: Cannot connect to redis://myuserlogin:**@127.0.0.1:6379/0: WRONGPASS invalid username-password pair.
Trying again in 2.00 seconds... (1/100)

[2020-11-02 17:09:56,368: INFO/SpawnPoolWorker-11] child process 80676 calling self.run()

```
Unfortunately without investigation cannot help with this issue. In any case if we are talking about `broker_url` the problem is connected to kombu because the connection string is passed there.
> Unfortunately without investigation cannot help with this issue. In any case if we are talking about `broker_url` the problem is connected to kombu because the connection string is passed there.

Actually it seems that handler just drops login for Redis:

https://github.com/celery/celery/blob/master/celery/backends/redis.py

```
    def _params_from_url(self, url, defaults):
        #  no login - _ it is not used:
        scheme, host, port, _, password, path, query = _parse_url(url)
        connparams = dict(
            defaults, **dictfilter({
                'host': host, 'port': port, 'password': password,
                'db': query.pop('virtual_host', None)})
        )
```
Because I think Redis used to not have a username. I might be wrong.
@ArtyomKozyrev8 Care to provide a fix?
> Because I think Redis used to not have a username. I might be wrong.
> @ArtyomKozyrev8 Care to provide a fix?

Yes it is a new feature available from Redis 6.0.0 Actually it is not really new, I have Redis 6 in production for at least 6 month already.

[https://redis.io/commands/acl-cat](https://redis.io/commands/acl-cat)

@thedrow yes I would like to have the feature, since Redis 6.0 make you to use login if you want to use password.
> > Because I think Redis used to not have a username. I might be wrong.
> > @ArtyomKozyrev8 Care to provide a fix?
> 
> Yes it is a new feature available from Redis 6.0.0 Actually it is not really new, I have Redis 6 in production for at least 6 month already.
> 
> https://redis.io/commands/acl-cat
> 
> @thedrow yes I would like to have the feature, since Redis 6.0 make you to use login if you want to use password.

please feel free to come with a PR so that we can also provide input
@ArtyomKozyrev8 Any chance you'll reach to this soon?
FYI @ArtyomKozyrev8 Heroku ist going to phase out Redis 4/5 and force-switch everybody to Redis 6 on June 30, 2021:

https://devcenter.heroku.com/changelog-items/2078

Resolving patch:
diff --git a/celery/app/defaults.py b/celery/app/defaults.py
--- a/celery/app/defaults.py
+++ b/celery/app/defaults.py
@@ -176,6 +176,7 @@ def __repr__(self):
         db=Option(type='int'),
         host=Option(type='string'),
         max_connections=Option(type='int'),
+        username=Option(type='string'),
         password=Option(type='string'),
         port=Option(type='int'),
         socket_timeout=Option(120.0, type='float'),
diff --git a/celery/backends/redis.py b/celery/backends/redis.py
--- a/celery/backends/redis.py
+++ b/celery/backends/redis.py
@@ -233,6 +233,17 @@ def __init__(self, host=None, port=None, db=None, password=None,
                 socket_connect_timeout and float(socket_connect_timeout),
         }
 
+        username = _get('redis_username')
+        if username:
+            # We're extra careful to avoid including this configuration value
+            # if it wasn't specified since older versions of py-redis
+            # don't support specifying a username.
+            # Only Redis>6.0 supports username/password authentication.
+
+            # TODO: Include this in connparams' definition once we drop
+            #       support for py-redis<3.4.0.
+            self.connparams['username'] = username
+
         if health_check_interval:
             self.connparams["health_check_interval"] = health_check_interval
 
@@ -285,11 +296,11 @@ def __init__(self, host=None, port=None, db=None, password=None,
         )
 
     def _params_from_url(self, url, defaults):
-        scheme, host, port, _, password, path, query = _parse_url(url)
+        scheme, host, port, username, password, path, query = _parse_url(url)
         connparams = dict(
             defaults, **dictfilter({
-                'host': host, 'port': port, 'password': password,
-                'db': query.pop('virtual_host', None)})
+                'host': host, 'port': port, 'username': username,
+                'password': password, 'db': query.pop('virtual_host', None)})
         )
 
         if scheme == 'socket':


Test patch:
diff --git a/t/unit/backends/test_redis.py b/t/unit/backends/test_redis.py
--- a/t/unit/backends/test_redis.py
+++ b/t/unit/backends/test_redis.py
@@ -340,6 +340,20 @@ def test_no_redis(self):
         with pytest.raises(ImproperlyConfigured):
             self.Backend(app=self.app)
 
+    def test_username_password_from_redis_conf(self):
+        self.app.conf.redis_password = 'password'
+        x = self.Backend(app=self.app)
+
+        assert x.connparams
+        assert 'username' not in x.connparams
+        assert x.connparams['password'] == 'password'
+        self.app.conf.redis_username = 'username'
+        x = self.Backend(app=self.app)
+
+        assert x.connparams
+        assert x.connparams['username'] == 'username'
+        assert x.connparams['password'] == 'password'
+
     def test_url(self):
         self.app.conf.redis_socket_timeout = 30.0
         self.app.conf.redis_socket_connect_timeout = 100.0
@@ -353,6 +367,19 @@ def test_url(self):
         assert x.connparams['password'] == 'bosco'
         assert x.connparams['socket_timeout'] == 30.0
         assert x.connparams['socket_connect_timeout'] == 100.0
+        assert 'username' not in x.connparams
+
+        x = self.Backend(
+            'redis://username:bosco@vandelay.com:123//1', app=self.app,
+        )
+        assert x.connparams
+        assert x.connparams['host'] == 'vandelay.com'
+        assert x.connparams['db'] == 1
+        assert x.connparams['port'] == 123
+        assert x.connparams['username'] == 'username'
+        assert x.connparams['password'] == 'bosco'
+        assert x.connparams['socket_timeout'] == 30.0
+        assert x.connparams['socket_connect_timeout'] == 100.0
 
     def test_timeouts_in_url_coerced(self):
         pytest.importorskip('redis')