feat: add login names to relational users (#11667)

Introduce a new SQL migration that creates the `login_names` table and
associated triggers/functions for managing login names based on domain
policies and user updates. Manual tests confirm the functionality works
as intended.
This commit is contained in:
Silvan
2026-03-03 09:36:43 +00:00
committed by GitHub
parent 4aacfa842f
commit 41b8aa3f18
12 changed files with 1412 additions and 187 deletions
@@ -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: "[email protected]",
IsPreferred: true,
},
{
LoginName: "[email protected]",
},
},
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, "[email protected]"),
)),
},
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("[email protected]"),
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("[email protected]"),
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("[email protected]"),
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()