From aacce1b6a90fa9dd64bb0104523aa0627fc7c03e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 17 Oct 2026 09:38:47 +0100 Subject: [PATCH 1/2] fix(cli): honour the self-signed setting when signing in with a password The endpoint check before sign-in trusted a self-signed certificate when the current session allowed it, but the sign-in client did not, so the request that followed the passing check failed TLS. The new session was also written without the setting, so later commands would have failed the same way. login now builds its client with the setting, stores it on the session it creates, and takes --self-signed (same true/false form as client) so a self-signed instance can be signed in to without a prior client call. --- templates/cli/internal/cmd/login.go | 40 ++++-- templates/cli/internal/cmd/login_test.go | 171 ++++++++++++++++++++++- 2 files changed, 196 insertions(+), 15 deletions(-) diff --git a/templates/cli/internal/cmd/login.go b/templates/cli/internal/cmd/login.go index f133f11ff0..a488b54300 100644 --- a/templates/cli/internal/cmd/login.go +++ b/templates/cli/internal/cmd/login.go @@ -31,11 +31,12 @@ import ( // loginOptions are the flags `login` was given. type loginOptions struct { - Endpoint string - Email string - Password string - MFA string - Code string + Endpoint string + Email string + Password string + MFA string + Code string + SelfSigned string // Switch changes which signed-in account is current, without signing in. Switch bool // New signs in again even when an account is already signed in. @@ -64,6 +65,8 @@ func newLoginCommand() *cobra.Command { strings.Join(mfaFactors, ", ")+". Pass the code itself with --code") flags.StringVar(&options.Code, "code", "", "MFA code from the factor chosen with --mfa, on self hosted instances") + flags.StringVar(&options.SelfSigned, "self-signed", "", + "Accept a self-signed certificate on self hosted instances ( true or false )") flags.BoolVar(&options.Switch, "switch", false, "Switch to another signed-in account") flags.BoolVar(&options.New, "new", false, "Sign in to another account") @@ -84,8 +87,9 @@ func runLogin(command *cobra.Command, options loginOptions) error { } previous := global.CurrentSessionID() - if options.Switch && (options.Password != "" || options.MFA != "" || options.Code != "") { - return errors.New("--password, --mfa and --code cannot be used with --switch. " + + if options.Switch && (options.Password != "" || options.MFA != "" || + options.Code != "" || options.SelfSigned != "") { + return errors.New("--password, --mfa, --code and --self-signed cannot be used with --switch. " + "Use --endpoint and/or --email to select a stored account") } @@ -97,6 +101,14 @@ func runLogin(command *cobra.Command, options loginOptions) error { return err } + selfSigned := global.CurrentBool(config.PreferenceSelfSigned) + if options.SelfSigned != "" { + selfSigned, err = strconv.ParseBool(options.SelfSigned) + if err != nil { + return fmt.Errorf("--self-signed must be true or false: %w", err) + } + } + configEndpoint := resolveLoginEndpoint(options.Endpoint) if options.Endpoint != "" && config.IsRegionalCloudEndpoint(options.Endpoint) { @@ -110,16 +122,15 @@ func runLogin(command *cobra.Command, options loginOptions) error { // Checked BEFORE anything is prompted for, so a wrong endpoint fails // immediately rather than after the email and password are typed. if options.Endpoint != "" && !cloud { - if err := verifyEndpoint(configEndpoint, - global.CurrentBool(config.PreferenceSelfSigned)); err != nil { + if err := verifyEndpoint(configEndpoint, selfSigned); err != nil { return err } } if cloud && (options.Email != "" || options.Password != "" || - options.MFA != "" || options.Code != "") { + options.MFA != "" || options.Code != "" || options.SelfSigned != "") { return fmt.Errorf("cloud sign-in happens in your browser. Run '%s login' "+ - "without --email, --password, --mfa or --code -- those options are "+ + "without --email, --password, --mfa, --code or --self-signed -- those options are "+ "for self-hosted instances", app.ExecutableName) } @@ -142,7 +153,7 @@ func runLogin(command *cobra.Command, options loginOptions) error { } if !cloud { - return loginWithPassword(command, global, configEndpoint, previous, options) + return loginWithPassword(command, global, configEndpoint, previous, selfSigned, options) } return loginWithDevice(command, configEndpoint) @@ -335,6 +346,7 @@ func loginWithPassword( command *cobra.Command, global *config.Global, endpoint, previous string, + selfSigned bool, options loginOptions, ) error { out := command.OutOrStdout() @@ -362,6 +374,9 @@ func loginWithPassword( session := config.NewObject() session.Set(config.PreferenceEndpoint, endpoint) session.Set(config.PreferenceEmail, email) + if selfSigned { + session.Set(config.PreferenceSelfSigned, true) + } global.AddSession(sessionID, session) global.SetCurrentSessionID(sessionID) if err := global.Write(); err != nil { @@ -376,6 +391,7 @@ func loginWithPassword( api := client.New(endpoint, app.Version). SetProject(config.ProjectConsole). + SetSelfSigned(selfSigned). SetLocale("en-US") credentials := jsonx.NewObject() diff --git a/templates/cli/internal/cmd/login_test.go b/templates/cli/internal/cmd/login_test.go index 207550c718..d21ac7eb68 100644 --- a/templates/cli/internal/cmd/login_test.go +++ b/templates/cli/internal/cmd/login_test.go @@ -18,7 +18,7 @@ func TestLoginOffersEveryOption(t *testing.T) { command := newLoginCommand() for _, name := range []string{ - "endpoint", "email", "password", "mfa", "code", "switch", "new", + "endpoint", "email", "password", "mfa", "code", "self-signed", "switch", "new", } { if command.Flags().Lookup(name) == nil { t.Errorf("--%s is missing", name) @@ -383,7 +383,7 @@ func TestLoginCompletesMFAWhenTheAccountAsksForMoreFactors(t *testing.T) { defer server.Close() command, _ := captureCommand() - err := loginWithPassword(command, global, server.URL, "", loginOptions{ + err := loginWithPassword(command, global, server.URL, "", false, loginOptions{ Email: "someone@example.com", Password: "password", MFA: "totp", @@ -435,7 +435,7 @@ func TestLoginAbandonsTheSessionWhenMFAFails(t *testing.T) { defer server.Close() command, _ := captureCommand() - err := loginWithPassword(command, global, server.URL, "", loginOptions{ + err := loginWithPassword(command, global, server.URL, "", false, loginOptions{ Email: "someone@example.com", Password: "password", MFA: "totp", @@ -478,3 +478,168 @@ func TestLoginRejectsAnUnknownFactor(t *testing.T) { t.Errorf("err = %v, want the accepted factors listed", err) } } + +func selfSignedAppwrite(t *testing.T) *httptest.Server { + t.Helper() + + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method + " " + r.URL.Path { + case "GET /health/version": + w.Write([]byte(`{"version":"1.8.0"}`)) + case "POST /account/sessions/email": + w.Header().Add("Set-Cookie", "a_session_console=secret") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{}`)) + case "GET /account": + w.Write([]byte(`{"email":"someone@example.com"}`)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + t.Cleanup(server.Close) + + return server +} + +func TestLoginSignsInWithTheStoredSelfSignedPreference(t *testing.T) { + preferencesWith(t, `{ + "current": "default", + "default": {"selfSigned": true} +}`) + server := selfSignedAppwrite(t) + command, _ := captureCommand() + + err := runLogin(command, loginOptions{ + Endpoint: server.URL, + Email: "someone@example.com", + Password: "password", + }) + if err != nil { + t.Fatalf("err = %v, want the sign-in to accept the self-signed certificate", err) + } + + stored, err := preferences() + if err != nil { + t.Fatal(err) + } + if !stored.CurrentBool(config.PreferenceSelfSigned) { + t.Error("the new session does not trust the self-signed certificate it signed in with") + } +} + +func TestLoginSelfSignedFlagWorksWithoutAPriorClientCall(t *testing.T) { + preferencesWith(t, `{}`) + server := selfSignedAppwrite(t) + command, _ := captureCommand() + + err := runLogin(command, loginOptions{ + Endpoint: server.URL, + Email: "someone@example.com", + Password: "password", + SelfSigned: "true", + }) + if err != nil { + t.Fatalf("err = %v, want --self-signed true to accept the certificate", err) + } + + stored, err := preferences() + if err != nil { + t.Fatal(err) + } + if !stored.CurrentBool(config.PreferenceSelfSigned) { + t.Error("the new session does not trust the self-signed certificate it signed in with") + } +} + +func TestLoginSelfSignedFlagOverridesTheStoredPreference(t *testing.T) { + preferencesWith(t, `{ + "current": "default", + "default": {"selfSigned": true} +}`) + server := selfSignedAppwrite(t) + command, _ := captureCommand() + + err := runLogin(command, loginOptions{ + Endpoint: server.URL, + Email: "someone@example.com", + Password: "password", + SelfSigned: "false", + }) + if err == nil { + t.Fatal("--self-signed false accepted an untrusted certificate") + } +} + +func TestLoginCompletesMFAOverASelfSignedCertificate(t *testing.T) { + global := preferencesWith(t, `{}`) + verified := false + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method + " " + r.URL.Path { + case "POST /account/sessions/email": + w.Header().Add("Set-Cookie", "a_session_console=partial") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{}`)) + case "GET /account": + if !verified { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"message":"More factors are required.","code":401,"type":"user_more_factors_required"}`)) + + return + } + w.Write([]byte(`{"email":"someone@example.com"}`)) + case "POST /account/mfa/challenges": + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"$id":"challenge"}`)) + case "PUT /account/mfa/challenges": + verified = true + w.Write([]byte(`{}`)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + command, _ := captureCommand() + + err := loginWithPassword(command, global, server.URL, "", true, loginOptions{ + Email: "someone@example.com", + Password: "password", + MFA: "totp", + Code: "123456", + }) + if err != nil { + t.Fatalf("err = %v, want MFA completed over the self-signed certificate", err) + } +} + +func TestLoginRejectsAnInvalidSelfSignedValue(t *testing.T) { + preferencesWith(t, `{}`) + + err := runLogin(newLoginCommand(), loginOptions{ + Endpoint: "http://127.0.0.1:1/v1", + SelfSigned: "maybe", + }) + if err == nil || !strings.Contains(err.Error(), "--self-signed must be true or false") { + t.Errorf("err = %v, want the invalid value rejected", err) + } +} + +func TestLoginRejectsSelfSignedAgainstCloud(t *testing.T) { + preferencesWith(t, `{}`) + + err := runLogin(newLoginCommand(), loginOptions{ + Endpoint: "https://cloud.appwrite.io/v1", + SelfSigned: "true", + }) + if err == nil || !strings.Contains(err.Error(), "--self-signed") { + t.Errorf("err = %v, want --self-signed rejected for Cloud", err) + } +} + +func TestLoginRejectsSelfSignedWithSwitch(t *testing.T) { + preferencesWith(t, `{}`) + + err := runLogin(newLoginCommand(), loginOptions{Switch: true, SelfSigned: "true"}) + if err == nil || !strings.Contains(err.Error(), "cannot be used with --switch") { + t.Errorf("err = %v, want --self-signed rejected with --switch", err) + } +} From 2c1d3a8556a80b0e38fe59a046a62ba2a75561db Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sat, 17 Oct 2026 10:31:47 +0100 Subject: [PATCH 2/2] fix(cli): reject an explicitly empty login --self-signed An empty value was indistinguishable from the flag being omitted, so --self-signed= silently fell back to the stored preference instead of being rejected like any other value that is not true or false. --- templates/cli/internal/cmd/login.go | 17 ++++++++++------ templates/cli/internal/cmd/login_test.go | 26 +++++++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/templates/cli/internal/cmd/login.go b/templates/cli/internal/cmd/login.go index a488b54300..5cf1c3766d 100644 --- a/templates/cli/internal/cmd/login.go +++ b/templates/cli/internal/cmd/login.go @@ -36,7 +36,7 @@ type loginOptions struct { Password string MFA string Code string - SelfSigned string + SelfSigned *string // Switch changes which signed-in account is current, without signing in. Switch bool // New signs in again even when an account is already signed in. @@ -45,11 +45,16 @@ type loginOptions struct { func newLoginCommand() *cobra.Command { options := loginOptions{} + selfSigned := "" command := &cobra.Command{ Use: "login", Short: "Sign in to an Appwrite endpoint", RunE: func(command *cobra.Command, args []string) error { + if command.Flags().Changed("self-signed") { + options.SelfSigned = &selfSigned + } + return runLogin(command, options) }, } @@ -65,7 +70,7 @@ func newLoginCommand() *cobra.Command { strings.Join(mfaFactors, ", ")+". Pass the code itself with --code") flags.StringVar(&options.Code, "code", "", "MFA code from the factor chosen with --mfa, on self hosted instances") - flags.StringVar(&options.SelfSigned, "self-signed", "", + flags.StringVar(&selfSigned, "self-signed", "", "Accept a self-signed certificate on self hosted instances ( true or false )") flags.BoolVar(&options.Switch, "switch", false, "Switch to another signed-in account") flags.BoolVar(&options.New, "new", false, "Sign in to another account") @@ -88,7 +93,7 @@ func runLogin(command *cobra.Command, options loginOptions) error { previous := global.CurrentSessionID() if options.Switch && (options.Password != "" || options.MFA != "" || - options.Code != "" || options.SelfSigned != "") { + options.Code != "" || options.SelfSigned != nil) { return errors.New("--password, --mfa, --code and --self-signed cannot be used with --switch. " + "Use --endpoint and/or --email to select a stored account") } @@ -102,8 +107,8 @@ func runLogin(command *cobra.Command, options loginOptions) error { } selfSigned := global.CurrentBool(config.PreferenceSelfSigned) - if options.SelfSigned != "" { - selfSigned, err = strconv.ParseBool(options.SelfSigned) + if options.SelfSigned != nil { + selfSigned, err = strconv.ParseBool(*options.SelfSigned) if err != nil { return fmt.Errorf("--self-signed must be true or false: %w", err) } @@ -128,7 +133,7 @@ func runLogin(command *cobra.Command, options loginOptions) error { } if cloud && (options.Email != "" || options.Password != "" || - options.MFA != "" || options.Code != "" || options.SelfSigned != "") { + options.MFA != "" || options.Code != "" || options.SelfSigned != nil) { return fmt.Errorf("cloud sign-in happens in your browser. Run '%s login' "+ "without --email, --password, --mfa, --code or --self-signed -- those options are "+ "for self-hosted instances", app.ExecutableName) diff --git a/templates/cli/internal/cmd/login_test.go b/templates/cli/internal/cmd/login_test.go index d21ac7eb68..c91fd5ed94 100644 --- a/templates/cli/internal/cmd/login_test.go +++ b/templates/cli/internal/cmd/login_test.go @@ -536,7 +536,7 @@ func TestLoginSelfSignedFlagWorksWithoutAPriorClientCall(t *testing.T) { Endpoint: server.URL, Email: "someone@example.com", Password: "password", - SelfSigned: "true", + SelfSigned: new("true"), }) if err != nil { t.Fatalf("err = %v, want --self-signed true to accept the certificate", err) @@ -563,7 +563,7 @@ func TestLoginSelfSignedFlagOverridesTheStoredPreference(t *testing.T) { Endpoint: server.URL, Email: "someone@example.com", Password: "password", - SelfSigned: "false", + SelfSigned: new("false"), }) if err == nil { t.Fatal("--self-signed false accepted an untrusted certificate") @@ -616,7 +616,7 @@ func TestLoginRejectsAnInvalidSelfSignedValue(t *testing.T) { err := runLogin(newLoginCommand(), loginOptions{ Endpoint: "http://127.0.0.1:1/v1", - SelfSigned: "maybe", + SelfSigned: new("maybe"), }) if err == nil || !strings.Contains(err.Error(), "--self-signed must be true or false") { t.Errorf("err = %v, want the invalid value rejected", err) @@ -628,7 +628,7 @@ func TestLoginRejectsSelfSignedAgainstCloud(t *testing.T) { err := runLogin(newLoginCommand(), loginOptions{ Endpoint: "https://cloud.appwrite.io/v1", - SelfSigned: "true", + SelfSigned: new("true"), }) if err == nil || !strings.Contains(err.Error(), "--self-signed") { t.Errorf("err = %v, want --self-signed rejected for Cloud", err) @@ -638,8 +638,24 @@ func TestLoginRejectsSelfSignedAgainstCloud(t *testing.T) { func TestLoginRejectsSelfSignedWithSwitch(t *testing.T) { preferencesWith(t, `{}`) - err := runLogin(newLoginCommand(), loginOptions{Switch: true, SelfSigned: "true"}) + err := runLogin(newLoginCommand(), loginOptions{Switch: true, SelfSigned: new("true")}) if err == nil || !strings.Contains(err.Error(), "cannot be used with --switch") { t.Errorf("err = %v, want --self-signed rejected with --switch", err) } } + +func TestLoginRejectsAnExplicitlyEmptySelfSigned(t *testing.T) { + preferencesWith(t, `{ + "current": "default", + "default": {"selfSigned": true} +}`) + command := newLoginCommand() + command.SetArgs([]string{"--endpoint", "http://127.0.0.1:1/v1", "--self-signed="}) + command.SilenceUsage = true + command.SilenceErrors = true + + err := command.Execute() + if err == nil || !strings.Contains(err.Error(), "--self-signed must be true or false") { + t.Errorf("err = %v, want an empty --self-signed rejected rather than inheriting the stored preference", err) + } +}