diff --git a/backend/v3/domain/mock/human_user.mock.go b/backend/v3/domain/mock/human_user.mock.go index 40c6ac0bb..a29eef3d0 100644 --- a/backend/v3/domain/mock/human_user.mock.go +++ b/backend/v3/domain/mock/human_user.mock.go @@ -16,6 +16,7 @@ import ( domain "github.com/zitadel/zitadel/backend/v3/domain" database "github.com/zitadel/zitadel/backend/v3/storage/database" + crypto "github.com/zitadel/zitadel/internal/crypto" gomock "go.uber.org/mock/gomock" language "golang.org/x/text/language" ) @@ -82,48 +83,6 @@ func (c *MockHumanUserRepositoryAddIdentityProviderLinkCall) DoAndReturn(f func( return c } -// AddMetadata mocks base method. -func (m *MockHumanUserRepository) AddMetadata(metadata ...*domain.Metadata) database.Change { - m.ctrl.T.Helper() - varargs := []any{} - for _, a := range metadata { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "AddMetadata", varargs...) - ret0, _ := ret[0].(database.Change) - return ret0 -} - -// AddMetadata indicates an expected call of AddMetadata. -func (mr *MockHumanUserRepositoryMockRecorder) AddMetadata(metadata ...any) *MockHumanUserRepositoryAddMetadataCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetadata", reflect.TypeOf((*MockHumanUserRepository)(nil).AddMetadata), metadata...) - return &MockHumanUserRepositoryAddMetadataCall{Call: call} -} - -// MockHumanUserRepositoryAddMetadataCall wrap *gomock.Call -type MockHumanUserRepositoryAddMetadataCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockHumanUserRepositoryAddMetadataCall) Return(arg0 database.Change) *MockHumanUserRepositoryAddMetadataCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockHumanUserRepositoryAddMetadataCall) Do(f func(...*domain.Metadata) database.Change) *MockHumanUserRepositoryAddMetadataCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockHumanUserRepositoryAddMetadataCall) DoAndReturn(f func(...*domain.Metadata) database.Change) *MockHumanUserRepositoryAddMetadataCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // AddPasskey mocks base method. func (m *MockHumanUserRepository) AddPasskey(passkey *domain.Passkey) database.Change { m.ctrl.T.Helper() @@ -162,6 +121,44 @@ func (c *MockHumanUserRepositoryAddPasskeyCall) DoAndReturn(f func(*domain.Passk return c } +// AddRecoveryCodes mocks base method. +func (m *MockHumanUserRepository) AddRecoveryCodes(codes []string) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddRecoveryCodes", codes) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// AddRecoveryCodes indicates an expected call of AddRecoveryCodes. +func (mr *MockHumanUserRepositoryMockRecorder) AddRecoveryCodes(codes any) *MockHumanUserRepositoryAddRecoveryCodesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRecoveryCodes", reflect.TypeOf((*MockHumanUserRepository)(nil).AddRecoveryCodes), codes) + return &MockHumanUserRepositoryAddRecoveryCodesCall{Call: call} +} + +// MockHumanUserRepositoryAddRecoveryCodesCall wrap *gomock.Call +type MockHumanUserRepositoryAddRecoveryCodesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositoryAddRecoveryCodesCall) Return(arg0 database.Change) *MockHumanUserRepositoryAddRecoveryCodesCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositoryAddRecoveryCodesCall) Do(f func([]string) database.Change) *MockHumanUserRepositoryAddRecoveryCodesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositoryAddRecoveryCodesCall) DoAndReturn(f func([]string) database.Change) *MockHumanUserRepositoryAddRecoveryCodesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // CreatedAtColumn mocks base method. func (m *MockHumanUserRepository) CreatedAtColumn() database.Column { m.ctrl.T.Helper() @@ -960,6 +957,44 @@ func (c *MockHumanUserRepositoryIncrementPasswordFailedAttemptsCall) DoAndReturn return c } +// IncrementRecoveryCodeFailedAttempts mocks base method. +func (m *MockHumanUserRepository) IncrementRecoveryCodeFailedAttempts() database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IncrementRecoveryCodeFailedAttempts") + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// IncrementRecoveryCodeFailedAttempts indicates an expected call of IncrementRecoveryCodeFailedAttempts. +func (mr *MockHumanUserRepositoryMockRecorder) IncrementRecoveryCodeFailedAttempts() *MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementRecoveryCodeFailedAttempts", reflect.TypeOf((*MockHumanUserRepository)(nil).IncrementRecoveryCodeFailedAttempts)) + return &MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall{Call: call} +} + +// MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall wrap *gomock.Call +type MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall) Return(arg0 database.Change) *MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall) Do(f func() database.Change) *MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall) DoAndReturn(f func() database.Change) *MockHumanUserRepositoryIncrementRecoveryCodeFailedAttemptsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // IncrementSMSOTPFailedAttempts mocks base method. func (m *MockHumanUserRepository) IncrementSMSOTPFailedAttempts() database.Change { m.ctrl.T.Helper() @@ -998,6 +1033,44 @@ func (c *MockHumanUserRepositoryIncrementSMSOTPFailedAttemptsCall) DoAndReturn(f return c } +// IncrementTOTPFailedAttempts mocks base method. +func (m *MockHumanUserRepository) IncrementTOTPFailedAttempts() database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IncrementTOTPFailedAttempts") + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// IncrementTOTPFailedAttempts indicates an expected call of IncrementTOTPFailedAttempts. +func (mr *MockHumanUserRepositoryMockRecorder) IncrementTOTPFailedAttempts() *MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementTOTPFailedAttempts", reflect.TypeOf((*MockHumanUserRepository)(nil).IncrementTOTPFailedAttempts)) + return &MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall{Call: call} +} + +// MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall wrap *gomock.Call +type MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall) Return(arg0 database.Change) *MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall) Do(f func() database.Change) *MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall) DoAndReturn(f func() database.Change) *MockHumanUserRepositoryIncrementTOTPFailedAttemptsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // InstanceIDColumn mocks base method. func (m *MockHumanUserRepository) InstanceIDColumn() database.Column { m.ctrl.T.Helper() @@ -1530,6 +1603,44 @@ func (c *MockHumanUserRepositoryPrimaryKeyConditionCall) DoAndReturn(f func(stri return c } +// RemoveAllRecoveryCodes mocks base method. +func (m *MockHumanUserRepository) RemoveAllRecoveryCodes() database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveAllRecoveryCodes") + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// RemoveAllRecoveryCodes indicates an expected call of RemoveAllRecoveryCodes. +func (mr *MockHumanUserRepositoryMockRecorder) RemoveAllRecoveryCodes() *MockHumanUserRepositoryRemoveAllRecoveryCodesCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAllRecoveryCodes", reflect.TypeOf((*MockHumanUserRepository)(nil).RemoveAllRecoveryCodes)) + return &MockHumanUserRepositoryRemoveAllRecoveryCodesCall{Call: call} +} + +// MockHumanUserRepositoryRemoveAllRecoveryCodesCall wrap *gomock.Call +type MockHumanUserRepositoryRemoveAllRecoveryCodesCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositoryRemoveAllRecoveryCodesCall) Return(arg0 database.Change) *MockHumanUserRepositoryRemoveAllRecoveryCodesCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositoryRemoveAllRecoveryCodesCall) Do(f func() database.Change) *MockHumanUserRepositoryRemoveAllRecoveryCodesCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositoryRemoveAllRecoveryCodesCall) DoAndReturn(f func() database.Change) *MockHumanUserRepositoryRemoveAllRecoveryCodesCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // RemoveIdentityProviderLink mocks base method. func (m *MockHumanUserRepository) RemoveIdentityProviderLink(providerID, providedUserID string) database.Change { m.ctrl.T.Helper() @@ -1682,6 +1793,44 @@ func (c *MockHumanUserRepositoryRemovePhoneCall) DoAndReturn(f func() database.C return c } +// RemoveRecoveryCode mocks base method. +func (m *MockHumanUserRepository) RemoveRecoveryCode(code string) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveRecoveryCode", code) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// RemoveRecoveryCode indicates an expected call of RemoveRecoveryCode. +func (mr *MockHumanUserRepositoryMockRecorder) RemoveRecoveryCode(code any) *MockHumanUserRepositoryRemoveRecoveryCodeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRecoveryCode", reflect.TypeOf((*MockHumanUserRepository)(nil).RemoveRecoveryCode), code) + return &MockHumanUserRepositoryRemoveRecoveryCodeCall{Call: call} +} + +// MockHumanUserRepositoryRemoveRecoveryCodeCall wrap *gomock.Call +type MockHumanUserRepositoryRemoveRecoveryCodeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositoryRemoveRecoveryCodeCall) Return(arg0 database.Change) *MockHumanUserRepositoryRemoveRecoveryCodeCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositoryRemoveRecoveryCodeCall) Do(f func(string) database.Change) *MockHumanUserRepositoryRemoveRecoveryCodeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositoryRemoveRecoveryCodeCall) DoAndReturn(f func(string) database.Change) *MockHumanUserRepositoryRemoveRecoveryCodeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // RemoveTOTP mocks base method. func (m *MockHumanUserRepository) RemoveTOTP() database.Change { m.ctrl.T.Helper() @@ -1796,6 +1945,44 @@ func (c *MockHumanUserRepositoryResetPasswordFailedAttemptsCall) DoAndReturn(f f return c } +// ResetRecoveryCodeFailedAttempts mocks base method. +func (m *MockHumanUserRepository) ResetRecoveryCodeFailedAttempts() database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResetRecoveryCodeFailedAttempts") + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// ResetRecoveryCodeFailedAttempts indicates an expected call of ResetRecoveryCodeFailedAttempts. +func (mr *MockHumanUserRepositoryMockRecorder) ResetRecoveryCodeFailedAttempts() *MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetRecoveryCodeFailedAttempts", reflect.TypeOf((*MockHumanUserRepository)(nil).ResetRecoveryCodeFailedAttempts)) + return &MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall{Call: call} +} + +// MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall wrap *gomock.Call +type MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall) Return(arg0 database.Change) *MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall) Do(f func() database.Change) *MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall) DoAndReturn(f func() database.Change) *MockHumanUserRepositoryResetRecoveryCodeFailedAttemptsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // ResetSMSOTPFailedAttempts mocks base method. func (m *MockHumanUserRepository) ResetSMSOTPFailedAttempts() database.Change { m.ctrl.T.Helper() @@ -1834,6 +2021,44 @@ func (c *MockHumanUserRepositoryResetSMSOTPFailedAttemptsCall) DoAndReturn(f fun return c } +// ResetTOTPFailedAttempts mocks base method. +func (m *MockHumanUserRepository) ResetTOTPFailedAttempts() database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ResetTOTPFailedAttempts") + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// ResetTOTPFailedAttempts indicates an expected call of ResetTOTPFailedAttempts. +func (mr *MockHumanUserRepositoryMockRecorder) ResetTOTPFailedAttempts() *MockHumanUserRepositoryResetTOTPFailedAttemptsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetTOTPFailedAttempts", reflect.TypeOf((*MockHumanUserRepository)(nil).ResetTOTPFailedAttempts)) + return &MockHumanUserRepositoryResetTOTPFailedAttemptsCall{Call: call} +} + +// MockHumanUserRepositoryResetTOTPFailedAttemptsCall wrap *gomock.Call +type MockHumanUserRepositoryResetTOTPFailedAttemptsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositoryResetTOTPFailedAttemptsCall) Return(arg0 database.Change) *MockHumanUserRepositoryResetTOTPFailedAttemptsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositoryResetTOTPFailedAttemptsCall) Do(f func() database.Change) *MockHumanUserRepositoryResetTOTPFailedAttemptsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositoryResetTOTPFailedAttemptsCall) DoAndReturn(f func() database.Change) *MockHumanUserRepositoryResetTOTPFailedAttemptsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SetAvatarKey mocks base method. func (m *MockHumanUserRepository) SetAvatarKey(avatarKey *string) database.Change { m.ctrl.T.Helper() @@ -2138,6 +2363,44 @@ func (c *MockHumanUserRepositorySetIdentityProviderLinkUsernameCall) DoAndReturn return c } +// SetInviteVerification mocks base method. +func (m *MockHumanUserRepository) SetInviteVerification(verification domain.VerificationType) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetInviteVerification", verification) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// SetInviteVerification indicates an expected call of SetInviteVerification. +func (mr *MockHumanUserRepositoryMockRecorder) SetInviteVerification(verification any) *MockHumanUserRepositorySetInviteVerificationCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetInviteVerification", reflect.TypeOf((*MockHumanUserRepository)(nil).SetInviteVerification), verification) + return &MockHumanUserRepositorySetInviteVerificationCall{Call: call} +} + +// MockHumanUserRepositorySetInviteVerificationCall wrap *gomock.Call +type MockHumanUserRepositorySetInviteVerificationCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositorySetInviteVerificationCall) Return(arg0 database.Change) *MockHumanUserRepositorySetInviteVerificationCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositorySetInviteVerificationCall) Do(f func(domain.VerificationType) database.Change) *MockHumanUserRepositorySetInviteVerificationCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositorySetInviteVerificationCall) DoAndReturn(f func(domain.VerificationType) database.Change) *MockHumanUserRepositorySetInviteVerificationCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SetLastName mocks base method. func (m *MockHumanUserRepository) SetLastName(lastName string) database.Change { m.ctrl.T.Helper() @@ -2252,6 +2515,44 @@ func (c *MockHumanUserRepositorySetLastSuccessfulPasswordCheckCall) DoAndReturn( return c } +// SetLastSuccessfulRecoveryCodeCheck mocks base method. +func (m *MockHumanUserRepository) SetLastSuccessfulRecoveryCodeCheck(checkedAt time.Time) database.Change { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetLastSuccessfulRecoveryCodeCheck", checkedAt) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// SetLastSuccessfulRecoveryCodeCheck indicates an expected call of SetLastSuccessfulRecoveryCodeCheck. +func (mr *MockHumanUserRepositoryMockRecorder) SetLastSuccessfulRecoveryCodeCheck(checkedAt any) *MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLastSuccessfulRecoveryCodeCheck", reflect.TypeOf((*MockHumanUserRepository)(nil).SetLastSuccessfulRecoveryCodeCheck), checkedAt) + return &MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall{Call: call} +} + +// MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall wrap *gomock.Call +type MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall) Return(arg0 database.Change) *MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall) Do(f func(time.Time) database.Change) *MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall) DoAndReturn(f func(time.Time) database.Change) *MockHumanUserRepositorySetLastSuccessfulRecoveryCodeCheckCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SetLastSuccessfulSMSOTPCheck mocks base method. func (m *MockHumanUserRepository) SetLastSuccessfulSMSOTPCheck(checkedAt time.Time) database.Change { m.ctrl.T.Helper() @@ -2328,6 +2629,48 @@ func (c *MockHumanUserRepositorySetLastSuccessfulTOTPCheckCall) DoAndReturn(f fu return c } +// SetMetadata mocks base method. +func (m *MockHumanUserRepository) SetMetadata(metadata ...*domain.Metadata) database.Change { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range metadata { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SetMetadata", varargs...) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// SetMetadata indicates an expected call of SetMetadata. +func (mr *MockHumanUserRepositoryMockRecorder) SetMetadata(metadata ...any) *MockHumanUserRepositorySetMetadataCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMetadata", reflect.TypeOf((*MockHumanUserRepository)(nil).SetMetadata), metadata...) + return &MockHumanUserRepositorySetMetadataCall{Call: call} +} + +// MockHumanUserRepositorySetMetadataCall wrap *gomock.Call +type MockHumanUserRepositorySetMetadataCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockHumanUserRepositorySetMetadataCall) Return(arg0 database.Change) *MockHumanUserRepositorySetMetadataCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockHumanUserRepositorySetMetadataCall) Do(f func(...*domain.Metadata) database.Change) *MockHumanUserRepositorySetMetadataCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockHumanUserRepositorySetMetadataCall) DoAndReturn(f func(...*domain.Metadata) database.Change) *MockHumanUserRepositorySetMetadataCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SetNickname mocks base method. func (m *MockHumanUserRepository) SetNickname(nickname string) database.Change { m.ctrl.T.Helper() @@ -2937,7 +3280,7 @@ func (c *MockHumanUserRepositorySetStateCall) DoAndReturn(f func(domain.UserStat } // SetTOTPSecret mocks base method. -func (m *MockHumanUserRepository) SetTOTPSecret(secret []byte) database.Change { +func (m *MockHumanUserRepository) SetTOTPSecret(secret *crypto.CryptoValue) database.Change { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetTOTPSecret", secret) ret0, _ := ret[0].(database.Change) @@ -2963,13 +3306,13 @@ func (c *MockHumanUserRepositorySetTOTPSecretCall) Return(arg0 database.Change) } // Do rewrite *gomock.Call.Do -func (c *MockHumanUserRepositorySetTOTPSecretCall) Do(f func([]byte) database.Change) *MockHumanUserRepositorySetTOTPSecretCall { +func (c *MockHumanUserRepositorySetTOTPSecretCall) Do(f func(*crypto.CryptoValue) database.Change) *MockHumanUserRepositorySetTOTPSecretCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockHumanUserRepositorySetTOTPSecretCall) DoAndReturn(f func([]byte) database.Change) *MockHumanUserRepositorySetTOTPSecretCall { +func (c *MockHumanUserRepositorySetTOTPSecretCall) DoAndReturn(f func(*crypto.CryptoValue) database.Change) *MockHumanUserRepositorySetTOTPSecretCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/backend/v3/domain/mock/machine_user.mock.go b/backend/v3/domain/mock/machine_user.mock.go index 2019b9776..95fae314e 100644 --- a/backend/v3/domain/mock/machine_user.mock.go +++ b/backend/v3/domain/mock/machine_user.mock.go @@ -81,48 +81,6 @@ func (c *MockMachineUserRepositoryAddKeyCall) DoAndReturn(f func(*domain.Machine return c } -// AddMetadata mocks base method. -func (m *MockMachineUserRepository) AddMetadata(metadata ...*domain.Metadata) database.Change { - m.ctrl.T.Helper() - varargs := []any{} - for _, a := range metadata { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "AddMetadata", varargs...) - ret0, _ := ret[0].(database.Change) - return ret0 -} - -// AddMetadata indicates an expected call of AddMetadata. -func (mr *MockMachineUserRepositoryMockRecorder) AddMetadata(metadata ...any) *MockMachineUserRepositoryAddMetadataCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetadata", reflect.TypeOf((*MockMachineUserRepository)(nil).AddMetadata), metadata...) - return &MockMachineUserRepositoryAddMetadataCall{Call: call} -} - -// MockMachineUserRepositoryAddMetadataCall wrap *gomock.Call -type MockMachineUserRepositoryAddMetadataCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockMachineUserRepositoryAddMetadataCall) Return(arg0 database.Change) *MockMachineUserRepositoryAddMetadataCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockMachineUserRepositoryAddMetadataCall) Do(f func(...*domain.Metadata) database.Change) *MockMachineUserRepositoryAddMetadataCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockMachineUserRepositoryAddMetadataCall) DoAndReturn(f func(...*domain.Metadata) database.Change) *MockMachineUserRepositoryAddMetadataCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // AddPersonalAccessToken mocks base method. func (m *MockMachineUserRepository) AddPersonalAccessToken(pat *domain.PersonalAccessToken) database.Change { m.ctrl.T.Helper() @@ -769,6 +727,48 @@ func (c *MockMachineUserRepositorySetDescriptionCall) DoAndReturn(f func(string) return c } +// SetMetadata mocks base method. +func (m *MockMachineUserRepository) SetMetadata(metadata ...*domain.Metadata) database.Change { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range metadata { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SetMetadata", varargs...) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// SetMetadata indicates an expected call of SetMetadata. +func (mr *MockMachineUserRepositoryMockRecorder) SetMetadata(metadata ...any) *MockMachineUserRepositorySetMetadataCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMetadata", reflect.TypeOf((*MockMachineUserRepository)(nil).SetMetadata), metadata...) + return &MockMachineUserRepositorySetMetadataCall{Call: call} +} + +// MockMachineUserRepositorySetMetadataCall wrap *gomock.Call +type MockMachineUserRepositorySetMetadataCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockMachineUserRepositorySetMetadataCall) Return(arg0 database.Change) *MockMachineUserRepositorySetMetadataCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockMachineUserRepositorySetMetadataCall) Do(f func(...*domain.Metadata) database.Change) *MockMachineUserRepositorySetMetadataCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockMachineUserRepositorySetMetadataCall) DoAndReturn(f func(...*domain.Metadata) database.Change) *MockMachineUserRepositorySetMetadataCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SetName mocks base method. func (m *MockMachineUserRepository) SetName(name string) database.Change { m.ctrl.T.Helper() diff --git a/backend/v3/domain/mock/user.mock.go b/backend/v3/domain/mock/user.mock.go index db420d532..cc114eb02 100644 --- a/backend/v3/domain/mock/user.mock.go +++ b/backend/v3/domain/mock/user.mock.go @@ -43,48 +43,6 @@ func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder { return m.recorder } -// AddMetadata mocks base method. -func (m *MockUserRepository) AddMetadata(metadata ...*domain.Metadata) database.Change { - m.ctrl.T.Helper() - varargs := []any{} - for _, a := range metadata { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "AddMetadata", varargs...) - ret0, _ := ret[0].(database.Change) - return ret0 -} - -// AddMetadata indicates an expected call of AddMetadata. -func (mr *MockUserRepositoryMockRecorder) AddMetadata(metadata ...any) *MockUserRepositoryAddMetadataCall { - mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetadata", reflect.TypeOf((*MockUserRepository)(nil).AddMetadata), metadata...) - return &MockUserRepositoryAddMetadataCall{Call: call} -} - -// MockUserRepositoryAddMetadataCall wrap *gomock.Call -type MockUserRepositoryAddMetadataCall struct { - *gomock.Call -} - -// Return rewrite *gomock.Call.Return -func (c *MockUserRepositoryAddMetadataCall) Return(arg0 database.Change) *MockUserRepositoryAddMetadataCall { - c.Call = c.Call.Return(arg0) - return c -} - -// Do rewrite *gomock.Call.Do -func (c *MockUserRepositoryAddMetadataCall) Do(f func(...*domain.Metadata) database.Change) *MockUserRepositoryAddMetadataCall { - c.Call = c.Call.Do(f) - return c -} - -// DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockUserRepositoryAddMetadataCall) DoAndReturn(f func(...*domain.Metadata) database.Change) *MockUserRepositoryAddMetadataCall { - c.Call = c.Call.DoAndReturn(f) - return c -} - // Create mocks base method. func (m *MockUserRepository) Create(ctx context.Context, client database.QueryExecutor, user *domain.User) error { m.ctrl.T.Helper() @@ -782,6 +740,48 @@ func (c *MockUserRepositoryRemoveMetadataCall) DoAndReturn(f func(database.Condi return c } +// SetMetadata mocks base method. +func (m *MockUserRepository) SetMetadata(metadata ...*domain.Metadata) database.Change { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range metadata { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SetMetadata", varargs...) + ret0, _ := ret[0].(database.Change) + return ret0 +} + +// SetMetadata indicates an expected call of SetMetadata. +func (mr *MockUserRepositoryMockRecorder) SetMetadata(metadata ...any) *MockUserRepositorySetMetadataCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMetadata", reflect.TypeOf((*MockUserRepository)(nil).SetMetadata), metadata...) + return &MockUserRepositorySetMetadataCall{Call: call} +} + +// MockUserRepositorySetMetadataCall wrap *gomock.Call +type MockUserRepositorySetMetadataCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockUserRepositorySetMetadataCall) Return(arg0 database.Change) *MockUserRepositorySetMetadataCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockUserRepositorySetMetadataCall) Do(f func(...*domain.Metadata) database.Change) *MockUserRepositorySetMetadataCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockUserRepositorySetMetadataCall) DoAndReturn(f func(...*domain.Metadata) database.Change) *MockUserRepositorySetMetadataCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + // SetState mocks base method. func (m *MockUserRepository) SetState(state domain.UserState) database.Change { m.ctrl.T.Helper() diff --git a/backend/v3/domain/user.go b/backend/v3/domain/user.go index e24ddc344..d74776088 100644 --- a/backend/v3/domain/user.go +++ b/backend/v3/domain/user.go @@ -9,19 +9,25 @@ import ( ) type User struct { - InstanceID string `json:"instanceId,omitempty" db:"instance_id"` - OrganizationID string `json:"organizationId,omitempty" db:"organization_id"` - ID string `json:"id,omitempty" db:"id"` - Username string `json:"username,omitempty" db:"username"` - State UserState `json:"state,omitempty" db:"state"` - CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"` - UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"` + InstanceID string `json:"instanceId,omitempty" db:"instance_id"` + OrganizationID string `json:"organizationId,omitempty" db:"organization_id"` + ID string `json:"id,omitempty" db:"id"` + Username string `json:"username,omitempty" db:"username"` + LoginNames []LoginName `json:"loginNames,omitempty" db:"login_names"` + State UserState `json:"state,omitempty" db:"state"` + CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"` + UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"` Machine *MachineUser `json:"machine,omitempty" db:"-"` Human *HumanUser `json:"human,omitempty" db:"-"` Metadata []*Metadata `json:"metadata,omitempty" db:"metadata"` } +type LoginName struct { + LoginName string `json:"loginName,omitempty" db:"-"` + IsPreferred bool `json:"isPreferred,omitempty" db:"-"` +} + type MachineUser struct { Name string `json:"name,omitempty" db:"-"` Description string `json:"description,omitempty" db:"-"` diff --git a/backend/v3/storage/database/dialect/postgres/migration/015_login_names.go b/backend/v3/storage/database/dialect/postgres/migration/015_login_names.go new file mode 100644 index 000000000..0b797afb2 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/015_login_names.go @@ -0,0 +1,16 @@ +package migration + +import ( + _ "embed" +) + +var ( + //go:embed 015_login_names/up.sql + up015LoginNames string + //go:embed 015_login_names/down.sql + down015LoginNames string +) + +func init() { + registerSQLMigration(15, up015LoginNames, down015LoginNames) +} diff --git a/backend/v3/storage/database/dialect/postgres/migration/015_login_names/down.sql b/backend/v3/storage/database/dialect/postgres/migration/015_login_names/down.sql new file mode 100644 index 000000000..f0c167ced --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/015_login_names/down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS zitadel.login_names CASCADE; +DROP FUNCTION IF EXISTS zitadel.apply_domain_policy_manipulation_to_login_names(); +DROP FUNCTION IF EXISTS zitadel.apply_user_insert_to_login_names(); +DROP FUNCTION IF EXISTS zitadel.apply_user_update_to_login_names(); +DROP FUNCTION IF EXISTS zitadel.apply_domain_manipulation_to_login_names(); \ No newline at end of file diff --git a/backend/v3/storage/database/dialect/postgres/migration/015_login_names/up.sql b/backend/v3/storage/database/dialect/postgres/migration/015_login_names/up.sql new file mode 100644 index 000000000..f2dd82776 --- /dev/null +++ b/backend/v3/storage/database/dialect/postgres/migration/015_login_names/up.sql @@ -0,0 +1,300 @@ +CREATE TABLE zitadel.login_names( + instance_id TEXT NOT NULL + , organization_id TEXT -- is set if the setting for loginNameIncludesDomain is set on organization level, otherwise null + , user_id TEXT NOT NULL + + , username TEXT NOT NULL CHECK (username <> '') + , domain TEXT + , login_name TEXT GENERATED ALWAYS AS (CASE WHEN domain IS NOT NULL THEN username || '@' || domain ELSE username END) STORED + , is_preferred BOOLEAN NOT NULL DEFAULT FALSE + + , used_setting TEXT NOT NULL + + , PRIMARY KEY (instance_id, user_id, login_name) + , FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users(instance_id, id) ON DELETE CASCADE + , FOREIGN KEY (instance_id, organization_id, domain) REFERENCES zitadel.org_domains(instance_id, org_id, domain) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX idx_login_names_instance_login_name ON zitadel.login_names(instance_id, lower(login_name)); +CREATE INDEX idx_login_names_instance_user ON zitadel.login_names(instance_id, user_id); +CREATE INDEX idx_login_names_setting ON zitadel.login_names(instance_id, used_setting); +CREATE INDEX idx_login_names_domain ON zitadel.login_names(instance_id, domain); -- used for cleanup of login names when a domain is deleted + +CREATE OR REPLACE FUNCTION zitadel.apply_domain_manipulation_to_login_names() RETURNS TRIGGER AS $$ +DECLARE + setting zitadel.settings%ROWTYPE; +BEGIN + IF (NOT NEW.is_verified) THEN + IF TG_OP = 'UPDATE' AND OLD.is_verified THEN + -- this case can currently not happen but is added for completeness and future-proofing. + RAISE NOTICE 'Domain changed from verified to unverified, removing associated login names'; + DELETE FROM zitadel.login_names + WHERE + login_names.instance_id = NEW.instance_id + AND login_names.organization_id = NEW.org_id + AND login_names.domain = NEW.domain; + ELSE + RAISE NOTICE 'Domain is not verified, skipping login name manipulation'; + END IF; + RETURN NULL; + END IF; + + SELECT settings.* + INTO setting + FROM zitadel.settings + WHERE + settings.instance_id = NEW.instance_id + AND settings.type = 'domain' + AND (settings.organization_id IS NULL OR settings.organization_id = NEW.org_id) + ORDER BY settings.organization_id NULLS LAST + LIMIT 1; + + IF NOT (setting.settings->'loginNameIncludesDomain')::BOOLEAN THEN + RAISE NOTICE 'skip adding domain because loginNameIncludesDomain is false for setting'; + RETURN NULL; + END IF; + + -- Lock based on the setting scope (organization-specific or instance-level/global). + -- The lock is released at transaction end. + PERFORM pg_advisory_xact_lock( + hashtext('zitadel.login_names') + , hashtext(setting.instance_id || ':' || COALESCE(setting.organization_id, 'global')) + ); + + IF NEW.is_primary IS DISTINCT FROM OLD.is_primary THEN + RAISE NOTICE 'primary domain changed, updating preferred login name'; + UPDATE zitadel.login_names + SET is_preferred = NEW.is_primary + WHERE + login_names.instance_id = NEW.instance_id + AND login_names.domain = NEW.domain; + END IF; + + IF NEW.domain IS DISTINCT FROM OLD.domain THEN + RAISE NOTICE 'domain changed, updating login names'; + UPDATE zitadel.login_names + SET domain = NEW.domain + WHERE + login_names.instance_id = NEW.instance_id + AND login_names.organization_id = NEW.org_id + AND login_names.domain = OLD.domain; + END IF; + + IF NEW.is_verified IS DISTINCT FROM OLD.is_verified THEN + RAISE NOTICE 'Domain verification changed, inserting login names for verified domain'; + INSERT INTO zitadel.login_names(instance_id, organization_id, user_id, username, domain, is_preferred, used_setting) + SELECT + NEW.instance_id + , NEW.org_id + , users.id + , users.username + , NEW.domain + , NEW.is_primary + , setting.id + FROM + zitadel.users + WHERE + users.instance_id = NEW.instance_id + AND users.organization_id = NEW.org_id; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER trg_apply_domain_manipulation_to_login_names +AFTER INSERT OR UPDATE ON zitadel.org_domains +FOR EACH ROW +EXECUTE FUNCTION zitadel.apply_domain_manipulation_to_login_names(); + +CREATE OR REPLACE FUNCTION zitadel.apply_user_update_to_login_names() RETURNS TRIGGER AS $$ +BEGIN + -- Lock global scope and organization scope to serialize with instance-level + -- and org-level setting/domain manipulations. + -- The lock is released at transaction end. + PERFORM pg_advisory_xact_lock(hashtext('zitadel.login_names'), hashtext(NEW.instance_id || ':global')); + PERFORM pg_advisory_xact_lock(hashtext('zitadel.login_names'), hashtext(NEW.instance_id || ':' || NEW.organization_id)); + + UPDATE + zitadel.login_names + SET + username = NEW.username + WHERE + login_names.instance_id = NEW.instance_id + AND login_names.user_id = NEW.id; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER trg_apply_user_update_to_login_names +AFTER UPDATE ON zitadel.users +FOR EACH ROW +WHEN (NEW.username IS DISTINCT FROM OLD.username) +EXECUTE FUNCTION zitadel.apply_user_update_to_login_names(); + +CREATE OR REPLACE FUNCTION zitadel.apply_user_insert_to_login_names() RETURNS TRIGGER AS $$ +DECLARE + setting zitadel.settings%ROWTYPE; +BEGIN + SELECT settings.* + INTO setting + FROM + zitadel.settings + WHERE + settings.instance_id = NEW.instance_id + AND settings.type = 'domain' + AND ( + settings.organization_id IS NULL + OR settings.organization_id = NEW.organization_id + ) + ORDER BY settings.organization_id NULLS LAST + LIMIT 1; + + -- Lock based on the setting scope (organization-specific or instance-level/global). + -- The lock is released at transaction end. + PERFORM pg_advisory_xact_lock( + hashtext('zitadel.login_names') + , hashtext(setting.instance_id || ':' || COALESCE(setting.organization_id, 'global')) + ); + + IF NOT (setting.settings->'loginNameIncludesDomain')::BOOLEAN THEN + RAISE NOTICE 'inserting username as login name'; + INSERT INTO zitadel.login_names(instance_id, organization_id, user_id, username, is_preferred, used_setting) + VALUES (NEW.instance_id, NEW.organization_id, NEW.id, NEW.username, TRUE, setting.id); + + RETURN NULL; + END IF; + + RAISE NOTICE 'inserting login names'; + INSERT INTO zitadel.login_names(instance_id, organization_id, user_id, username, domain, is_preferred, used_setting) + SELECT + NEW.instance_id + , NEW.organization_id + , NEW.id + , NEW.username + , org_domains.domain + , org_domains.is_primary IS NULL OR org_domains.is_primary + , setting.id + FROM + zitadel.org_domains + WHERE + org_domains.instance_id = NEW.instance_id + AND org_domains.org_id = NEW.organization_id + AND org_domains.is_verified = TRUE; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER trg_apply_user_insert_to_login_names +AFTER INSERT ON zitadel.users +FOR EACH ROW +EXECUTE FUNCTION zitadel.apply_user_insert_to_login_names(); + +CREATE OR REPLACE FUNCTION zitadel.apply_domain_policy_manipulation_to_login_names() RETURNS TRIGGER AS $$ +DECLARE + old_lock_key INT4; + new_lock_key INT4; +BEGIN + CASE TG_OP + -- insert and delete can only happen on organization level therefore we can load the instance level setting as OLD or NEW. + WHEN 'DELETE' THEN + SELECT + settings.* + INTO + NEW + FROM + zitadel.settings + WHERE + settings.instance_id = OLD.instance_id + AND settings.type = 'domain' + AND settings.organization_id IS NULL; + WHEN 'INSERT' THEN + SELECT + settings.* + INTO + OLD + FROM + zitadel.settings + WHERE + settings.instance_id = NEW.instance_id + AND settings.type = 'domain' + AND settings.organization_id IS NULL; + ELSE + -- OLD and NEW are already populated, do nothing + END CASE; + + -- Lock both OLD and NEW setting scopes to serialize transitions across settings. + -- Acquire in deterministic order to avoid deadlocks. + old_lock_key := hashtext(OLD.instance_id || ':' || COALESCE(OLD.organization_id, 'global')); + new_lock_key := hashtext(NEW.instance_id || ':' || COALESCE(NEW.organization_id, 'global')); + IF old_lock_key = new_lock_key THEN + PERFORM pg_advisory_xact_lock(hashtext('zitadel.login_names'), old_lock_key); + ELSE + PERFORM pg_advisory_xact_lock(hashtext('zitadel.login_names'), new_lock_key); + PERFORM pg_advisory_xact_lock(hashtext('zitadel.login_names'), old_lock_key); + END IF; + + IF (OLD.settings->'loginNameIncludesDomain')::BOOLEAN IS NOT DISTINCT FROM (NEW.settings->'loginNameIncludesDomain')::BOOLEAN THEN + -- field not changed but the setting id did change. + IF TG_OP = 'DELETE' OR TG_OP = 'INSERT' THEN + UPDATE + zitadel.login_names + SET + used_setting = NEW.id + WHERE + login_names.instance_id = OLD.instance_id + AND login_names.used_setting = OLD.id + AND (NEW.organization_id IS NULL OR login_names.organization_id = NEW.organization_id); + END IF; + + -- no change in the setting, so we can skip the update + RETURN NULL; + END IF; + + RAISE NOTICE 'recompute login names'; + + WITH affected_login_names AS ( + DELETE FROM zitadel.login_names + WHERE + login_names.instance_id = NEW.instance_id + AND login_names.used_setting = OLD.id + AND (NEW.organization_id IS NULL OR login_names.organization_id = NEW.organization_id) + RETURNING instance_id, organization_id, user_id + ) + INSERT INTO zitadel.login_names(instance_id, organization_id, user_id, username, domain, is_preferred, used_setting) + SELECT DISTINCT ON (affected_login_names.instance_id, affected_login_names.user_id, users.username, org_domains.domain) + affected_login_names.instance_id + , affected_login_names.organization_id + , affected_login_names.user_id + , users.username + , org_domains.domain + , org_domains.is_primary IS NULL OR org_domains.is_primary + , NEW.id + FROM + affected_login_names + JOIN zitadel.users ON + users.instance_id = affected_login_names.instance_id + AND users.id = affected_login_names.user_id + LEFT JOIN zitadel.org_domains ON + org_domains.instance_id = affected_login_names.instance_id + AND org_domains.org_id = affected_login_names.organization_id + AND org_domains.is_verified + AND (NEW.settings->'loginNameIncludesDomain')::BOOLEAN; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE TRIGGER trg_apply_domain_policy_manipulation_to_login_names +AFTER INSERT OR UPDATE ON zitadel.settings +FOR EACH ROW +WHEN (NEW.type = 'domain') +EXECUTE FUNCTION zitadel.apply_domain_policy_manipulation_to_login_names(); + +CREATE OR REPLACE TRIGGER trg_apply_domain_policy_removed_to_login_names +AFTER DELETE ON zitadel.settings +FOR EACH ROW +WHEN (OLD.type = 'domain') +EXECUTE FUNCTION zitadel.apply_domain_policy_manipulation_to_login_names(); \ No newline at end of file diff --git a/backend/v3/storage/database/repository/repository_test.go b/backend/v3/storage/database/repository/repository_test.go index 9a093a5e5..68c0ab8ad 100644 --- a/backend/v3/storage/database/repository/repository_test.go +++ b/backend/v3/storage/database/repository/repository_test.go @@ -186,7 +186,7 @@ func createProjectGrant(t *testing.T, tx database.Transaction, instanceID, grant return projectGrant.ID } -func createIdentityProvider(t *testing.T, tx database.Transaction, instanceID, orgID string) string { +func createIdentityProvider(t *testing.T, tx database.QueryExecutor, instanceID, orgID string) string { t.Helper() idp := domain.IdentityProvider{ InstanceID: instanceID, diff --git a/backend/v3/storage/database/repository/user.go b/backend/v3/storage/database/repository/user.go index d3ed7c382..7c8b415cc 100644 --- a/backend/v3/storage/database/repository/user.go +++ b/backend/v3/storage/database/repository/user.go @@ -30,6 +30,7 @@ type user struct { userMetadata userPasskey userIdentityProviderLink + userLoginName } func (u user) HumanRepository() domain.HumanUserRepository { @@ -71,6 +72,8 @@ var queryUserStmt = "SELECT users.instance_id, users.organization_id, users.id, ", users.state, users.created_at, users.updated_at" + // metadata `, jsonb_agg(DISTINCT jsonb_build_object('instanceId', user_metadata.instance_id, 'key', user_metadata.key, 'value', encode(user_metadata.value, 'base64'), 'createdAt', user_metadata.created_at, 'updatedAt', user_metadata.updated_at)) FILTER (WHERE user_metadata.user_id IS NOT NULL) AS metadata` + + // login names + `, jsonb_agg(DISTINCT jsonb_build_object('loginName', login_names.login_name, 'isPreferred', login_names.is_preferred)) FILTER (WHERE login_names.user_id IS NOT NULL) AS login_names` + // machine `, CASE WHEN users.type = 'machine' THEN jsonb_build_object('name', users.name` + `, 'description', users.description, 'secret', users.secret` + @@ -147,6 +150,7 @@ func (u user) appendQueryOpts(opts []database.QueryOption) []database.QueryOptio u.joinVerifications(), u.joinPasskeys(), u.joinIdentityProviderLinks(), + u.joinLoginNames(), database.WithGroupBy(u.PrimaryKeyColumns()...), ) } @@ -228,11 +232,6 @@ func (u user) InstanceIDCondition(instanceID string) database.Condition { return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID) } -// LoginNameCondition implements [domain.UserRepository]. -func (u user) LoginNameCondition(op database.TextOperation, loginName string) database.Condition { - panic("unimplemented") -} - // OrganizationIDCondition implements [domain.UserRepository]. func (u user) OrganizationIDCondition(orgID string) database.Condition { return database.NewTextCondition(u.organizationIDColumn(), database.TextOperationEqual, orgID) @@ -478,3 +477,13 @@ func (u user) joinIdentityProviderLinks() database.QueryOption { ), ) } + +func (u user) joinLoginNames() database.QueryOption { + return database.WithLeftJoin( + u.userLoginName.qualifiedTableName(), + database.And( + database.NewColumnCondition(u.InstanceIDColumn(), u.userLoginName.instanceIDColumn()), + database.NewColumnCondition(u.IDColumn(), u.userLoginName.userIDColumn()), + ), + ) +} diff --git a/backend/v3/storage/database/repository/user_login_name.go b/backend/v3/storage/database/repository/user_login_name.go new file mode 100644 index 000000000..6ea151eff --- /dev/null +++ b/backend/v3/storage/database/repository/user_login_name.go @@ -0,0 +1,38 @@ +package repository + +import "github.com/zitadel/zitadel/backend/v3/storage/database" + +type userLoginName struct{} + +func (u userLoginName) qualifiedTableName() string { + return "zitadel.login_names" +} + +func (u userLoginName) unqualifiedTableName() string { + return "login_names" +} + +// ------------------------------------------------------------- +// conditions +// ------------------------------------------------------------- + +// LoginNameCondition implements [domain.UserRepository]. +func (u userLoginName) LoginNameCondition(op database.TextOperation, loginName string) database.Condition { + return database.NewTextCondition(u.loginNameColumn(), op, loginName) +} + +// ------------------------------------------------------------- +// columns +// ------------------------------------------------------------- + +func (u userLoginName) loginNameColumn() database.Column { + return database.NewColumn(u.unqualifiedTableName(), "login_name") +} + +func (u userLoginName) instanceIDColumn() database.Column { + return database.NewColumn(u.unqualifiedTableName(), "instance_id") +} + +func (u userLoginName) userIDColumn() database.Column { + return database.NewColumn(u.unqualifiedTableName(), "user_id") +} diff --git a/backend/v3/storage/database/repository/user_login_name_test.go b/backend/v3/storage/database/repository/user_login_name_test.go new file mode 100644 index 000000000..fa4b805f3 --- /dev/null +++ b/backend/v3/storage/database/repository/user_login_name_test.go @@ -0,0 +1,378 @@ +package repository_test + +import ( + "context" + "fmt" + "maps" + "strconv" + "testing" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" +) + +func TestUserLoginNameManipulations(t *testing.T) { + tx, err := pool.Begin(t.Context(), nil) + require.NoError(t, err) + t.Cleanup(func() { + err := tx.Rollback(context.Background()) + if err != nil { + t.Log("failed to rollback transaction:", err) + } + }) + + orgDomainRepo := repository.OrganizationDomainRepository() + userRepo := repository.UserRepository() + settingsRepo := repository.DomainSettingsRepository() + + instanceID := createInstance(t, tx) + err = settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(false), + }, + }) + require.NoError(t, err) + + org1 := createOrganization(t, tx, instanceID) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID, + OrgID: org1, + Domain: "verified.example.com", + IsVerified: true, + }) + require.NoError(t, err) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID, + OrgID: org1, + Domain: "primary.example.com", + IsVerified: true, + IsPrimary: true, + }) + require.NoError(t, err) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID, + OrgID: org1, + Domain: "unverified.example.com", + }) + require.NoError(t, err) + err = settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: &org1, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + }, + }) + require.NoError(t, err) + + org2 := createOrganization(t, tx, instanceID) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID, + OrgID: org2, + Domain: "primary.example2.com", + IsVerified: true, + IsPrimary: true, + }) + require.NoError(t, err) + + loginNames := setupLoginNameTestUsers(t, tx, instanceID, org1, org2) + + for _, tt := range []struct { + name string + opts []database.QueryOption + expectedLoginNames map[string][]domain.LoginName + before func(t *testing.T, tx database.QueryExecutor) + }{ + { + name: "list all users", + opts: []database.QueryOption{ + database.WithCondition(userRepo.InstanceIDCondition(instanceID)), + }, + expectedLoginNames: maps.Clone(loginNames), + }, + { + name: "update usernames", + before: func(t *testing.T, tx database.QueryExecutor) { + for i := range 100 { + _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID, strconv.Itoa(i)), + userRepo.SetUsername("updated-user"+strconv.Itoa(i)), + ) + require.NoError(t, err) + } + }, + opts: []database.QueryOption{ + database.WithCondition(userRepo.InstanceIDCondition(instanceID)), + }, + expectedLoginNames: expectedLoginNames(loginNames, func(id string, loginNames []domain.LoginName) []domain.LoginName { + updated := make([]domain.LoginName, len(loginNames)) + for i, ln := range loginNames { + updated[i] = ln + updated[i].LoginName = "updated-" + ln.LoginName + } + return updated + }), + }, + { + name: "add org setting without impact", + before: func(t *testing.T, tx database.QueryExecutor) { + err := settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: &org2, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + RequireOrgDomainVerification: gu.Ptr(false), + }, + }) + require.NoError(t, err) + }, + opts: []database.QueryOption{ + database.WithCondition(userRepo.InstanceIDCondition(instanceID)), + }, + expectedLoginNames: maps.Clone(loginNames), + }, + { + name: "add and update org setting without impact", + before: func(t *testing.T, tx database.QueryExecutor) { + err := settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: &org2, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + RequireOrgDomainVerification: gu.Ptr(false), + }, + }) + require.NoError(t, err) + + err = settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: &org2, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + RequireOrgDomainVerification: gu.Ptr(true), + }, + }) + require.NoError(t, err) + }, + opts: []database.QueryOption{ + database.WithCondition(userRepo.InstanceIDCondition(instanceID)), + }, + expectedLoginNames: maps.Clone(loginNames), + }, + { + name: "add without impact and update with impact org setting", + before: func(t *testing.T, tx database.QueryExecutor) { + err := settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: &org2, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + RequireOrgDomainVerification: gu.Ptr(false), + }, + }) + require.NoError(t, err) + + err = settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: &org2, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + }, + }) + require.NoError(t, err) + }, + opts: []database.QueryOption{ + database.WithCondition(userRepo.InstanceIDCondition(instanceID)), + }, + expectedLoginNames: expectedLoginNames(loginNames, func(id string, loginNames []domain.LoginName) []domain.LoginName { + if len(loginNames) == 2 { + return loginNames + } + return []domain.LoginName{ + { + LoginName: fmt.Sprintf("user%s@primary.example2.com", id), + IsPreferred: true, + }, + } + }), + }, + { + name: "change org setting with impact", + before: func(t *testing.T, tx database.QueryExecutor) { + err := settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: &org1, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(false), + }, + }) + require.NoError(t, err) + }, + opts: []database.QueryOption{ + database.WithCondition(userRepo.InstanceIDCondition(instanceID)), + }, + expectedLoginNames: expectedLoginNames(loginNames, func(id string, loginNames []domain.LoginName) []domain.LoginName { + if len(loginNames) == 1 { + return loginNames + } + return []domain.LoginName{ + { + LoginName: "user" + id, + IsPreferred: true, + }, + } + }), + }, + { + name: "reset org setting", + before: func(t *testing.T, tx database.QueryExecutor) { + _, err := settingsRepo.Delete(t.Context(), tx, settingsRepo.UniqueCondition(instanceID, &org1, domain.SettingTypeDomain, domain.SettingStateActive)) + require.NoError(t, err) + }, + opts: []database.QueryOption{ + database.WithCondition(userRepo.InstanceIDCondition(instanceID)), + }, + expectedLoginNames: expectedLoginNames(loginNames, func(id string, loginNames []domain.LoginName) []domain.LoginName { + if len(loginNames) == 1 { + return loginNames + } + return []domain.LoginName{ + { + LoginName: "user" + id, + IsPreferred: true, + }, + } + }), + }, + { + name: "change instance setting", + before: func(t *testing.T, tx database.QueryExecutor) { + err := settingsRepo.Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + Type: domain.SettingTypeDomain, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + }, + }) + require.NoError(t, err) + }, + opts: []database.QueryOption{ + database.WithCondition(userRepo.InstanceIDCondition(instanceID)), + }, + expectedLoginNames: expectedLoginNames(loginNames, func(id string, loginNames []domain.LoginName) []domain.LoginName { + if len(loginNames) == 2 { + return loginNames + } + return []domain.LoginName{ + { + LoginName: fmt.Sprintf("user%s@primary.example2.com", id), + IsPreferred: true, + }, + } + }), + }, + } { + t.Run(tt.name, func(t *testing.T) { + savepoint, err := tx.Begin(t.Context()) + require.NoError(t, err) + t.Cleanup(func() { + err := savepoint.Rollback(context.Background()) + if err != nil { + t.Log("failed to rollback savepoint:", err) + } + }) + + if tt.before != nil { + tt.before(t, savepoint) + } + + users, err := userRepo.List(t.Context(), savepoint, tt.opts...) + require.NoError(t, err) + + for _, user := range users { + expected, ok := tt.expectedLoginNames[user.ID] + require.True(t, ok, "unexpected user ID: %s", user.ID) + assertLoginNames(t, expected, user.LoginNames) + delete(tt.expectedLoginNames, user.ID) + } + assert.Empty(t, tt.expectedLoginNames, "some expected users were not found") + }) + } +} + +func setupLoginNameTestUsers(t *testing.T, tx database.QueryExecutor, instanceID, org1, org2 string) map[string][]domain.LoginName { + userRepo := repository.UserRepository() + + expectedLoginNames := make(map[string][]domain.LoginName, 100) + for i := range 100 { + orgID := org1 + id := strconv.Itoa(i) + + expectedLoginNames[id] = []domain.LoginName{ + { + LoginName: fmt.Sprintf("user%d@verified.example.com", i), + IsPreferred: false, + }, + { + LoginName: fmt.Sprintf("user%d@primary.example.com", i), + IsPreferred: true, + }, + } + + if i%2 == 0 { + orgID = org2 + expectedLoginNames[id] = []domain.LoginName{ + { + LoginName: "user" + id, + IsPreferred: true, + }, + } + } + err := userRepo.Create(t.Context(), tx, &domain.User{ + InstanceID: instanceID, + OrganizationID: orgID, + ID: id, + Username: "user" + id, + State: domain.UserStateActive, + Machine: &domain.MachineUser{ + Name: "machine" + id, + }, + }) + require.NoError(t, err) + } + return expectedLoginNames +} + +func expectedLoginNames(loginNames map[string][]domain.LoginName, replace func(id string, loginNames []domain.LoginName) []domain.LoginName) map[string][]domain.LoginName { + updated := maps.Clone(loginNames) + for id, userLoginNames := range updated { + updated[id] = replace(id, userLoginNames) + } + return updated +} diff --git a/backend/v3/storage/database/repository/user_test.go b/backend/v3/storage/database/repository/user_test.go index e3c24cb90..f4d576ea7 100644 --- a/backend/v3/storage/database/repository/user_test.go +++ b/backend/v3/storage/database/repository/user_test.go @@ -26,12 +26,66 @@ func Test_user_Get(t *testing.T) { t.Cleanup(rollback) userRepo := repository.UserRepository() + orgDomainRepo := repository.OrganizationDomainRepository() instanceID := createInstance(t, tx) + err := repository.DomainSettingsRepository().Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + Type: domain.SettingTypeDomain, + State: domain.SettingStateActive, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(false), + RequireOrgDomainVerification: gu.Ptr(false), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(false), + }, + }) + require.NoError(t, err) + orgID1 := createOrganization(t, tx, instanceID) orgID2 := createOrganization(t, tx, instanceID) + err = repository.DomainSettingsRepository().Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID, + OrganizationID: &orgID2, + Type: domain.SettingTypeDomain, + State: domain.SettingStateActive, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(false), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(false), + }, + }) + require.NoError(t, err) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID, + OrgID: orgID2, + Domain: "primary.zitadel.com", + IsVerified: true, + IsPrimary: true, + }) + require.NoError(t, err) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID, + OrgID: orgID2, + Domain: "verified.zitadel.com", + IsVerified: true, + IsPrimary: false, + }) + require.NoError(t, err) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID, + OrgID: orgID2, + Domain: "unverified.zitadel.com", + IsVerified: false, + IsPrimary: false, + }) + require.NoError(t, err) + now := time.Now().Round(time.Millisecond) human := &domain.User{ @@ -42,6 +96,12 @@ func Test_user_Get(t *testing.T) { State: domain.UserStateActive, CreatedAt: now, UpdatedAt: now, + LoginNames: []domain.LoginName{ + { + LoginName: "human-user", + IsPreferred: true, + }, + }, Human: &domain.HumanUser{ FirstName: "John", LastName: "Doe", @@ -66,20 +126,28 @@ func Test_user_Get(t *testing.T) { State: domain.UserStateActive, CreatedAt: now, UpdatedAt: now, + LoginNames: []domain.LoginName{ + { + LoginName: "machine-user@primary.zitadel.com", + IsPreferred: true, + }, + { + LoginName: "machine-user@verified.zitadel.com", + }, + }, Machine: &domain.MachineUser{ Name: "My Machine", Description: "This is my machine user", }, } - err := userRepo.Create(t.Context(), tx, human) + err = userRepo.Create(t.Context(), tx, human) require.NoError(t, err) err = userRepo.Create(t.Context(), tx, machine) require.NoError(t, err) tests := []struct { name string - setup func(t *testing.T, tx database.Transaction) error condition database.Condition wantErr error expected *domain.User @@ -120,10 +188,6 @@ func Test_user_Get(t *testing.T) { t.Log("rollback savepoint failed", err) } }) - if tt.setup != nil { - err := tt.setup(t, savepoint) - require.NoError(t, err) - } user, err := userRepo.Get(t.Context(), savepoint, database.WithCondition(tt.condition)) require.ErrorIs(t, err, tt.wantErr) @@ -536,10 +600,57 @@ func Test_user_ListConditions(t *testing.T) { userRepo := repository.UserRepository() humanRepo := repository.HumanUserRepository() + orgDomainRepo := repository.OrganizationDomainRepository() instanceID1 := createInstance(t, tx) + + err := repository.DomainSettingsRepository().Set(t.Context(), tx, &domain.DomainSettings{ + Settings: domain.Settings{ + InstanceID: instanceID1, + Type: domain.SettingTypeDomain, + State: domain.SettingStateActive, + }, + DomainSettingsAttributes: domain.DomainSettingsAttributes{ + LoginNameIncludesDomain: gu.Ptr(true), + RequireOrgDomainVerification: gu.Ptr(false), + SMTPSenderAddressMatchesInstanceDomain: gu.Ptr(false), + }, + }) + require.NoError(t, err) + instance1OrgID1 := createOrganization(t, tx, instanceID1) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID1, + OrgID: instance1OrgID1, + Domain: "org1.zitadel.com", + IsVerified: true, + IsPrimary: true, + }) + require.NoError(t, err) + instance1OrgID2 := createOrganization(t, tx, instanceID1) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID1, + OrgID: instance1OrgID2, + Domain: "org2-primary.zitadel.com", + IsVerified: true, + IsPrimary: true, + }) + require.NoError(t, err) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID1, + OrgID: instance1OrgID2, + Domain: "org2-verified.zitadel.com", + IsVerified: true, + }) + require.NoError(t, err) + err = orgDomainRepo.Add(t.Context(), tx, &domain.AddOrganizationDomain{ + InstanceID: instanceID1, + OrgID: instance1OrgID2, + Domain: "org2-unverified.zitadel.com", + }) + require.NoError(t, err) + idpID := createIdentityProvider(t, tx, instanceID1, instance1OrgID1) instanceID2 := createInstance(t, tx) @@ -645,7 +756,7 @@ func Test_user_ListConditions(t *testing.T) { } tests := []struct { name string - setup func(t *testing.T, tx database.Transaction) + setup func(t *testing.T, tx database.QueryExecutor) opts []database.QueryOption want want }{ @@ -711,7 +822,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by state", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), userRepo.SetState(domain.UserStateInactive), ) @@ -739,7 +850,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by type", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { }, opts: []database.QueryOption{ database.WithCondition(database.And( @@ -752,25 +863,24 @@ func Test_user_ListConditions(t *testing.T) { ids: []string{"m1", "m2"}, }, }, - // { - // name: "by login name", - // setup: func(t *testing.T, tx database.Transaction) { - // }, - // opts: []database.QueryOption{ - // database.WithCondition(database.And( - // userRepo.InstanceIDCondition(instanceID1), - // )), - // }, - // want: want{ - // err: nil, - // ids: []string{}, - // }, - // }, + { + name: "by login name found", + opts: []database.QueryOption{ + database.WithCondition(database.And( + userRepo.InstanceIDCondition(instanceID1), + userRepo.LoginNameCondition(database.TextOperationEqual, "human-user-2@org2-primary.zitadel.com"), + )), + }, + want: want{ + err: nil, + ids: []string{"h2"}, + }, + }, // metadata conditions { name: "by metadata key", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), userRepo.SetMetadata( &domain.Metadata{ @@ -812,7 +922,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by metadata value", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), userRepo.SetMetadata( &domain.Metadata{ @@ -864,7 +974,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by metadata key and value", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), userRepo.SetMetadata( &domain.Metadata{ @@ -921,7 +1031,7 @@ func Test_user_ListConditions(t *testing.T) { // human conditions { name: "by display name", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { }, opts: []database.QueryOption{ database.WithCondition(database.And( @@ -936,7 +1046,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by first name", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { }, opts: []database.QueryOption{ database.WithCondition(database.And( @@ -951,7 +1061,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by last name", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { }, opts: []database.QueryOption{ database.WithCondition(database.And( @@ -966,7 +1076,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by nickname", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { }, opts: []database.QueryOption{ database.WithCondition(database.And( @@ -981,7 +1091,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by preferred language", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { }, opts: []database.QueryOption{ database.WithCondition(database.And( @@ -998,7 +1108,7 @@ func Test_user_ListConditions(t *testing.T) { // phone conditions { name: "by phone", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := humanRepo.Update(t.Context(), tx, humanRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.SetPhone("+1234567890"), humanRepo.SetPhoneVerification(&domain.VerificationTypeSkipped{}), @@ -1025,7 +1135,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by unverified phone", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := humanRepo.Update(t.Context(), tx, humanRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.SetPhone("+1234567890"), humanRepo.SetPhoneVerification(&domain.VerificationTypeSkipped{}), @@ -1052,7 +1162,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by verified phone", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := humanRepo.Update(t.Context(), tx, humanRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.SetPhone("+1234567890"), humanRepo.SetPhoneVerification(&domain.VerificationTypeSkipped{}), @@ -1081,7 +1191,7 @@ func Test_user_ListConditions(t *testing.T) { // human email conditions { name: "by email", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := humanRepo.Update(t.Context(), tx, humanRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.SetEmail("test@email.com"), humanRepo.SetEmailVerification(&domain.VerificationTypeSkipped{}), @@ -1108,7 +1218,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by unverified email", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := humanRepo.Update(t.Context(), tx, humanRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.SetEmail("test@email.com"), humanRepo.SetEmailVerification(&domain.VerificationTypeSkipped{}), @@ -1135,7 +1245,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by verified email", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := humanRepo.Update(t.Context(), tx, humanRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.SetEmail("test@email.com"), humanRepo.SetEmailVerification(&domain.VerificationTypeSkipped{}), @@ -1164,7 +1274,7 @@ func Test_user_ListConditions(t *testing.T) { // passkey conditions { name: "by passkey same challenge", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.AddPasskey( &domain.Passkey{ @@ -1224,7 +1334,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by passkey challenge", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.AddPasskey( &domain.Passkey{ @@ -1284,7 +1394,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by passkey id", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.AddPasskey( &domain.Passkey{ @@ -1344,7 +1454,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by passkey key id", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.AddPasskey( &domain.Passkey{ @@ -1404,7 +1514,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by passkey type", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.AddPasskey( &domain.Passkey{ @@ -1466,7 +1576,7 @@ func Test_user_ListConditions(t *testing.T) { // idp link conditions { name: "by idp link provider id", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.AddIdentityProviderLink(&domain.IdentityProviderLink{ ProviderID: idpID, @@ -1505,7 +1615,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by idp link provided user id", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.AddIdentityProviderLink(&domain.IdentityProviderLink{ ProviderID: idpID, @@ -1544,7 +1654,7 @@ func Test_user_ListConditions(t *testing.T) { }, { name: "by idp link username", - setup: func(t *testing.T, tx database.Transaction) { + setup: func(t *testing.T, tx database.QueryExecutor) { _, err := userRepo.Update(t.Context(), tx, userRepo.PrimaryKeyCondition(instanceID1, "h1"), humanRepo.AddIdentityProviderLink(&domain.IdentityProviderLink{ ProviderID: idpID, @@ -4395,6 +4505,7 @@ func assertUser(t *testing.T, expected, actual *domain.User) { require.Equal(t, expected.Human == nil, actual.Human == nil, "human check") assertMetadata(t, expected.Metadata, actual.Metadata) + assertLoginNames(t, expected.LoginNames, actual.LoginNames) if expected.Machine != nil { assertMachineUser(t, expected.Machine, actual.Machine) @@ -4409,6 +4520,25 @@ func assertUser(t *testing.T, expected, actual *domain.User) { t.Fail() } +func assertLoginNames(t *testing.T, expected, gotten []domain.LoginName) { + t.Helper() + + assert.Len(t, gotten, len(expected), "login names length mismatch") + for _, exp := range expected { + var actual domain.LoginName + gotten = slices.DeleteFunc(gotten, func(ln domain.LoginName) bool { + if ln.LoginName != exp.LoginName { + return false + } + actual = ln + return true + }) + require.NotEmpty(t, actual, "login name %q not found", exp.LoginName) + assert.Equal(t, exp.IsPreferred, actual.IsPreferred, "login name %q is preferred", exp.LoginName) + } + assert.Empty(t, gotten, "unmatched login names found") +} + func assertMachineUser(t *testing.T, expected, actual *domain.MachineUser) { t.Helper()