diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index bc8f9a7..26851de 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -450,6 +450,37 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn return fmt.Errorf("must provide at least one of: --field, --mfa-option-id, or --sso-button-selector") } + // Resolve MFA option: the user may pass the label (e.g. "Get a text"), the + // type (e.g. "sms"), or the display string ("Get a text (sms)"). The API + // expects the type, so look up the connection's available options and map + // whatever the user provided to the correct type value. + if hasMfaOption { + conn, err := c.svc.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: fmt.Errorf("failed to fetch connection for MFA option resolution: %w", err)} + } + if len(conn.MfaOptions) > 0 { + resolved := false + for _, opt := range conn.MfaOptions { + displayName := fmt.Sprintf("%s (%s)", opt.Label, opt.Type) + if strings.EqualFold(in.MfaOptionID, opt.Type) || + strings.EqualFold(in.MfaOptionID, opt.Label) || + strings.EqualFold(in.MfaOptionID, displayName) { + in.MfaOptionID = opt.Type + resolved = true + break + } + } + if !resolved { + available := make([]string, 0, len(conn.MfaOptions)) + for _, opt := range conn.MfaOptions { + available = append(available, fmt.Sprintf("%s (%s)", opt.Label, opt.Type)) + } + return fmt.Errorf("unknown MFA option %q; available: %s", in.MfaOptionID, strings.Join(available, ", ")) + } + } + } + params := kernel.AuthConnectionSubmitParams{ SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{ Fields: in.FieldValues, diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index 41bcb28..ca12c91 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -196,3 +196,138 @@ func TestAuthConnectionsList_JSONOutput_PrintsRawResponse(t *testing.T) { assert.Contains(t, out, "\"profile_name\"") assert.Contains(t, out, "\"raf-leaseweb\"") } + +func newFakeWithMfaOptions(options []kernel.ManagedAuthMfaOption) *FakeAuthConnectionService { + return &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return &kernel.ManagedAuth{ + ID: id, + MfaOptions: options, + }, nil + }, + SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + }, + } +} + +func TestSubmit_MfaOptionResolvesType(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + {Label: "Have us call you", Type: "call"}, + }) + + var submittedID string + fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submittedID = body.SubmitFieldsRequest.MfaOptionID.Value + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "sms", + Output: "json", + }) + require.NoError(t, err) + assert.Equal(t, "sms", submittedID) +} + +func TestSubmit_MfaOptionResolvesLabel(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + {Label: "Have us call you", Type: "call"}, + }) + + var submittedID string + fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submittedID = body.SubmitFieldsRequest.MfaOptionID.Value + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "Get a text", + Output: "json", + }) + require.NoError(t, err) + assert.Equal(t, "sms", submittedID) +} + +func TestSubmit_MfaOptionResolvesDisplayString(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + }) + + var submittedID string + fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submittedID = body.SubmitFieldsRequest.MfaOptionID.Value + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "Get a text (sms)", + Output: "json", + }) + require.NoError(t, err) + assert.Equal(t, "sms", submittedID) +} + +func TestSubmit_MfaOptionResolvesLabelCaseInsensitive(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + }) + + var submittedID string + fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submittedID = body.SubmitFieldsRequest.MfaOptionID.Value + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "get a TEXT", + Output: "json", + }) + require.NoError(t, err) + assert.Equal(t, "sms", submittedID) +} + +func TestSubmit_MfaOptionGetErrorSurfaced(t *testing.T) { + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return nil, errors.New("connection not found") + }, + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "sms", + Output: "json", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "connection not found") +} + +func TestSubmit_MfaOptionRejectsUnknown(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + {Label: "Have us call you", Type: "call"}, + }) + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "carrier pigeon", + Output: "json", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown MFA option") + assert.Contains(t, err.Error(), "carrier pigeon") + assert.Contains(t, err.Error(), "Get a text (sms)") +}