diff --git a/e2e/tests.bats b/e2e/tests.bats index 71547316..9a3424d7 100644 --- a/e2e/tests.bats +++ b/e2e/tests.bats @@ -207,7 +207,7 @@ wait_for_exporter() { jmp config client delete test-client-oidc - run jmp login test-client-oidc@${LOGIN_ENDPOINT} --insecure-login-http \ + run jmp login test-client-oidc@${LOGIN_ENDPOINT} --insecure --nointeractive \ --username test-client-oidc@example.com --password password --unsafe assert_success diff --git a/python/docs/source/getting-started/configuration/authentication.md b/python/docs/source/getting-started/configuration/authentication.md index f37886b4..524e5145 100644 --- a/python/docs/source/getting-started/configuration/authentication.md +++ b/python/docs/source/getting-started/configuration/authentication.md @@ -52,14 +52,14 @@ prefixed with "keycloak:" (e.g., keycloak:example-user). prefix usernames with `keycloak:` as configured in the claim mappings: ```console -$ jmp admin create client test-client --insecure-tls-config --oidc-username keycloak:developer-1 +$ jmp admin create client test-client --insecure --oidc-username keycloak:developer-1 ``` 4. Instruct users to log in with: ```console $ jmp login --client \ - --insecure-tls-config \ + --insecure \ --endpoint \ --namespace --name \ --issuer https:///realms/ @@ -69,7 +69,7 @@ For non-interactive login, add username and password: ```console $ jmp login --client [other parameters] \ - --insecure-tls-config \ + --insecure \ --username \ --password ``` @@ -84,7 +84,7 @@ For exporters, use similar login command but with the `--exporter` flag: ```console $ jmp login --exporter \ - --insecure-tls-config \ + --insecure \ --endpoint \ --namespace --name \ --issuer https:///realms/ @@ -197,7 +197,7 @@ spec: ```console $ jmp admin create exporter test-exporter --label foo=bar \ - --insecure-tls-config \ + --insecure \ --oidc-username dex:system:serviceaccount:default:test-service-account ``` @@ -207,7 +207,7 @@ For clients: ```console $ jmp login --client \ - --insecure-tls-config \ + --insecure \ --endpoint \ --namespace --name \ --issuer https://dex.dex.svc.cluster.local:5556 \ @@ -219,7 +219,7 @@ For exporters: ```console $ jmp login --exporter \ - --insecure-tls-config \ + --insecure \ --endpoint \ --namespace --name \ --issuer https://dex.dex.svc.cluster.local:5556 \ diff --git a/python/docs/source/getting-started/guides/setup-distributed-mode.md b/python/docs/source/getting-started/guides/setup-distributed-mode.md index 157ce72e..c0e3d794 100644 --- a/python/docs/source/getting-started/guides/setup-distributed-mode.md +++ b/python/docs/source/getting-started/guides/setup-distributed-mode.md @@ -7,7 +7,7 @@ controller service, configuring drivers, and running the exporter. The jumpstarter-controller endpoints are secured by TLS. However, in release 0.7.x, the certificates are self-signed and rotated on every restart. This means the client will not be able to verify the server certificate. To bypass this, you should use the -`--insecure-tls-config` flag when creating clients and exporters. This issue will be +`--insecure` flag when creating clients and exporters. This issue will be resolved in the next release. See [issue #72](https://github.com/jumpstarter-dev/jumpstarter/issues/72) for more details. Alternatively, you can configure the ingress/route in reencrypt mode with your own key and certificate. @@ -40,7 +40,7 @@ Run this command to create an exporter named `example-distributed` and save the configuration locally: ```console -$ jmp admin create exporter example-distributed --label foo=bar --save --insecure-tls-config +$ jmp admin create exporter example-distributed --label foo=bar --save --insecure ``` After creating the exporter, find the new configuration file at @@ -88,7 +88,7 @@ development purposes, and saves the configuration locally in `${HOME}/.config/jumpstarter/clients/`: ```console -$ jmp admin create client hello --save --unsafe --insecure-tls-config +$ jmp admin create client hello --save --unsafe --insecure ``` ### Spawn an Exporter Shell diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py index 295c527e..c1bde49d 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py @@ -6,9 +6,9 @@ from jumpstarter_cli_common.callbacks import ClickCallback from jumpstarter_cli_common.opt import ( OutputType, - confirm_insecure_tls, + confirm_insecure, opt_context, - opt_insecure_tls_config, + opt_insecure, opt_kubeconfig, opt_labels, opt_namespace, @@ -69,7 +69,7 @@ def create(): @opt_labels() @opt_kubeconfig @opt_context -@opt_insecure_tls_config +@opt_insecure @opt_oidc_username @opt_nointeractive @opt_output_all @@ -78,7 +78,7 @@ async def create_client( name: Optional[str], kubeconfig: Optional[str], context: Optional[str], - insecure_tls_config: bool, + insecure: bool, namespace: str, labels: dict[str, str], save: bool, @@ -91,7 +91,7 @@ async def create_client( ): """Create a client object in the Kubernetes cluster""" try: - confirm_insecure_tls(insecure_tls_config, nointeractive) + confirm_insecure(insecure, nointeractive) async with ClientsV1Alpha1Api(namespace, kubeconfig, context) as api: if output is None: # Only print status if is not JSON/YAML @@ -111,7 +111,7 @@ async def create_client( allow_drivers = allow.split(",") if allow is not None and len(allow) > 0 else [] client_config.drivers.unsafe = unsafe client_config.drivers.allow = allow_drivers - client_config.tls.insecure = insecure_tls_config + client_config.tls.insecure = insecure ClientConfigV1Alpha1.save(client_config, out) # If this is the only client config, set it as default if out is None and len(ClientConfigV1Alpha1.list().items) == 1: @@ -146,7 +146,7 @@ async def create_client( @opt_labels(required=True) @opt_kubeconfig @opt_context -@opt_insecure_tls_config +@opt_insecure @opt_oidc_username @opt_nointeractive @opt_output_all @@ -155,7 +155,7 @@ async def create_exporter( name: Optional[str], kubeconfig: Optional[str], context: Optional[str], - insecure_tls_config: bool, + insecure: bool, namespace: str, labels: dict[str, str], save: bool, @@ -166,7 +166,7 @@ async def create_exporter( ): """Create an exporter object in the Kubernetes cluster""" try: - confirm_insecure_tls(insecure_tls_config, nointeractive) + confirm_insecure(insecure, nointeractive) async with ExportersV1Alpha1Api(namespace, kubeconfig, context) as api: if output is None: click.echo(f"Creating exporter '{name}' in namespace '{namespace}'") @@ -176,7 +176,7 @@ async def create_exporter( if output is None: click.echo("Fetching exporter credentials from cluster") exporter_config = await api.get_exporter_config(name) - exporter_config.tls.insecure = insecure_tls_config + exporter_config.tls.insecure = insecure ExporterConfigV1Alpha1.save(exporter_config, out) if output is None: click.echo(f"Exporter configuration successfully saved to {exporter_config.path}") diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py index 5d4c2d9c..1235560a 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py @@ -118,7 +118,7 @@ def test_create_client( mock_get_client_config.return_value = INSECURE_TLS_CLIENT_CONFIG # Save with prompts accept insecure = Y, save = Y, unsafe = Y - result = runner.invoke(create, ["client", "--insecure-tls-config", CLIENT_NAME], input="Y\nY\nY\n") + result = runner.invoke(create, ["client", "--insecure", CLIENT_NAME], input="Y\nY\nY\n") assert result.exit_code == 0 assert "Client configuration successfully saved" in result.output mock_save_client.assert_called_once_with(INSECURE_TLS_CLIENT_CONFIG, None) @@ -126,7 +126,7 @@ def test_create_client( # Save no interactive and insecure tls result = runner.invoke( - create, ["client", "--insecure-tls-config", "--unsafe", "--save", "--nointeractive", CLIENT_NAME] + create, ["client", "--insecure", "--unsafe", "--save", "--nointeractive", CLIENT_NAME] ) assert result.exit_code == 0 assert "Client configuration successfully saved" in result.output @@ -137,7 +137,7 @@ def test_create_client( mock_get_client_config.return_value = INSECURE_TLS_CLIENT_CONFIG # Save with prompts accept insecure = N - result = runner.invoke(create, ["client", "--insecure-tls-config", CLIENT_NAME], input="n\n") + result = runner.invoke(create, ["client", "--insecure", CLIENT_NAME], input="n\n") assert result.exit_code == 1 assert "Aborted" in result.output @@ -295,7 +295,7 @@ def test_create_exporter( _get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG # Save with prompts accept insecure = Y, save = Y result = runner.invoke( - create, ["exporter", "--insecure-tls-config", EXPORTER_NAME, "--label", "foo=bar"], input="Y\nY\n" + create, ["exporter", "--insecure", EXPORTER_NAME, "--label", "foo=bar"], input="Y\nY\n" ) assert result.exit_code == 0 assert "Exporter configuration successfully saved" in result.output @@ -305,7 +305,7 @@ def test_create_exporter( _get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG # Save with prompts accept no interactive result = runner.invoke( - create, ["exporter", "--insecure-tls-config", "--nointeractive", "--save", EXPORTER_NAME, "--label", "foo=bar"] + create, ["exporter", "--insecure", "--nointeractive", "--save", EXPORTER_NAME, "--label", "foo=bar"] ) assert result.exit_code == 0 assert "Exporter configuration successfully saved" in result.output @@ -316,7 +316,7 @@ def test_create_exporter( _get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG # Save with prompts accept insecure = N result = runner.invoke( - create, ["exporter", "--insecure-tls-config", EXPORTER_NAME, "--label", "foo=bar"], input="n\n" + create, ["exporter", "--insecure", EXPORTER_NAME, "--label", "foo=bar"], input="n\n" ) assert result.exit_code == 1 assert "Aborted" in result.output diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/import_res.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/import_res.py index e4bb656f..b49cf381 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/import_res.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/import_res.py @@ -4,9 +4,9 @@ from jumpstarter_cli_common.blocking import blocking from jumpstarter_cli_common.opt import ( PathOutputType, - confirm_insecure_tls, + confirm_insecure, opt_context, - opt_insecure_tls_config, + opt_insecure, opt_kubeconfig, opt_namespace, opt_nointeractive, @@ -48,7 +48,7 @@ def import_res(): @opt_namespace @opt_kubeconfig @opt_context -@opt_insecure_tls_config +@opt_insecure @opt_output_path_only @opt_nointeractive @blocking @@ -57,7 +57,7 @@ async def import_client( namespace: str, kubeconfig: Optional[str], context: Optional[str], - insecure_tls_config: bool, + insecure: bool, allow: Optional[str], unsafe: bool, out: Optional[str], @@ -69,7 +69,7 @@ async def import_client( if out is None and ClientConfigV1Alpha1.exists(name): raise click.ClickException(f"A client with the name '{name}' already exists") try: - confirm_insecure_tls(insecure_tls_config, nointeractive) + confirm_insecure(insecure, nointeractive) async with ClientsV1Alpha1Api(namespace, kubeconfig, context) as api: if unsafe is False and allow is None and nointeractive is False: unsafe = click.confirm("Allow unsafe driver client imports?") @@ -81,7 +81,7 @@ async def import_client( click.echo("Fetching client credentials from cluster") allow_drivers = allow.split(",") if allow is not None and len(allow) > 0 else [] client_config = await api.get_client_config(name, allow=allow_drivers, unsafe=unsafe) - client_config.tls.insecure = insecure_tls_config + client_config.tls.insecure = insecure config_path = ClientConfigV1Alpha1.save(client_config, out) # If this is the only client config, set it as default if out is None and len(ClientConfigV1Alpha1.list().items) == 1: @@ -108,7 +108,7 @@ async def import_client( @opt_namespace @opt_kubeconfig @opt_context -@opt_insecure_tls_config +@opt_insecure @opt_output_path_only @opt_nointeractive @blocking @@ -118,7 +118,7 @@ async def import_exporter( out: Optional[str], kubeconfig: Optional[str], context: Optional[str], - insecure_tls_config: bool, + insecure: bool, output: PathOutputType, nointeractive: bool, ): @@ -130,12 +130,12 @@ async def import_exporter( else: raise click.ClickException(f'An exporter with the name "{name}" already exists') try: - confirm_insecure_tls(insecure_tls_config, nointeractive) + confirm_insecure(insecure, nointeractive) async with ExportersV1Alpha1Api(namespace, kubeconfig, context) as api: if output is None: click.echo("Fetching exporter credentials from cluster") exporter_config = await api.get_exporter_config(name) - exporter_config.tls.insecure = insecure_tls_config + exporter_config.tls.insecure = insecure config_path = ExporterConfigV1Alpha1.save(exporter_config, out) if output is None: click.echo(f"Exporter configuration successfully saved to {config_path}") diff --git a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/import_res_test.py b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/import_res_test.py index e375c1a0..c1dfe534 100644 --- a/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/import_res_test.py +++ b/python/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/import_res_test.py @@ -71,21 +71,21 @@ def test_import_client(_load_kube_config_mock, get_client_config_mock: AsyncMock get_client_config_mock.return_value = INSECURE_TLS_CLIENT_CONFIG # Save with prompts accept insecure = Y - result = runner.invoke(import_res, ["client", CLIENT_NAME, "--insecure-tls-config"], input="Y\nY\n") + result = runner.invoke(import_res, ["client", CLIENT_NAME, "--insecure"], input="Y\nY\n") assert result.exit_code == 0 assert "Client configuration successfully saved" in result.output save_client_config_mock.assert_called_once_with(INSECURE_TLS_CLIENT_CONFIG, None) save_client_config_mock.reset_mock() # Save with prompts no interactive prompts and insecure tls cert - result = runner.invoke(import_res, ["client", CLIENT_NAME, "--nointeractive", "--insecure-tls-config"]) + result = runner.invoke(import_res, ["client", CLIENT_NAME, "--nointeractive", "--insecure"]) assert result.exit_code == 0 assert "Client configuration successfully saved" in result.output save_client_config_mock.assert_called_once_with(INSECURE_TLS_CLIENT_CONFIG, None) save_client_config_mock.reset_mock() # Save with prompts accept insecure = N - result = runner.invoke(import_res, ["client", CLIENT_NAME, "--insecure-tls-config"], input="n\n") + result = runner.invoke(import_res, ["client", CLIENT_NAME, "--insecure"], input="n\n") assert result.exit_code == 1 assert "Aborted" in result.output save_client_config_mock.assert_not_called() @@ -168,14 +168,14 @@ def test_import_exporter(_load_kube_config_mock, _get_exporter_config_mock, save _get_exporter_config_mock.return_value = INSECURE_TLS_EXPORTER_CONFIG # Save with prompts accept insecure = Y - result = runner.invoke(import_res, ["exporter", EXPORTER_NAME, "--insecure-tls-config"], input="Y\n") + result = runner.invoke(import_res, ["exporter", EXPORTER_NAME, "--insecure"], input="Y\n") assert result.exit_code == 0 assert "Exporter configuration successfully saved" in result.output save_exporter_config_mock.assert_called_once_with(INSECURE_TLS_EXPORTER_CONFIG, None) save_exporter_config_mock.reset_mock() # Save with prompts accept insecure = N - result = runner.invoke(import_res, ["exporter", EXPORTER_NAME, "--insecure-tls-config"], input="n\n") + result = runner.invoke(import_res, ["exporter", EXPORTER_NAME, "--insecure"], input="n\n") assert result.exit_code == 1 assert "Aborted" in result.output save_exporter_config_mock.assert_not_called() diff --git a/python/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py b/python/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py index cdbeefc1..0fa81174 100644 --- a/python/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py +++ b/python/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py @@ -88,31 +88,30 @@ def _opt_labels_callback(ctx, param, value): callback=_opt_labels_callback, ) -opt_insecure_tls_config = click.option( - "--insecure-tls-config", - "insecure_tls_config", +opt_insecure = click.option( + "--insecure", is_flag=True, default=False, - help="Disable endpoint TLS verification. This is insecure and should only be used for testing purposes", + help="Disable TLS verification and allow insecure connections, including plain HTTP", ) +opt_insecure_tls = opt_insecure +opt_insecure_tls_config = opt_insecure -def confirm_insecure_tls(insecure_tls_config: bool, nointeractive: bool): - """Confirm if insecure TLS config is enabled and user wants to continue. - Args: - insecure_tls_config (bool): Insecure TLS config flag requested by the user. - nointeractive (bool): This flag is set to True if the command is run in non-interactive mode. - - Raises: - click.Abort: Abort the command if user does not want to continue. - """ - if nointeractive is False and insecure_tls_config: - if not click.confirm("Insecure TLS config is enabled. Are you sure you want to continue?"): +def confirm_insecure(insecure: bool, nointeractive: bool): + if nointeractive is False and insecure: + if not click.confirm( + "Insecure mode is enabled. TLS verification will be disabled" + " and plain HTTP may be used. Continue?" + ): click.echo("Aborting.") raise click.Abort() +confirm_insecure_tls = confirm_insecure + + class OutputMode(str): JSON = "json" YAML = "yaml" diff --git a/python/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt_test.py b/python/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt_test.py index 834a617e..9ef09db5 100644 --- a/python/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt_test.py +++ b/python/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt_test.py @@ -1,8 +1,11 @@ -"""Tests for SourcePrefixFormatter in opt.py.""" +"""Tests for SourcePrefixFormatter and insecure TLS option in opt.py.""" import logging -from jumpstarter_cli_common.opt import SourcePrefixFormatter +import click +from click.testing import CliRunner + +from jumpstarter_cli_common.opt import SourcePrefixFormatter, opt_insecure class TestSourcePrefixFormatter: @@ -76,3 +79,28 @@ def test_prefix_omitted_on_consecutive_same_source(self) -> None: ) formatted3 = formatter.format(record3) assert "[different.source]" in formatted3 + + +def _make_insecure_command(): + @click.command() + @opt_insecure + def cmd(insecure: bool): + click.echo(f"insecure={insecure}") + + return cmd + + +class TestInsecureOption: + def test_insecure_flag_is_accepted(self) -> None: + runner = CliRunner() + cmd = _make_insecure_command() + result = runner.invoke(cmd, ["--insecure"]) + assert result.exit_code == 0 + assert "insecure=True" in result.output + + def test_insecure_flag_defaults_to_false(self) -> None: + runner = CliRunner() + cmd = _make_insecure_command() + result = runner.invoke(cmd, []) + assert result.exit_code == 0 + assert "insecure=False" in result.output diff --git a/python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py b/python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py index 8f3c15ce..eeebfeb6 100644 --- a/python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py +++ b/python/packages/jumpstarter-cli/jumpstarter_cli/get_test.py @@ -241,7 +241,7 @@ def test_get_leases_calls_list_leases(self): with patch("jumpstarter_cli.get.model_print"): get_leases.callback.__wrapped__.__wrapped__( - config=config, selector=None, output="text", show_all=False + config=config, selector=None, output="text", show_all=False, all_clients=False ) config.list_leases.assert_called_once_with(filter=None, only_active=True) diff --git a/python/packages/jumpstarter-cli/jumpstarter_cli/login.py b/python/packages/jumpstarter-cli/jumpstarter_cli/login.py index 63938ae4..7d0a7cb7 100644 --- a/python/packages/jumpstarter-cli/jumpstarter_cli/login.py +++ b/python/packages/jumpstarter-cli/jumpstarter_cli/login.py @@ -9,11 +9,7 @@ from jumpstarter_cli_common.config import opt_config from jumpstarter_cli_common.exceptions import handle_exceptions from jumpstarter_cli_common.oidc import Config, decode_jwt_issuer, opt_oidc -from jumpstarter_cli_common.opt import ( - confirm_insecure_tls, - opt_insecure_tls_config, - opt_nointeractive, -) +from jumpstarter_cli_common.opt import confirm_insecure, opt_insecure, opt_nointeractive from jumpstarter.common.exceptions import ReauthenticationFailed from jumpstarter.config.client import ClientConfigV1Alpha1, ClientConfigV1Alpha1Drivers @@ -56,37 +52,19 @@ def _validate_auth_config_payload(payload: Any, source_url: str) -> dict[str, An async def fetch_auth_config( login_endpoint: str, - insecure_tls: bool = False, - use_http: bool = False, + insecure: bool = False, ) -> dict[str, Any]: - """Fetch authentication configuration from the login endpoint. - - Args: - login_endpoint: The login endpoint URL (e.g., login.example.com or https://login.example.com) - insecure_tls: Skip TLS certificate verification for HTTPS connections - use_http: Use HTTP instead of HTTPS (for local testing) + if login_endpoint.startswith("http://") and not insecure: + raise click.UsageError("HTTP login endpoints require --insecure.") - Returns: - Dictionary containing: - - grpcEndpoint: The gRPC controller endpoint - - routerEndpoint: The router endpoint (optional) - - namespace: Default namespace for clients - - caBundle: base64-encoded PEM CA certificate (optional) - - oidc: List of OIDC provider configurations (optional) - """ - # Ensure the URL has a scheme if not login_endpoint.startswith(("http://", "https://")): - scheme = "http" if use_http else "https" + scheme = "http" if insecure else "https" login_endpoint = f"{scheme}://{login_endpoint}" - _validate_login_endpoint_url(login_endpoint, allow_http=use_http) + _validate_login_endpoint_url(login_endpoint, allow_http=insecure) url = f"{login_endpoint.rstrip('/')}/v1/auth/config" - - # Configure SSL context: False disables verification, True enables it - ssl_context: ssl.SSLContext | bool = False if insecure_tls else True - - # Use a timeout to prevent the CLI from hanging indefinitely + ssl_context: ssl.SSLContext | bool = False if insecure else True timeout = aiohttp.ClientTimeout(total=_HTTP_TIMEOUT_SECONDS) try: @@ -159,19 +137,7 @@ def parse_login_argument(login_arg: str) -> tuple[str | None, str]: "--unsafe", is_flag=True, help="Should all driver client packages be allowed to load (UNSAFE!).", default=None ) # end client specific -@opt_insecure_tls_config -@click.option( - "--insecure-login-tls", - is_flag=True, - help="Skip TLS certificate verification when fetching config from login endpoint.", - default=False, -) -@click.option( - "--insecure-login-http", - is_flag=True, - help="Use HTTP instead of HTTPS when fetching config from login endpoint (for local testing).", - default=False, -) +@opt_insecure @opt_nointeractive @opt_config(allow_missing=True) @handle_exceptions @@ -191,9 +157,7 @@ async def login( # noqa: C901 callback_port: int | None, offline_access: bool, unsafe, - insecure_tls_config: bool, - insecure_login_tls: bool, - insecure_login_http: bool, + insecure: bool, nointeractive: bool, allow, ): @@ -212,9 +176,7 @@ async def login( # noqa: C901 - Default namespace """ - confirm_insecure_tls(insecure_tls_config, nointeractive) - if insecure_login_http and insecure_login_tls: - raise click.UsageError("--insecure-login-http and --insecure-login-tls cannot be used together.") + confirm_insecure(insecure, nointeractive) # Handle simplified login format: [client-name@]login.endpoint.com ca_bundle = None @@ -231,8 +193,7 @@ async def login( # noqa: C901 click.echo(f"Fetching configuration from {login_endpoint}...") auth_config = await fetch_auth_config( login_endpoint, - insecure_tls=insecure_login_tls or insecure_tls_config, - use_http=insecure_login_http, + insecure=insecure, ) # Use fetched values if not explicitly provided @@ -305,7 +266,7 @@ async def login( # noqa: C901 ) # Build TLS config with CA bundle if available - tls_config = TLSConfigV1Alpha1(insecure=insecure_tls_config, ca=ca_bundle or "") + tls_config = TLSConfigV1Alpha1(insecure=insecure, ca=ca_bundle or "") if kind.startswith("client"): config = ClientConfigV1Alpha1( diff --git a/python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py b/python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py index 021f9b0e..24af16db 100644 --- a/python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py +++ b/python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py @@ -1,6 +1,7 @@ import asyncio import json import ssl +from unittest.mock import AsyncMock, MagicMock, patch import click import pytest @@ -53,7 +54,7 @@ def test_validate_login_endpoint_url_rejects_unsupported_scheme() -> None: def test_validate_login_endpoint_url_rejects_http_without_explicit_opt_in() -> None: - with pytest.raises(click.ClickException, match="Use --insecure-login-http"): + with pytest.raises(click.ClickException, match="Use --insecure"): _validate_login_endpoint_url("http://login.example.com") @@ -150,22 +151,36 @@ async def fake_fetch_auth_config(*args, **kwargs): assert "TLS certificate verification failed" in result.output -def test_login_cli_rejects_conflicting_insecure_flags() -> None: - runner = CliRunner() - result = runner.invoke( - jmp, - [ - "login", - "login.example.com", - "--client-config", - "/tmp/nonexistent-client.yaml", - "--insecure-login-http", - "--insecure-login-tls", - ], - ) +@pytest.mark.asyncio +async def test_fetch_auth_config_rejects_http_without_insecure(): + with pytest.raises(click.UsageError, match="--insecure"): + await fetch_auth_config("http://login.example.com", insecure=False) - assert result.exit_code != 0 - assert "--insecure-login-http and --insecure-login-tls cannot be used together" in result.output + +@pytest.mark.asyncio +async def test_fetch_auth_config_allows_http_with_insecure(): + mock_response = MagicMock() + mock_response.status = 200 + mock_response.json = AsyncMock(return_value={"grpcEndpoint": "grpc.example.com"}) + + mock_get_cm = MagicMock() + mock_get_cm.__aenter__ = AsyncMock(return_value=mock_response) + mock_get_cm.__aexit__ = AsyncMock(return_value=False) + + mock_session = MagicMock() + mock_session.get = MagicMock(return_value=mock_get_cm) + + mock_client_cm = MagicMock() + mock_client_cm.__aenter__ = AsyncMock(return_value=mock_session) + mock_client_cm.__aexit__ = AsyncMock(return_value=False) + + with patch("aiohttp.ClientSession", return_value=mock_client_cm): + result = await fetch_auth_config("http://login.example.com", insecure=True) + + mock_session.get.assert_called_once() + call_url = mock_session.get.call_args[0][0] + assert "http://login.example.com" in call_url + assert result["grpcEndpoint"] == "grpc.example.com" def test_login_maps_ssl_cert_error_during_oidc_to_friendly_message(monkeypatch) -> None: diff --git a/python/packages/jumpstarter-driver-flashers/README.md b/python/packages/jumpstarter-driver-flashers/README.md index 0e725f3f..9e8f08d6 100644 --- a/python/packages/jumpstarter-driver-flashers/README.md +++ b/python/packages/jumpstarter-driver-flashers/README.md @@ -147,7 +147,7 @@ Options: --force-exporter-http Force use of exporter HTTP --force-flash-bundle TEXT Force use of a specific flasher OCI bundle --cacert FILE CA certificate to use for HTTPS - --insecure-tls Skip TLS certificate verification + --insecure Disable TLS verification and allow insecure connections --header TEXT Custom HTTP header in 'Key: Value' format --bearer TEXT Bearer token for HTTP authentication --retries INTEGER Number of retry attempts for flash operation