mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
fix: increase performance of ListUser by login name ignore case (#12350)
# Which Problems Are Solved Login v2 uses the `users.v2.ListUsers`-endpoint to get a user by login name. This query had an inefficient `WHERE`-clause. # How the Problems Are Solved Update the view and use a specific clause for this query. # Additional information Added in https://github.com/zitadel/zitadel/pull/10475 --------- Co-authored-by: Marco A. <[email protected]> Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
co-authored by
Marco A.
Livio Spring
parent
561da54ff7
commit
f9995ee39c
@@ -587,6 +587,8 @@ export type ListUsersCommand = WithServiceConfig<{
|
|||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
const userLookupQuery = { limit: 2 };
|
||||||
|
|
||||||
export async function listUsers({ serviceConfig, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
|
export async function listUsers({ serviceConfig, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
|
||||||
const queries: SearchQuery[] = [];
|
const queries: SearchQuery[] = [];
|
||||||
|
|
||||||
@@ -672,7 +674,7 @@ export async function listUsers({ serviceConfig, loginName, userName, phone, ema
|
|||||||
|
|
||||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||||
|
|
||||||
return userService.listUsers({ queries });
|
return userService.listUsers({ query: userLookupQuery, queries });
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchUsersCommand = WithServiceConfig<{
|
export type SearchUsersCommand = WithServiceConfig<{
|
||||||
@@ -755,7 +757,7 @@ export async function searchUsers({
|
|||||||
|
|
||||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||||
|
|
||||||
const loginNameResult = await userService.listUsers({ queries });
|
const loginNameResult = await userService.listUsers({ query: userLookupQuery, queries });
|
||||||
|
|
||||||
if (!loginNameResult || !loginNameResult.details) {
|
if (!loginNameResult || !loginNameResult.details) {
|
||||||
return { error: t("errors.errorOccured") };
|
return { error: t("errors.errorOccured") };
|
||||||
@@ -817,6 +819,7 @@ export async function searchUsers({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const emailOrPhoneResult = await userService.listUsers({
|
const emailOrPhoneResult = await userService.listUsers({
|
||||||
|
query: userLookupQuery,
|
||||||
queries: emailAndPhoneQueries,
|
queries: emailAndPhoneQueries,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed 72.sql
|
||||||
|
addColumnsToLoginNamesView string
|
||||||
|
)
|
||||||
|
|
||||||
|
type AddColumnsToLoginNamesView struct {
|
||||||
|
dbClient *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mig *AddColumnsToLoginNamesView) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||||
|
_, err := mig.dbClient.ExecContext(ctx, addColumnsToLoginNamesView)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mig *AddColumnsToLoginNamesView) String() string {
|
||||||
|
return "72_add_columns_to_login_names_view"
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
CREATE OR REPLACE VIEW projections.login_names3 AS (
|
||||||
|
SELECT
|
||||||
|
u.id AS user_id
|
||||||
|
, CASE
|
||||||
|
WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name)
|
||||||
|
ELSE u.user_name
|
||||||
|
END AS login_name
|
||||||
|
, COALESCE(d.is_primary, TRUE) AS is_primary
|
||||||
|
, u.instance_id
|
||||||
|
, u.resource_owner
|
||||||
|
, CASE
|
||||||
|
WHEN p.must_be_domain THEN CONCAT(u.user_name_lower, '@', d.name_lower)
|
||||||
|
ELSE u.user_name_lower
|
||||||
|
END AS login_name_lower
|
||||||
|
FROM
|
||||||
|
projections.login_names3_users AS u
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT
|
||||||
|
must_be_domain
|
||||||
|
, is_default
|
||||||
|
FROM
|
||||||
|
projections.login_names3_policies AS p
|
||||||
|
WHERE
|
||||||
|
(
|
||||||
|
p.instance_id = u.instance_id
|
||||||
|
AND NOT p.is_default
|
||||||
|
AND p.resource_owner = u.resource_owner
|
||||||
|
) OR (
|
||||||
|
p.instance_id = u.instance_id
|
||||||
|
AND p.is_default
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
p.is_default -- custom first
|
||||||
|
LIMIT 1
|
||||||
|
) AS p ON TRUE
|
||||||
|
LEFT JOIN
|
||||||
|
projections.login_names3_domains d
|
||||||
|
ON
|
||||||
|
p.must_be_domain
|
||||||
|
AND u.resource_owner = d.resource_owner
|
||||||
|
AND u.instance_id = d.instance_id
|
||||||
|
);
|
||||||
@@ -191,6 +191,7 @@ type Steps struct {
|
|||||||
s69CacheTablesLogged *CacheTablesLogged
|
s69CacheTablesLogged *CacheTablesLogged
|
||||||
s70AddEventStoreCommandEnforceOwner *AddEventStoreCommandEnforceOwnerColumn
|
s70AddEventStoreCommandEnforceOwner *AddEventStoreCommandEnforceOwnerColumn
|
||||||
s71JWTProvideAddAudienceColumn *JWTProvideAddAudienceColumn
|
s71JWTProvideAddAudienceColumn *JWTProvideAddAudienceColumn
|
||||||
|
s72AddColumnsToLoginNamesView *AddColumnsToLoginNamesView
|
||||||
RelationalTables *TransactionalTables
|
RelationalTables *TransactionalTables
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
|||||||
steps.s69CacheTablesLogged = &CacheTablesLogged{dbClient: dbClient}
|
steps.s69CacheTablesLogged = &CacheTablesLogged{dbClient: dbClient}
|
||||||
steps.s70AddEventStoreCommandEnforceOwner = &AddEventStoreCommandEnforceOwnerColumn{dbClient: dbClient}
|
steps.s70AddEventStoreCommandEnforceOwner = &AddEventStoreCommandEnforceOwnerColumn{dbClient: dbClient}
|
||||||
steps.s71JWTProvideAddAudienceColumn = &JWTProvideAddAudienceColumn{dbClient: dbClient}
|
steps.s71JWTProvideAddAudienceColumn = &JWTProvideAddAudienceColumn{dbClient: dbClient}
|
||||||
|
steps.s72AddColumnsToLoginNamesView = &AddColumnsToLoginNamesView{dbClient: dbClient}
|
||||||
|
|
||||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
|
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -378,6 +379,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
|||||||
steps.s66SessionRecoveryCodeCheckedAt,
|
steps.s66SessionRecoveryCodeCheckedAt,
|
||||||
steps.s68TargetAddPayloadTypeColumn,
|
steps.s68TargetAddPayloadTypeColumn,
|
||||||
steps.s71JWTProvideAddAudienceColumn,
|
steps.s71JWTProvideAddAudienceColumn,
|
||||||
|
steps.s72AddColumnsToLoginNamesView,
|
||||||
} {
|
} {
|
||||||
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
|
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
|
||||||
if setupErr != nil {
|
if setupErr != nil {
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ type InitColumn struct {
|
|||||||
nullable bool
|
nullable bool
|
||||||
defaultValue interface{}
|
defaultValue interface{}
|
||||||
deleteCascade string
|
deleteCascade string
|
||||||
|
generated string
|
||||||
|
isStored bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColumnOption func(*InitColumn)
|
type ColumnOption func(*InitColumn)
|
||||||
@@ -105,6 +107,13 @@ func DeleteCascade(column string) ColumnOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Generated(stmt string, isStored bool) ColumnOption {
|
||||||
|
return func(c *InitColumn) {
|
||||||
|
c.generated = stmt
|
||||||
|
c.isStored = isStored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type PrimaryKey []string
|
type PrimaryKey []string
|
||||||
|
|
||||||
func NewPrimaryKey(columnNames ...string) PrimaryKey {
|
func NewPrimaryKey(columnNames ...string) PrimaryKey {
|
||||||
@@ -381,6 +390,12 @@ func createColumnsStatement(cols []*InitColumn, tableName string) string {
|
|||||||
if col.defaultValue != nil {
|
if col.defaultValue != nil {
|
||||||
column += " DEFAULT " + defaultValue(col.defaultValue)
|
column += " DEFAULT " + defaultValue(col.defaultValue)
|
||||||
}
|
}
|
||||||
|
if col.generated != "" {
|
||||||
|
column += " GENERATED ALWAYS AS (" + col.generated + ")"
|
||||||
|
if col.isStored {
|
||||||
|
column += " STORED"
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(col.deleteCascade) != 0 {
|
if len(col.deleteCascade) != 0 {
|
||||||
column += fmt.Sprintf(" REFERENCES %s (%s) ON DELETE CASCADE", tableName, col.deleteCascade)
|
column += fmt.Sprintf(" REFERENCES %s (%s) ON DELETE CASCADE", tableName, col.deleteCascade)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ var (
|
|||||||
name: projection.LoginNameCol,
|
name: projection.LoginNameCol,
|
||||||
table: loginNameTable,
|
table: loginNameTable,
|
||||||
}
|
}
|
||||||
|
LoginNameNameLowerCol = Column{
|
||||||
|
name: projection.LoginNameLowerCol,
|
||||||
|
table: loginNameTable,
|
||||||
|
}
|
||||||
LoginNameIsPrimaryCol = Column{
|
LoginNameIsPrimaryCol = Column{
|
||||||
name: projection.LoginNameIsPrimaryCol,
|
name: projection.LoginNameIsPrimaryCol,
|
||||||
table: loginNameTable,
|
table: loginNameTable,
|
||||||
@@ -23,4 +27,8 @@ var (
|
|||||||
name: projection.LoginNameInstanceIDCol,
|
name: projection.LoginNameInstanceIDCol,
|
||||||
table: loginNameTable,
|
table: loginNameTable,
|
||||||
}
|
}
|
||||||
|
LoginNameResourceOwnerCol = Column{
|
||||||
|
name: projection.LoginNameResourceOwnerCol,
|
||||||
|
table: loginNameTable,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ const (
|
|||||||
LoginNamePolicyProjectionTable = LoginNameProjectionTable + "_" + loginNamePolicySuffix
|
LoginNamePolicyProjectionTable = LoginNameProjectionTable + "_" + loginNamePolicySuffix
|
||||||
LoginNameDomainProjectionTable = LoginNameProjectionTable + "_" + loginNameDomainSuffix
|
LoginNameDomainProjectionTable = LoginNameProjectionTable + "_" + loginNameDomainSuffix
|
||||||
|
|
||||||
LoginNameCol = "login_name"
|
LoginNameCol = "login_name"
|
||||||
LoginNameUserCol = "user_id"
|
LoginNameUserCol = "user_id"
|
||||||
LoginNameIsPrimaryCol = "is_primary"
|
LoginNameIsPrimaryCol = "is_primary"
|
||||||
LoginNameInstanceIDCol = "instance_id"
|
LoginNameInstanceIDCol = "instance_id"
|
||||||
|
LoginNameLowerCol = "login_name_lower"
|
||||||
|
LoginNameResourceOwnerCol = "resource_owner"
|
||||||
|
|
||||||
usersAlias = "users"
|
usersAlias = "users"
|
||||||
policyCustomAlias = "policy_custom"
|
policyCustomAlias = "policy_custom"
|
||||||
@@ -76,8 +78,7 @@ func (*loginNameProjection) Init() *old_handler.Check {
|
|||||||
[]*handler.InitColumn{
|
[]*handler.InitColumn{
|
||||||
handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText),
|
handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText),
|
||||||
handler.NewColumn(LoginNameUserUserNameCol, handler.ColumnTypeText),
|
handler.NewColumn(LoginNameUserUserNameCol, handler.ColumnTypeText),
|
||||||
// TODO: implement computed columns
|
handler.NewColumn(loginNameUserUserNameLowerCol, handler.ColumnTypeText, handler.Generated("lower("+LoginNameUserUserNameCol+")", true)),
|
||||||
// handler.NewComputedColumn(loginNameUserUserNameLowerCol, handler.ColumnTypeText),
|
|
||||||
handler.NewColumn(LoginNameUserResourceOwnerCol, handler.ColumnTypeText),
|
handler.NewColumn(LoginNameUserResourceOwnerCol, handler.ColumnTypeText),
|
||||||
handler.NewColumn(LoginNameUserInstanceIDCol, handler.ColumnTypeText),
|
handler.NewColumn(LoginNameUserInstanceIDCol, handler.ColumnTypeText),
|
||||||
},
|
},
|
||||||
@@ -93,33 +94,30 @@ func (*loginNameProjection) Init() *old_handler.Check {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// TODO: uncomment the following line when login_names4 will be created
|
handler.WithIndex(
|
||||||
// handler.WithIndex(
|
handler.NewIndex("search", []string{LoginNameUserInstanceIDCol, loginNameUserUserNameLowerCol},
|
||||||
// handler.NewIndex("search", []string{LoginNameUserInstanceIDCol, loginNameUserUserNameLowerCol},
|
handler.WithInclude(LoginNameUserResourceOwnerCol),
|
||||||
// handler.WithInclude(LoginNameUserResourceOwnerCol),
|
),
|
||||||
// ),
|
),
|
||||||
// ),
|
|
||||||
),
|
),
|
||||||
handler.NewSuffixedTable(
|
handler.NewSuffixedTable(
|
||||||
[]*handler.InitColumn{
|
[]*handler.InitColumn{
|
||||||
handler.NewColumn(LoginNameDomainNameCol, handler.ColumnTypeText),
|
handler.NewColumn(LoginNameDomainNameCol, handler.ColumnTypeText),
|
||||||
// TODO: implement computed columns
|
handler.NewColumn(loginNameDomainNameLowerCol, handler.ColumnTypeText, handler.Generated("lower("+LoginNameDomainNameCol+")", true)),
|
||||||
// handler.NewComputedColumn(loginNameDomainNameLowerCol, handler.ColumnTypeText),
|
|
||||||
handler.NewColumn(LoginNameDomainIsPrimaryCol, handler.ColumnTypeBool, handler.Default(false)),
|
handler.NewColumn(LoginNameDomainIsPrimaryCol, handler.ColumnTypeBool, handler.Default(false)),
|
||||||
handler.NewColumn(LoginNameDomainResourceOwnerCol, handler.ColumnTypeText),
|
handler.NewColumn(LoginNameDomainResourceOwnerCol, handler.ColumnTypeText),
|
||||||
handler.NewColumn(LoginNameDomainInstanceIDCol, handler.ColumnTypeText),
|
handler.NewColumn(LoginNameDomainInstanceIDCol, handler.ColumnTypeText),
|
||||||
},
|
},
|
||||||
handler.NewPrimaryKey(LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, LoginNameDomainNameCol),
|
handler.NewPrimaryKey(LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, LoginNameDomainNameCol),
|
||||||
loginNameDomainSuffix,
|
loginNameDomainSuffix,
|
||||||
// TODO: uncomment the following line when login_names4 will be created
|
handler.WithIndex(
|
||||||
// handler.WithIndex(
|
handler.NewIndex("search", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, loginNameDomainNameLowerCol}),
|
||||||
// handler.NewIndex("search", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, loginNameDomainNameLowerCol}),
|
),
|
||||||
// ),
|
handler.WithIndex(
|
||||||
// handler.WithIndex(
|
handler.NewIndex("search_result", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol},
|
||||||
// handler.NewIndex("search_result", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol},
|
handler.WithInclude(LoginNameDomainIsPrimaryCol),
|
||||||
// handler.WithInclude(LoginNameDomainIsPrimaryCol),
|
),
|
||||||
// ),
|
),
|
||||||
// ),
|
|
||||||
),
|
),
|
||||||
handler.NewSuffixedTable(
|
handler.NewSuffixedTable(
|
||||||
[]*handler.InitColumn{
|
[]*handler.InitColumn{
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ SELECT
|
|||||||
END AS login_name
|
END AS login_name
|
||||||
, COALESCE(d.is_primary, TRUE) AS is_primary
|
, COALESCE(d.is_primary, TRUE) AS is_primary
|
||||||
, u.instance_id
|
, u.instance_id
|
||||||
|
, u.resource_owner
|
||||||
|
, CASE
|
||||||
|
WHEN p.must_be_domain THEN CONCAT(u.user_name_lower, '@', d.name_lower)
|
||||||
|
ELSE u.user_name_lower
|
||||||
|
END AS login_name_lower
|
||||||
FROM
|
FROM
|
||||||
projections.login_names3_users AS u
|
projections.login_names3_users AS u
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ func Projections() []projection {
|
|||||||
func Init(ctx context.Context) error {
|
func Init(ctx context.Context) error {
|
||||||
for _, p := range projections {
|
for _, p := range projections {
|
||||||
if err := p.Init(ctx); err != nil {
|
if err := p.Init(ctx); err != nil {
|
||||||
|
logging.WithError(err).WithField("projection", p.ProjectionName()).Error("failed to initialize projection")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -803,13 +803,21 @@ func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (Searc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
resourceOwnerQuery, err := NewColumnComparisonQuery(LoginNameResourceOwnerCol, UserResourceOwnerCol, ColumnEquals)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
// text query to select data from the linked sub select
|
// text query to select data from the linked sub select
|
||||||
loginNameQuery, err := NewTextQuery(LoginNameNameCol, value, comparison)
|
var loginNameQuery SearchQuery
|
||||||
|
loginNameQuery, err = NewTextQuery(LoginNameNameCol, value, comparison)
|
||||||
|
if comparison == TextEqualsIgnoreCase {
|
||||||
|
loginNameQuery, err = NewTextQuery(LoginNameNameLowerCol, strings.ToLower(value), TextEquals)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// full definition of the sub select
|
// full definition of the sub select
|
||||||
subSelect, err := NewSubSelect(LoginNameUserIDCol, []SearchQuery{instanceQuery, userIDQuery, loginNameQuery})
|
subSelect, err := NewSubSelect(LoginNameUserIDCol, []SearchQuery{instanceQuery, userIDQuery, resourceOwnerQuery, loginNameQuery})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user