Repository: privacyidea/privacyidea

Issue:
Connection strings starting with "oracle+cx_oracle://" are not sanitized in the logs
<!--
Thank you for filing a bug within privacyIDEA. The more information you provide, the easier it is to identify the problem!

Issues are not meant for asking questions! You're welcome to ask questions in our community forum: https://community.privacyidea.org

privacyIDEA is licensed under the AGPL and comes without warranty. If you require reliable, professional help, please consider getting either
- consultancy 
  (https://netknights.it/en/leistungen/one-time-services/)
- or the privacyIDEA Enterprise Edition 
  (https://netknights.it/en/leistungen/service-level-agreements/)
-->

### Top-level intent

What did you try to achieve?

I was checking log sanitization and found the database password in the logs.

### Steps to reproduce

1. Add a sql resolver with driver "oracle+cx_oracle"
2. Go to the users list
3. Check the logs and find the line matching "using the connect string"

### Expected outcome

What do you think, how the system should have worked?

Expected the sanitized connect string "oracle+cx_oracle://user:xxxx@dsnname"

### Actual outcome

What did actually happen?

Found the original connect string "oracle+cx_oracle://user:password@dsnname"

### Configuration

* **privacyIDEA version**: 3.3.3
* **Installation method**: pip
* **Python version**: 3.6.8
* **Operating system**: RHEL 7.8
* **Webserver**: Nginx Unit 1.17
* **Token database**: Oracle 19

### Log file

**Set PI_LOGLEVEL = logging.DEBUG in pi.cfg and take a look at the privacyidea.log!**
**If appropriate, attach the log file or paste relevant portions.**

Redacted extract

```
[INFO][privacyidea.lib.resolvers.SQLIdResolver:559] using the connect string oracle+cx_oracle://<user>:<password>@<dsnname>
```



Discussion:
Note that there is an easy workaround for this database and it is to simply use oracle:// as the default driverfor this dialect is cx_oracle per https://docs.sqlalchemy.org/en/13/core/engines.html#oracle.
However there may be other combinations that confuse the parser.
This seems to be caused by urlparse not recognizing "oracle+cx_oracle" as a schema. It is used in [censor_connect_string](https://github.com/privacyidea/privacyidea/blob/3b260fe7dd7d9dcb33de6e52beee55efd134da6e/privacyidea/lib/utils/__init__.py#L1084)
The issue seems to be caused by the underscore ( _ ) character not being allowed in the schema per [RFC1708 Section 2.1](https://tools.ietf.org/html/rfc1738#section-2.1).
Urlparse works as expected and puts everything in path when not recognizing the schema [Ref.](https://docs.python.org/3/library/urllib.parse.html#url-parsing).
Interestingly, SQL Alchemy uses its own class to parse URLs and even has a way to hide passwords. Maybe this class can be used instead of parsing/unparsing the conn string with urlparse. See [here](https://github.com/sqlalchemy/sqlalchemy/blob/b330ffbc13ddb4274a004eab6a13ce40d641e555/lib/sqlalchemy/engine/url.py#L77). I've been playing with it in [repl.it](https://repl.it/@LuisGallo/sqlalchemyurl).
Maybe I can propose a solution using SQL Alchemy's own url class.

Resolving patch:
diff --git a/privacyidea/lib/utils/__init__.py b/privacyidea/lib/utils/__init__.py
--- a/privacyidea/lib/utils/__init__.py
+++ b/privacyidea/lib/utils/__init__.py
@@ -1077,20 +1077,12 @@ def censor_connect_string(connect_string):
     """
     Take a SQLAlchemy connect string and return a sanitized version
     that can be written to the log without disclosing the password.
-    The password is replaced with "xxxx".
+    The password is replaced with "***".
     In case any error occurs, return "<error when censoring connect string>"
     """
     try:
-        parsed = urlparse(connect_string)
-        if parsed.password is not None:
-            # We need to censor the ``netloc`` attribute: user:pass@host
-            _, host = parsed.netloc.rsplit("@", 1)
-            new_netloc = u'{}:{}@{}'.format(parsed.username, 'xxxx', host)
-            # Convert the URL to six components. netloc is component #1.
-            splitted = list(parsed)
-            splitted[1] = new_netloc
-            return urlunparse(splitted)
-        return connect_string
+        parsed = sqlalchemy.engine.url.make_url(connect_string)
+        return parsed.__repr__()
     except Exception:
         return "<error when censoring connect string>"
 


Test patch:
diff --git a/tests/test_lib_utils.py b/tests/test_lib_utils.py
--- a/tests/test_lib_utils.py
+++ b/tests/test_lib_utils.py
@@ -535,15 +535,17 @@ def test_18_censor_connect_string(self):
         self.assertEqual(censor_connect_string("mysql://pi@localhost/pi"),
                          "mysql://pi@localhost/pi")
         self.assertEqual(censor_connect_string("mysql://pi:kW44sqqWtGYX@localhost/pi"),
-                         "mysql://pi:xxxx@localhost/pi")
+                         "mysql://pi:***@localhost/pi")
         self.assertEqual(censor_connect_string("psql+odbc://pi@localhost/pi"),
                          "psql+odbc://pi@localhost/pi")
         self.assertEqual(censor_connect_string("psql+odbc://pi:MySecretPassword123466$@localhost/pi"),
-                         "psql+odbc://pi:xxxx@localhost/pi")
+                         "psql+odbc://pi:***@localhost/pi")
         self.assertEqual(censor_connect_string("mysql://pi:kW44s@@qqWtGYX@localhost/pi"),
-                         "mysql://pi:xxxx@localhost/pi")
+                         "mysql://pi:***@localhost/pi")
         self.assertEqual(censor_connect_string(u"mysql://knöbel:föö@localhost/pi"),
-                         u"mysql://knöbel:xxxx@localhost/pi")
+                         u"mysql://knöbel:***@localhost/pi")
+        self.assertEqual(censor_connect_string(u"oracle+cx_oracle://pi:MySecretPassword1234@localhost:1521/?service_name=my_database"),
+                         u"oracle+cx_oracle://pi:***@localhost:1521/?service_name=my_database")
 
     def test_19_truncate_comma_list(self):
         r = truncate_comma_list("123456,234567,345678", 19)