diff --git a/docs/source/explanations/security.md b/docs/source/explanations/security.md index 2e2bfcb30..df4fd0c29 100644 --- a/docs/source/explanations/security.md +++ b/docs/source/explanations/security.md @@ -35,8 +35,13 @@ client, a cookie will be set in your client and you won’t need to use the toke again. It is valid indefinitely. For horizontally-scaled deployments where you need multiple instances of the -server to share the same secret, you can set it via an environment variable like -so. +server to share the same secret, you can set it with a CLI option + +``` +tiled serve ... --api-key=YOUR_SECRET +``` + +or via an environment variable ``` TILED_SINGLE_USER_API_KEY=YOUR_SECRET tiled serve ... diff --git a/tiled/commandline/_serve.py b/tiled/commandline/_serve.py index e9859cfad..39e9d2372 100644 --- a/tiled/commandline/_serve.py +++ b/tiled/commandline/_serve.py @@ -34,6 +34,14 @@ def serve_directory( "this option selected." ), ), + api_key: str = typer.Option( + None, + "--api-key", + help=( + "Set the single-user API key. " + "By default, a random key is generated at startup and printed." + ), + ), keep_ext: bool = typer.Option( False, "--keep-ext", @@ -182,7 +190,10 @@ def serve_directory( register_logger.setLevel("INFO") web_app = build_app( catalog_adapter, - {"allow_anonymous_access": public}, + { + "allow_anonymous_access": public, + "single_user_api_key": api_key, + }, server_settings, ) if watch: @@ -270,6 +281,14 @@ def serve_catalog( "this option selected." ), ), + api_key: str = typer.Option( + None, + "--api-key", + help=( + "Set the single-user API key. " + "By default, a random key is generated at startup and printed." + ), + ), host: str = typer.Option( "127.0.0.1", help=( @@ -388,7 +407,13 @@ def serve_catalog( init_if_not_exists=init, ) web_app = build_app( - tree, {"allow_anonymous_access": public}, server_settings, scalable=scalable + tree, + { + "allow_anonymous_access": public, + "single_user_api_key": api_key, + }, + server_settings, + scalable=scalable, ) print_admin_api_key_if_generated(web_app, host=host, port=port) @@ -414,6 +439,14 @@ def serve_pyobject( "option selected." ), ), + api_key: str = typer.Option( + None, + "--api-key", + help=( + "Set the single-user API key. " + "By default, a random key is generated at startup and printed." + ), + ), host: str = typer.Option( "127.0.0.1", help=( @@ -453,7 +486,13 @@ def serve_pyobject( "available_bytes" ] = object_cache_available_bytes web_app = build_app( - tree, {"allow_anonymous_access": public}, server_settings, scalable=scalable + tree, + { + "allow_anonymous_access": public, + "single_user_api_key": api_key, + }, + server_settings, + scalable=scalable, ) print_admin_api_key_if_generated(web_app, host=host, port=port) @@ -507,6 +546,14 @@ def serve_config( "option selected." ), ), + api_key: str = typer.Option( + None, + "--api-key", + help=( + "Set the single-user API key. " + "By default, a random key is generated at startup and printed." + ), + ), host: str = typer.Option( None, help=( @@ -543,6 +590,11 @@ def serve_config( if "authentication" not in parsed_config: parsed_config["authentication"] = {} parsed_config["authentication"]["allow_anonymous_access"] = True + # Let --api-key flag override config. + if api_key: + if "authentication" not in parsed_config: + parsed_config["authentication"] = {} + parsed_config["authentication"]["single_user_api_key"] = api_key # Delay this import so that we can fail faster if config-parsing fails above. diff --git a/tiled/server/app.py b/tiled/server/app.py index 93bd50427..0c3ac7a71 100644 --- a/tiled/server/app.py +++ b/tiled/server/app.py @@ -409,6 +409,8 @@ def override_get_settings(): ]: if authentication.get(item) is not None: setattr(settings, item, authentication[item]) + if authentication.get("single_user_api_key") is not None: + settings.single_user_api_key_generated = False for item in [ "allow_origins", "response_bytesize_limit",