Repository: joke2k/django-environ

Issue:
Fix _cast_urlstr to use unquote vs unquote_plus
Currently, `Env.db_url_config` uses `_cast_urlstr` to url unquote the username and password from database connection URIs. The underlying problem occurs when you try to use a `+` in your postgres password. 

`_cast_urlstr` uses `unquote_plus`, ([introduced in this PR here](https://github.com/joke2k/django-environ/pull/69)), which which will turn plus signs into spaces when unquoted. This is undesired, as plus signs should only be replaced for spaces in HTML form values that are URL encoded, not in connection URIs.

Unquote docs
[unquote](https://docs.python.org/3/library/urllib.parse.html#urllib.parse.unquote) vs [unquote_plus](https://docs.python.org/3/library/urllib.parse.html#urllib.parse.unquote_plus)

This bug was discovered when using [CrunchyData's PGO](https://access.crunchydata.com/documentation/postgres-operator/v5/) on kubernetes. The PGO autogenerates postgres URIs for users and stores them inside of a kubernetes secret. An example URI is shown below:

```
postgresql://myuser:Le-%7BFsIaYnaQw%7Da2B%2F%5BV8bS+@postgres-pgbouncer.postgres-operator.svc:5432/mydb
```

Using the different unquotes to decode the password:
```
>>> unquote('Le-%7BFsIaYnaQw%7Da2B%2F%5BV8bS+')
'Le-{FsIaYnaQw}a2B/[V8bS+'

>>> unquote_plus('Le-%7BFsIaYnaQw%7Da2B%2F%5BV8bS+')
'Le-{FsIaYnaQw}a2B/[V8bS '
```
The first one can be used to sign into the database using both `psql` and `psycopg2.connect`, while the second gives an auth error. 

I ran the test suite for `django-environ` using both `unquote_plus` and `unquote` and all tests passed, regardless of which implementation is used, so I believe moving to `unquote` shouldn't be a huge change. I'll open a PR with changes and link them to this issue.



Discussion:

Patch:
diff --git a/environ/environ.py b/environ/environ.py
index acdfb15..0123c02 100644
--- a/environ/environ.py
+++ b/environ/environ.py
@@ -21,6 +21,7 @@ import warnings
 from urllib.parse import (
     parse_qs,
     ParseResult,
+    unquote,
     unquote_plus,
     urlparse,
     urlunparse,
@@ -61,7 +62,7 @@ def _cast_int(v):
 
 
 def _cast_urlstr(v):
-    return unquote_plus(v) if isinstance(v, str) else v
+    return unquote(v) if isinstance(v, str) else v
 
 
 class NoValue:


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 523a72d..f32d7cc 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -7,7 +7,7 @@
 # the LICENSE.txt file that was distributed with this source code.
 
 import pytest
-from environ.environ import _cast
+from environ.environ import _cast, _cast_urlstr
 
 
 @pytest.mark.parametrize(
@@ -20,3 +20,20 @@ def test_cast(literal):
 
     See https://github.com/joke2k/django-environ/issues/200 for details."""
     assert _cast(literal) == literal
+
+@pytest.mark.parametrize(
+    "quoted_url_str,expected_unquoted_str",
+    [
+        ("Le-%7BFsIaYnaQw%7Da2B%2F%5BV8bS+", "Le-{FsIaYnaQw}a2B/[V8bS+"),
+        ("my_test-string+", "my_test-string+"),
+        ("my%20test%20string+", "my test string+")
+    ]
+)
+def test_cast_urlstr(quoted_url_str, expected_unquoted_str):
+    """Make sure that a url str that contains plus sign literals does not get unquoted incorrectly
+    Plus signs should not be converted to spaces, since spaces are encoded with %20 in URIs
+
+    see https://github.com/joke2k/django-environ/issues/357 for details.
+    related to https://github.com/joke2k/django-environ/pull/69"""
+
+    assert _cast_urlstr(quoted_url_str) == expected_unquoted_str