Repository: ckan/ckan

Issue:
Exception on a fresh install / no session when trying to login
**CKAN version**
master/2.10

**Describe the bug**
On a fresh install, if `REMEMBER_COOKIE_DURATION`  is set in the ini file I get an exception trying to login

```
Traceback (most recent call last):
  File "/home/adria/dev/pyenvs/ckan-py3/lib/python3.8/site-packages/flask_login/login_manager.py", line 476, in _set_cookie
    expires = datetime.utcnow() + duration
TypeError: unsupported operand type(s) for +: 'datetime.datetime' and 'str'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/adria/dev/pyenvs/ckan-py3/lib/python3.8/site-packages/flask/app.py", line 1540, in finalize_request
    response = self.process_response(response)
  File "/home/adria/dev/pyenvs/ckan-py3/lib/python3.8/site-packages/flask/app.py", line 1885, in process_response
    response = self.ensure_sync(func)(response)
  File "/home/adria/dev/pyenvs/ckan-py3/lib/python3.8/site-packages/flask_login/login_manager.py", line 447, in _update_remember_cookie
    self._set_cookie(response)
  File "/home/adria/dev/pyenvs/ckan-py3/lib/python3.8/site-packages/flask_login/login_manager.py", line 478, in _set_cookie
    raise Exception(
Exception: REMEMBER_COOKIE_DURATION must be a datetime.timedelta, instead got: 31536000
```

**Steps to reproduce**
* Add `REMEMBER_COOKIE_DURATION = 31536000` to your ini file
* Delete the sessions folder in `/tmp/<ckan.site_id>`
* Try to login

**Additional details**
Based on the stack trace complaining about `duration` being a str, I guess that the problem is that we are passing raw config values to Flask's `app.config` object instead of parsed ones or something along those lines



Discussion:

Resolving patch:
diff --git a/ckan/common.py b/ckan/common.py
--- a/ckan/common.py
+++ b/ckan/common.py
@@ -128,6 +128,9 @@ def __delitem__(self, key: str):
         except RuntimeError:
             pass
 
+    def is_declared(self, key: str) -> bool:
+        return key in config_declaration
+
     def get_value(self, key: str) -> Any:
         if self.get("config.mode") == "strict":
             return self[key]
diff --git a/ckan/config/declaration/__init__.py b/ckan/config/declaration/__init__.py
--- a/ckan/config/declaration/__init__.py
+++ b/ckan/config/declaration/__init__.py
@@ -64,7 +64,7 @@ def __init__(self):
     def __bool__(self):
         return bool(self._members)
 
-    def __contains__(self, key: Key):
+    def __contains__(self, key: Union[Key, str]):
         return key in self._options
 
     def __getitem__(self, key: Key) -> Option[Any]:
diff --git a/ckan/config/middleware/flask_app.py b/ckan/config/middleware/flask_app.py
--- a/ckan/config/middleware/flask_app.py
+++ b/ckan/config/middleware/flask_app.py
@@ -163,7 +163,16 @@ def make_flask_stack(conf: Union[Config, CKANConfig]) -> CKANApp:
     # Update Flask config with the CKAN values. We use the common config
     # object as values might have been modified on `load_environment`
     if config:
-        app.config.update(config)
+        if config.get_value("config.mode") == "strict":
+            # Config values have been already parsed and validated
+            app.config.update(config)
+        else:
+            # Parse all values to ensure Flask gets the validated values
+            for key in config.keys():
+                if config.is_declared(key):
+                    app.config[key] = config.get_value(key)
+                else:
+                    app.config[key] = config.get(key)
     else:
         app.config.update(conf)
 


Test patch:
diff --git a/ckan/tests/config/test_middleware.py b/ckan/tests/config/test_middleware.py
--- a/ckan/tests/config/test_middleware.py
+++ b/ckan/tests/config/test_middleware.py
@@ -93,3 +93,18 @@ def test_all_plugin_blueprints_are_registered(app):
     assert url == "/another_simple_url"
     res = app.get(url, status=200)
     assert "Hello World, this is another view served from an extension" in res.body
+
+
+@pytest.mark.ckan_config("REMEMBER_COOKIE_DURATION", "12345")
+def test_flask_config_values_are_parsed(app):
+    assert (
+        app.flask_app.config["REMEMBER_COOKIE_DURATION"] == 12345
+    )
+
+
+@pytest.mark.ckan_config("config.mode", "strict")
+@pytest.mark.ckan_config("REMEMBER_COOKIE_DURATION", "12345")
+def test_flask_config_values_are_parsed_in_strict_mode(app):
+    assert (
+        app.flask_app.config["REMEMBER_COOKIE_DURATION"] == 12345
+    )