diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index aa4432851..b0c452c1c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -138,60 +138,57 @@ one could use the User model that can be found in `/asreview/webapp/authenticati ### Full configuration -To configure the authentication in more detail we need to create a JSON file -that contains all authentication parameters. The keys in that JSON file will override any parameter that was passed in the CLI. Here's an example: -```json -{ - "DEBUG": true, - "AUTHENTICATION_ENABLED": true, - "SECRET_KEY": "", - "SECURITY_PASSWORD_SALT": "", - "SESSION_COOKIE_SECURE": true, - "REMEMBER_COOKIE_SECURE": true, - "SESSION_COOKIE_SAMESITE": "Lax", - "SQLALCHEMY_TRACK_MODIFICATIONS": true, - "ALLOW_ACCOUNT_CREATION": true, - "EMAIL_VERIFICATION": true, - "EMAIL_CONFIG": { - "SERVER": "", - "PORT": "", - "USERNAME": "", - "PASSWORD": "", - "USE_TLS": false, - "USE_SSL": true, - "REPLY_ADDRESS": "" - }, - "OAUTH": { - "GitHub": { - "AUTHORIZATION_URL": "https://github.com/login/oauth/authorize", - "TOKEN_URL": "https://github.com/login/oauth/access_token", - "CLIENT_ID": "", - "CLIENT_SECRET": "", - "SCOPE": "" - }, - "Orcid": { - "AUTHORIZATION_URL": "https://sandbox.orcid.org/oauth/authorize", - "TOKEN_URL": "https://sandbox.orcid.org/oauth/token", - "CLIENT_ID": "", - "CLIENT_SECRET": "", - "SCOPE": "/authenticate" - }, - "Google": { - "AUTHORIZATION_URL": "https://accounts.google.com/o/oauth2/auth", - "TOKEN_URL": "https://oauth2.googleapis.com/token", - "CLIENT_ID": "", - "CLIENT_SECRET": "", - "SCOPE": "profile email" - } - } -} -``` -Store the JSON file on the server and start the ASReview application from the CLI with the +To configure the authentication in more detail we need to create a TOML or a JSON file that contains all authentication parameters. The keys in that TOML/JSON file will override any parameter that was passed in the CLI. Here's an example of a TOML file: +```toml +DEBUG = true +AUTHENTICATION_ENABLED = true +SECRET_KEY = "" +SECURITY_PASSWORD_SALT = "" +SESSION_COOKIE_SECURE = true +REMEMBER_COOKIE_SECURE = true +SESSION_COOKIE_SAMESITE = "Lax" +SQLALCHEMY_TRACK_MODIFICATIONS = true +ALLOW_ACCOUNT_CREATION = true +ALLOW_TEAMS = false +EMAIL_VERIFICATION = false + +[EMAIL_CONFIG] +SERVER = "" +PORT = 465 +USERNAME = "" +PASSWORD = "" +USE_TLS = false +USE_SSL = true +REPLY_ADDRESS = "" + +[OAUTH] + [OAUTH.GitHub] + AUTHORIZATION_URL = "https://github.com/login/oauth/authorize" + TOKEN_URL = "https://github.com/login/oauth/access_token" + CLIENT_ID = "" + CLIENT_SECRET = "" + SCOPE = "" + + [OAUTH.Orcid] + AUTHORIZATION_URL = "https://sandbox.orcid.org/oauth/authorize" + TOKEN_URL = "https://sandbox.orcid.org/oauth/token" + CLIENT_ID = "" + CLIENT_SECRET = "" + SCOPE = "/authenticate" + + [OAUTH.Google] + AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/auth" + TOKEN_URL = "https://oauth2.googleapis.com/token" + CLIENT_ID = "" + CLIENT_SECRET = "" + SCOPE = "profile email" +``` +Store the TOML file on the server and start the ASReview application from the CLI with the `--flask-configfile` parameter: ``` -$ python3 -m asreview lab --flask-configfile= +$ python3 -m asreview lab --flask-configfile= ``` -A number of the keys in the JSON file are standard Flask parameters. The keys that are specific for authenticating ASReview are summarised below: +A number of the keys in the TOML file are standard Flask parameters. The keys that are specific for authenticating ASReview are summarised below: * AUTHENTICATION_ENABLED: if set to `true` the application will start with authentication enabled. If the SQLite database does not exist, one will be created during startup. * SECRET_KEY: the secret key is a string that is used to encrypt cookies and is mandatory if authentication is required. * SECURITY_PASSWORD_SALT: another string used to hash passwords, also mandatory if authentication is required. @@ -204,12 +201,10 @@ A number of the keys in the JSON file are standard Flask parameters. The keys th There are three optional parameters available that control what address the ASReview server listens to, and avoid CORS issues: -```json -{ - "HOST": "0.0.0.0", - "PORT": 5001, - "ALLOWED_ORIGINS": ["http://localhost:3001"], -} +```toml +HOST = "0.0.0.0" +PORT = 5001 +ALLOWED_ORIGINS = ["http://localhost:3000"] ``` The HOST and PORT determine what address the ASReview server listens to. If this deviates from `localhost` and port 5000, and you run the front end separately, make sure the [front end can find the backend](#front-end-development-and-connectioncors-issues). The ALLOWED_ORIGINS key must be set if you run the front end separately. Put in a list all URLs that your front end uses. This can be more than one URL. Failing to do so will certainly lead to CORS issues. diff --git a/asreview/webapp/start_flask.py b/asreview/webapp/start_flask.py index 0accb0bca..a1b9b8901 100644 --- a/asreview/webapp/start_flask.py +++ b/asreview/webapp/start_flask.py @@ -13,14 +13,19 @@ # limitations under the License. import argparse -import json import logging import os import socket +import sys import webbrowser from pathlib import Path from threading import Timer +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + from flask import Flask from flask import send_from_directory from flask.json import jsonify @@ -112,12 +117,11 @@ def _open_browser(host, port, protocol, no_browser): def _lab_parser(): - # parse arguments if available parser = argparse.ArgumentParser( prog="lab", description="""ASReview LAB - Active learning for Systematic Reviews.""", # noqa - formatter_class=argparse.RawTextHelpFormatter + formatter_class=argparse.RawTextHelpFormatter, ) parser.add_argument( @@ -183,7 +187,7 @@ def _lab_parser(): "--flask-configfile", default="", type=str, - help="Full path to a JSON file containing Flask parameters" + help="Full path to a TOML file containing Flask parameters" "for authentication.", ) @@ -264,7 +268,13 @@ def create_app(**kwargs): config_file_path = kwargs.get("flask_configfile", "").strip() # Use absolute path, because otherwise it is relative to the config root. if config_file_path != "": - app.config.from_file(Path(config_file_path).absolute(), load=json.load) + config_file_path = Path(config_file_path) + if config_file_path.suffix == ".toml": + app.config.from_file( + config_file_path.absolute(), load=tomllib.load, text=False + ) + else: + raise ValueError("'flask_configfile' should have a .toml extension") # If the frontend runs on a different port, or even on a different # URL, then allowed-origins must be set to avoid CORS issues. You can @@ -361,11 +371,7 @@ def load_user(user_id): # allowed origins to avoid CORS problems. The allowed-origins # can be set in the config file. if app.config.get("ALLOWED_ORIGINS", False): - CORS( - app, - origins=app.config.get("ALLOWED_ORIGINS"), - supports_credentials=True - ) + CORS(app, origins=app.config.get("ALLOWED_ORIGINS"), supports_credentials=True) with app.app_context(): app.register_blueprint(projects.bp) diff --git a/asreview/webapp/tests/config/auth_basic_config.json b/asreview/webapp/tests/config/auth_basic_config.json deleted file mode 100644 index 094950dbd..000000000 --- a/asreview/webapp/tests/config/auth_basic_config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "TESTING": true, - "DEBUG": true, - "SECRET_KEY": "my_very_secret_key", - "SECURITY_PASSWORD_SALT": "my_salt", - "AUTHENTICATION_ENABLED": true, - "ALLOW_ACCOUNT_CREATION": true -} \ No newline at end of file diff --git a/asreview/webapp/tests/config/auth_basic_config.toml b/asreview/webapp/tests/config/auth_basic_config.toml new file mode 100644 index 000000000..457c7bef0 --- /dev/null +++ b/asreview/webapp/tests/config/auth_basic_config.toml @@ -0,0 +1,6 @@ +TESTING = true +DEBUG = true +SECRET_KEY = "my_very_secret_key" +SECURITY_PASSWORD_SALT = "my_salt" +AUTHENTICATION_ENABLED = true +ALLOW_ACCOUNT_CREATION = true \ No newline at end of file diff --git a/asreview/webapp/tests/config/auth_no_creation.json b/asreview/webapp/tests/config/auth_no_creation.json deleted file mode 100644 index 62c926370..000000000 --- a/asreview/webapp/tests/config/auth_no_creation.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "TESTING": true, - "DEBUG": true, - "SECRET_KEY": "my_very_secret_key", - "SECURITY_PASSWORD_SALT": "my_salt", - "AUTHENTICATION_ENABLED": true, - "ALLOW_ACCOUNT_CREATION": false -} \ No newline at end of file diff --git a/asreview/webapp/tests/config/auth_no_creation.toml b/asreview/webapp/tests/config/auth_no_creation.toml new file mode 100644 index 000000000..3de4ccd1a --- /dev/null +++ b/asreview/webapp/tests/config/auth_no_creation.toml @@ -0,0 +1,6 @@ +TESTING = true +DEBUG = true +SECRET_KEY = "my_very_secret_key" +SECURITY_PASSWORD_SALT = "my_salt" +AUTHENTICATION_ENABLED = true +ALLOW_ACCOUNT_CREATION = false \ No newline at end of file diff --git a/asreview/webapp/tests/config/auth_verified_config.json b/asreview/webapp/tests/config/auth_verified_config.json deleted file mode 100644 index ae9a7b0f3..000000000 --- a/asreview/webapp/tests/config/auth_verified_config.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "TESTING": true, - "DEBUG": true, - "SECRET_KEY": "my_very_secret_key", - "SECURITY_PASSWORD_SALT": "my_salt", - "AUTHENTICATION_ENABLED": true, - "ALLOW_ACCOUNT_CREATION": true, - "EMAIL_VERIFICATION": true, - "EMAIL_CONFIG": { - "SERVER": "localhost", - "PORT": 465, - "USERNAME": "admin@asreview.nl", - "PASSWORD": "secret_password", - "USE_TLS": false, - "USE_SSL": true, - "REPLY_ADDRESS": "no_reply@asreview.nl" - } -} \ No newline at end of file diff --git a/asreview/webapp/tests/config/auth_verified_config.toml b/asreview/webapp/tests/config/auth_verified_config.toml new file mode 100644 index 000000000..a2b2be0ff --- /dev/null +++ b/asreview/webapp/tests/config/auth_verified_config.toml @@ -0,0 +1,16 @@ +TESTING = true +DEBUG = true +SECRET_KEY = "my_very_secret_key" +SECURITY_PASSWORD_SALT = "my_salt" +AUTHENTICATION_ENABLED = true +ALLOW_ACCOUNT_CREATION = true +EMAIL_VERIFICATION = true + +[EMAIL_CONFIG] +SERVER = "localhost" +PORT = 465 +USERNAME = "admin@asreview.nl" +PASSWORD = "secret_password" +USE_TLS = false +USE_SSL = true +REPLY_ADDRESS = "no_reply@asreview.nl" \ No newline at end of file diff --git a/asreview/webapp/tests/config/no_auth_config.json b/asreview/webapp/tests/config/no_auth_config.json deleted file mode 100644 index d9714b491..000000000 --- a/asreview/webapp/tests/config/no_auth_config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "TESTING": true, - "DEBUG": true, - "AUTHENTICATION_ENABLED": false -} \ No newline at end of file diff --git a/asreview/webapp/tests/config/no_auth_config.toml b/asreview/webapp/tests/config/no_auth_config.toml new file mode 100644 index 000000000..aa05e2a05 --- /dev/null +++ b/asreview/webapp/tests/config/no_auth_config.toml @@ -0,0 +1,3 @@ +TESTING = true +DEBUG = true +AUTHENTICATION_ENABLED = false \ No newline at end of file diff --git a/asreview/webapp/tests/conftest.py b/asreview/webapp/tests/conftest.py index 7c06e67a6..e21f74942 100644 --- a/asreview/webapp/tests/conftest.py +++ b/asreview/webapp/tests/conftest.py @@ -45,13 +45,13 @@ def _get_app(app_type="auth-basic", path=None): # get path of appropriate flask config base_dir = Path(__file__).resolve().parent / "config" if app_type == "auth-basic": - config_path = str(base_dir / "auth_basic_config.json") + config_path = str(base_dir / "auth_basic_config.toml") elif app_type == "auth-no-creation": - config_path = str(base_dir / "auth_no_creation.json") + config_path = str(base_dir / "auth_no_creation.toml") elif app_type == "auth-verified": - config_path = str(base_dir / "auth_verified_config.json") + config_path = str(base_dir / "auth_verified_config.toml") elif app_type == "no-auth": - config_path = str(base_dir / "no_auth_config.json") + config_path = str(base_dir / "no_auth_config.toml") else: raise ValueError(f"Unknown config {app_type}") # create app diff --git a/setup.py b/setup.py index 8b0cd960a..3a6eeeca9 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def get_long_description(): "rispy~=0.7.0", "xlrd>=1.0.0", "setuptools", - "flask>=2.0", + "flask>=2.3.0", "flask_cors", "flask-login", "flask-mail", @@ -63,6 +63,7 @@ def get_long_description(): "tqdm", "gevent>=20", "datahugger>=0.2", + "tomli", # included in Python 3.11 as tomllib ] if sys.version_info < (3, 10):