Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions templates/cli/internal/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -44,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)
},
}
Expand All @@ -64,6 +70,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(&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")

Expand All @@ -84,8 +92,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 != 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")
}

Expand All @@ -97,6 +106,14 @@ func runLogin(command *cobra.Command, options loginOptions) error {
return err
}

selfSigned := global.CurrentBool(config.PreferenceSelfSigned)
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)
}
}

configEndpoint := resolveLoginEndpoint(options.Endpoint)

if options.Endpoint != "" && config.IsRegionalCloudEndpoint(options.Endpoint) {
Expand All @@ -110,16 +127,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 != nil) {
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)
}

Expand All @@ -142,7 +158,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)
Expand Down Expand Up @@ -335,6 +351,7 @@ func loginWithPassword(
command *cobra.Command,
global *config.Global,
endpoint, previous string,
selfSigned bool,
options loginOptions,
) error {
out := command.OutOrStdout()
Expand Down Expand Up @@ -362,6 +379,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 {
Expand All @@ -376,6 +396,7 @@ func loginWithPassword(

api := client.New(endpoint, app.Version).
SetProject(config.ProjectConsole).
SetSelfSigned(selfSigned).
SetLocale("en-US")

credentials := jsonx.NewObject()
Expand Down
187 changes: 184 additions & 3 deletions templates/cli/internal/cmd/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -478,3 +478,184 @@ 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: new("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: new("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: 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)
}
}

func TestLoginRejectsSelfSignedAgainstCloud(t *testing.T) {
preferencesWith(t, `{}`)

err := runLogin(newLoginCommand(), loginOptions{
Endpoint: "https://cloud.appwrite.io/v1",
SelfSigned: new("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: 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)
}
}
Loading