From 0625dba344e1de4fe8789dcf23e56557b85e246e Mon Sep 17 00:00:00 2001 From: "David E. Lovas" Date: Wed, 27 May 2026 02:14:41 -0400 Subject: [PATCH 1/5] fix: treat nil enabled_clients as enabled for all apps When enabled_clients is null in the API response (meaning the connection is enabled for all applications), GetEnabledClients() returns nil. len(nil) == 0 in Go, so the CLI incorrectly skips these connections, producing "no active database or passwordless connections" errors. This fix distinguishes between nil (all apps, no restriction) and an empty slice (explicitly disabled for all apps). Only the latter should be treated as disabled. Fixes all 3 occurrences in users.go: createUserCmd, importUsersCmd, and databaseAndPasswordlessConnectionOptions. --- internal/cli/users.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/cli/users.go b/internal/cli/users.go index 6284369d6..75ef6b4e9 100644 --- a/internal/cli/users.go +++ b/internal/cli/users.go @@ -404,7 +404,10 @@ func createUserCmd(cli *cli) *cobra.Command { return fmt.Errorf("failed to find connection with name %q: %w", inputs.connectionName, err) } - if len(connection.GetEnabledClients()) == 0 { + // A nil slice means enabled_clients was null in the API response, + // which means the connection is enabled for all applications. + // Only reject if explicitly set to an empty slice (disabled for all). + if enabledClients := connection.GetEnabledClients(); enabledClients != nil && len(enabledClients) == 0 { return fmt.Errorf( "failed to continue due to the connection with name %q being disabled, enable an application on this connection and try again", inputs.connectionName, @@ -894,7 +897,10 @@ The file size limit for a bulk import is 500KB. You will need to start multiple return fmt.Errorf("failed to read connection with name %q: %w", inputs.ConnectionName, err) } - if len(connection.GetEnabledClients()) == 0 { + // A nil slice means enabled_clients was null in the API response, + // which means the connection is enabled for all applications. + // Only reject if explicitly set to an empty slice (disabled for all). + if enabledClients := connection.GetEnabledClients(); enabledClients != nil && len(enabledClients) == 0 { return fmt.Errorf( "failed to continue due to the connection with name %q being disabled, enable an application on this connection and try again", inputs.ConnectionName, @@ -996,7 +1002,10 @@ func (c *cli) databaseAndPasswordlessConnectionOptions(ctx context.Context) ([]s var connectionNames []string for _, connection := range connectionList.Connections { - if len(connection.GetEnabledClients()) == 0 { + // A nil slice means enabled_clients was null in the API response, + // which means the connection is enabled for all applications. + // Only skip if explicitly set to an empty slice (disabled for all). + if enabledClients := connection.GetEnabledClients(); enabledClients != nil && len(enabledClients) == 0 { continue } From 649385bc0eb2abf070e0065d1523dc9630929bde Mon Sep 17 00:00:00 2001 From: "David E. Lovas" Date: Tue, 2 Jun 2026 18:10:58 -0400 Subject: [PATCH 2/5] fix: use ReadEnabledClients endpoint instead of deprecated enabled_clients field Replace connection.GetEnabledClients() with the dedicated ReadEnabledClients endpoint per the Auth0 migration guide. The enabled_clients field on connections is deprecated and will be removed in a future API version. Adds ReadEnabledClients to ConnectionAPI interface and uses a helper function to check client associations via the new GET /api/v2/connections/{id}/clients endpoint. --- internal/auth0/connection.go | 3 ++ internal/auth0/mock/connection_mock.go | 20 +++++++++++ internal/cli/users.go | 46 ++++++++++++++++++-------- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/internal/auth0/connection.go b/internal/auth0/connection.go index c0b553fbb..cecc24418 100644 --- a/internal/auth0/connection.go +++ b/internal/auth0/connection.go @@ -27,4 +27,7 @@ type ConnectionAPI interface { // List all connections. List(ctx context.Context, opts ...management.RequestOption) (ul *management.ConnectionList, err error) + + // ReadEnabledClients retrieves the enabled clients for a connection. + ReadEnabledClients(ctx context.Context, id string, opts ...management.RequestOption) (c *management.ConnectionEnabledClientList, err error) } diff --git a/internal/auth0/mock/connection_mock.go b/internal/auth0/mock/connection_mock.go index 0060253aa..c63d4fb83 100644 --- a/internal/auth0/mock/connection_mock.go +++ b/internal/auth0/mock/connection_mock.go @@ -133,6 +133,26 @@ func (mr *MockConnectionAPIMockRecorder) ReadByName(ctx, id interface{}, opts .. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadByName", reflect.TypeOf((*MockConnectionAPI)(nil).ReadByName), varargs...) } +// ReadEnabledClients mocks base method. +func (m *MockConnectionAPI) ReadEnabledClients(ctx context.Context, id string, opts ...management.RequestOption) (*management.ConnectionEnabledClientList, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, id} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ReadEnabledClients", varargs...) + ret0, _ := ret[0].(*management.ConnectionEnabledClientList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadEnabledClients indicates an expected call of ReadEnabledClients. +func (mr *MockConnectionAPIMockRecorder) ReadEnabledClients(ctx, id interface{}, opts ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, id}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadEnabledClients", reflect.TypeOf((*MockConnectionAPI)(nil).ReadEnabledClients), varargs...) +} + // Update mocks base method. func (m *MockConnectionAPI) Update(ctx context.Context, id string, c *management.Connection, opts ...management.RequestOption) error { m.ctrl.T.Helper() diff --git a/internal/cli/users.go b/internal/cli/users.go index 75ef6b4e9..c4d9217ab 100644 --- a/internal/cli/users.go +++ b/internal/cli/users.go @@ -404,10 +404,11 @@ func createUserCmd(cli *cli) *cobra.Command { return fmt.Errorf("failed to find connection with name %q: %w", inputs.connectionName, err) } - // A nil slice means enabled_clients was null in the API response, - // which means the connection is enabled for all applications. - // Only reject if explicitly set to an empty slice (disabled for all). - if enabledClients := connection.GetEnabledClients(); enabledClients != nil && len(enabledClients) == 0 { + hasClients, err := connectionHasEnabledClients(cmd.Context(), cli.api.Connection, connection.GetID()) + if err != nil { + return fmt.Errorf("failed to check enabled clients for connection %q: %w", inputs.connectionName, err) + } + if !hasClients { return fmt.Errorf( "failed to continue due to the connection with name %q being disabled, enable an application on this connection and try again", inputs.connectionName, @@ -897,18 +898,19 @@ The file size limit for a bulk import is 500KB. You will need to start multiple return fmt.Errorf("failed to read connection with name %q: %w", inputs.ConnectionName, err) } - // A nil slice means enabled_clients was null in the API response, - // which means the connection is enabled for all applications. - // Only reject if explicitly set to an empty slice (disabled for all). - if enabledClients := connection.GetEnabledClients(); enabledClients != nil && len(enabledClients) == 0 { + inputs.ConnectionID = connection.GetID() + + hasClients, err := connectionHasEnabledClients(cmd.Context(), cli.api.Connection, inputs.ConnectionID) + if err != nil { + return fmt.Errorf("failed to check enabled clients for connection %q: %w", inputs.ConnectionName, err) + } + if !hasClients { return fmt.Errorf( "failed to continue due to the connection with name %q being disabled, enable an application on this connection and try again", inputs.ConnectionName, ) } - inputs.ConnectionID = connection.GetID() - pipedUsersBody := iostream.PipedInput() if len(pipedUsersBody) > 0 && inputs.UsersBody == "" { inputs.UsersBody = string(pipedUsersBody) @@ -1002,10 +1004,11 @@ func (c *cli) databaseAndPasswordlessConnectionOptions(ctx context.Context) ([]s var connectionNames []string for _, connection := range connectionList.Connections { - // A nil slice means enabled_clients was null in the API response, - // which means the connection is enabled for all applications. - // Only skip if explicitly set to an empty slice (disabled for all). - if enabledClients := connection.GetEnabledClients(); enabledClients != nil && len(enabledClients) == 0 { + hasClients, err := connectionHasEnabledClients(ctx, c.api.Connection, connection.GetID()) + if err != nil { + continue + } + if !hasClients { continue } @@ -1019,6 +1022,21 @@ func (c *cli) databaseAndPasswordlessConnectionOptions(ctx context.Context) ([]s return connectionNames, nil } +// connectionHasEnabledClients checks if a connection has any enabled clients +// using the dedicated endpoint (replaces deprecated enabled_clients field). +func connectionHasEnabledClients(ctx context.Context, api auth0.ConnectionAPI, connectionID string) (bool, error) { + clients, err := api.ReadEnabledClients(ctx, connectionID) + if err != nil { + return false, err + } + + if clients.Clients == nil { + return true, nil + } + + return len(*clients.Clients) > 0, nil +} + func (c *cli) getUserConnection(users *management.User) []string { var res []string for _, i := range users.Identities { From ee2dd8eff3f61f8df4094e66a77469ac8af7883a Mon Sep 17 00:00:00 2001 From: "David E. Lovas" Date: Tue, 2 Jun 2026 19:05:53 -0400 Subject: [PATCH 3/5] test: update mock expectations for ReadEnabledClients --- internal/cli/users_test.go | 81 ++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/internal/cli/users_test.go b/internal/cli/users_test.go index c66f21709..c820563d0 100644 --- a/internal/cli/users_test.go +++ b/internal/cli/users_test.go @@ -16,35 +16,26 @@ import ( func TestConnectionsPickerOptions(t *testing.T) { tests := []struct { - name string - connections []*management.Connection - apiError error - assertOutput func(t testing.TB, options []string) - assertError func(t testing.TB, err error) + name string + connections []*management.Connection + enabledClients map[string]*management.ConnectionEnabledClientList // keyed by connection ID + apiError error + assertOutput func(t testing.TB, options []string) + assertError func(t testing.TB, err error) }{ { name: "happy path", connections: []*management.Connection{ - { - Name: auth0.String("some-name-1"), - Strategy: auth0.String("auth0"), - EnabledClients: &[]string{"1"}, - }, - { - Name: auth0.String("some-name-2"), - Strategy: auth0.String("auth0"), - EnabledClients: &[]string{"1"}, - }, - { - Name: auth0.String("some-name-3"), - Strategy: auth0.String("sms"), - EnabledClients: &[]string{"1"}, - }, - { - Name: auth0.String("some-name-4"), - Strategy: auth0.String("email"), - EnabledClients: &[]string{"1"}, - }, + {ID: auth0.String("conn-1"), Name: auth0.String("some-name-1"), Strategy: auth0.String("auth0")}, + {ID: auth0.String("conn-2"), Name: auth0.String("some-name-2"), Strategy: auth0.String("auth0")}, + {ID: auth0.String("conn-3"), Name: auth0.String("some-name-3"), Strategy: auth0.String("sms")}, + {ID: auth0.String("conn-4"), Name: auth0.String("some-name-4"), Strategy: auth0.String("email")}, + }, + enabledClients: map[string]*management.ConnectionEnabledClientList{ + "conn-1": {Clients: &[]management.ConnectionEnabledClient{{ClientID: auth0.String("app-1")}}}, + "conn-2": {Clients: &[]management.ConnectionEnabledClient{{ClientID: auth0.String("app-1")}}}, + "conn-3": {Clients: &[]management.ConnectionEnabledClient{{ClientID: auth0.String("app-1")}}}, + "conn-4": {Clients: &[]management.ConnectionEnabledClient{{ClientID: auth0.String("app-1")}}}, }, assertOutput: func(t testing.TB, options []string) { assert.Len(t, options, 4) @@ -60,24 +51,16 @@ func TestConnectionsPickerOptions(t *testing.T) { { name: "happy path: returning only active connections", connections: []*management.Connection{ - { - Name: auth0.String("some-name-1"), - Strategy: auth0.String("auth0"), - EnabledClients: &[]string{"1"}, - }, - { - Name: auth0.String("some-name-2"), - Strategy: auth0.String("auth0"), - EnabledClients: &[]string{"1"}, - }, - { - Name: auth0.String("some-name-3"), - Strategy: auth0.String("sms"), - }, - { - Name: auth0.String("some-name-4"), - Strategy: auth0.String("email"), - }, + {ID: auth0.String("conn-1"), Name: auth0.String("some-name-1"), Strategy: auth0.String("auth0")}, + {ID: auth0.String("conn-2"), Name: auth0.String("some-name-2"), Strategy: auth0.String("auth0")}, + {ID: auth0.String("conn-3"), Name: auth0.String("some-name-3"), Strategy: auth0.String("sms")}, + {ID: auth0.String("conn-4"), Name: auth0.String("some-name-4"), Strategy: auth0.String("email")}, + }, + enabledClients: map[string]*management.ConnectionEnabledClientList{ + "conn-1": {Clients: &[]management.ConnectionEnabledClient{{ClientID: auth0.String("app-1")}}}, + "conn-2": {Clients: &[]management.ConnectionEnabledClient{{ClientID: auth0.String("app-1")}}}, + "conn-3": {Clients: &[]management.ConnectionEnabledClient{}}, + "conn-4": {Clients: &[]management.ConnectionEnabledClient{}}, }, assertOutput: func(t testing.TB, options []string) { assert.Len(t, options, 2) @@ -127,6 +110,18 @@ func TestConnectionsPickerOptions(t *testing.T) { test.apiError, ) + // Set up ReadEnabledClients expectations for each connection + if test.enabledClients != nil { + for _, conn := range test.connections { + id := conn.GetID() + if clients, ok := test.enabledClients[id]; ok { + connectionAPI.EXPECT(). + ReadEnabledClients(ctx, id). + Return(clients, nil) + } + } + } + cli := &cli{ api: &auth0.API{ Connection: connectionAPI, From 8a085723b531c3c3ebb3439d21842ae564d1fac3 Mon Sep 17 00:00:00 2001 From: "David E. Lovas" Date: Fri, 5 Jun 2026 16:07:25 -0400 Subject: [PATCH 4/5] Simplify: remove nil check, treat nil and empty the same as zero enabled clients --- internal/cli/users.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/cli/users.go b/internal/cli/users.go index c4d9217ab..fb0955f95 100644 --- a/internal/cli/users.go +++ b/internal/cli/users.go @@ -1030,11 +1030,7 @@ func connectionHasEnabledClients(ctx context.Context, api auth0.ConnectionAPI, c return false, err } - if clients.Clients == nil { - return true, nil - } - - return len(*clients.Clients) > 0, nil + return clients.Clients != nil && len(*clients.Clients) > 0, nil } func (c *cli) getUserConnection(users *management.User) []string { From d1a85c872b8255a75a1440fdd3d9d5b383c77904 Mon Sep 17 00:00:00 2001 From: "David E. Lovas" Date: Sat, 6 Jun 2026 03:08:42 -0400 Subject: [PATCH 5/5] Fix godot lint: capitalize comment, add trailing period --- internal/cli/users_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cli/users_test.go b/internal/cli/users_test.go index c820563d0..c1fe74dea 100644 --- a/internal/cli/users_test.go +++ b/internal/cli/users_test.go @@ -18,7 +18,7 @@ func TestConnectionsPickerOptions(t *testing.T) { tests := []struct { name string connections []*management.Connection - enabledClients map[string]*management.ConnectionEnabledClientList // keyed by connection ID + enabledClients map[string]*management.ConnectionEnabledClientList // Keyed by connection ID. apiError error assertOutput func(t testing.TB, options []string) assertError func(t testing.TB, err error) @@ -110,7 +110,7 @@ func TestConnectionsPickerOptions(t *testing.T) { test.apiError, ) - // Set up ReadEnabledClients expectations for each connection + // Set up ReadEnabledClients expectations for each connection. if test.enabledClients != nil { for _, conn := range test.connections { id := conn.GetID()