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;
|
||||
}>;
|
||||
|
||||
const userLookupQuery = { limit: 2 };
|
||||
|
||||
export async function listUsers({ serviceConfig, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
|
||||
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);
|
||||
|
||||
return userService.listUsers({ queries });
|
||||
return userService.listUsers({ query: userLookupQuery, queries });
|
||||
}
|
||||
|
||||
export type SearchUsersCommand = WithServiceConfig<{
|
||||
@@ -755,7 +757,7 @@ export async function searchUsers({
|
||||
|
||||
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) {
|
||||
return { error: t("errors.errorOccured") };
|
||||
@@ -817,6 +819,7 @@ export async function searchUsers({
|
||||
}
|
||||
|
||||
const emailOrPhoneResult = await userService.listUsers({
|
||||
query: userLookupQuery,
|
||||
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
|
||||
s70AddEventStoreCommandEnforceOwner *AddEventStoreCommandEnforceOwnerColumn
|
||||
s71JWTProvideAddAudienceColumn *JWTProvideAddAudienceColumn
|
||||
s72AddColumnsToLoginNamesView *AddColumnsToLoginNamesView
|
||||
RelationalTables *TransactionalTables
|
||||
}
|
||||
|
||||
|
||||
@@ -254,6 +254,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s69CacheTablesLogged = &CacheTablesLogged{dbClient: dbClient}
|
||||
steps.s70AddEventStoreCommandEnforceOwner = &AddEventStoreCommandEnforceOwnerColumn{dbClient: dbClient}
|
||||
steps.s71JWTProvideAddAudienceColumn = &JWTProvideAddAudienceColumn{dbClient: dbClient}
|
||||
steps.s72AddColumnsToLoginNamesView = &AddColumnsToLoginNamesView{dbClient: dbClient}
|
||||
|
||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||
if err != nil {
|
||||
@@ -378,6 +379,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s66SessionRecoveryCodeCheckedAt,
|
||||
steps.s68TargetAddPayloadTypeColumn,
|
||||
steps.s71JWTProvideAddAudienceColumn,
|
||||
steps.s72AddColumnsToLoginNamesView,
|
||||
} {
|
||||
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
|
||||
if setupErr != nil {
|
||||
|
||||
@@ -70,6 +70,8 @@ type InitColumn struct {
|
||||
nullable bool
|
||||
defaultValue interface{}
|
||||
deleteCascade string
|
||||
generated string
|
||||
isStored bool
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
func NewPrimaryKey(columnNames ...string) PrimaryKey {
|
||||
@@ -381,6 +390,12 @@ func createColumnsStatement(cols []*InitColumn, tableName string) string {
|
||||
if col.defaultValue != nil {
|
||||
column += " DEFAULT " + defaultValue(col.defaultValue)
|
||||
}
|
||||
if col.generated != "" {
|
||||
column += " GENERATED ALWAYS AS (" + col.generated + ")"
|
||||
if col.isStored {
|
||||
column += " STORED"
|
||||
}
|
||||
}
|
||||
if len(col.deleteCascade) != 0 {
|
||||
column += fmt.Sprintf(" REFERENCES %s (%s) ON DELETE CASCADE", tableName, col.deleteCascade)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ var (
|
||||
name: projection.LoginNameCol,
|
||||
table: loginNameTable,
|
||||
}
|
||||
LoginNameNameLowerCol = Column{
|
||||
name: projection.LoginNameLowerCol,
|
||||
table: loginNameTable,
|
||||
}
|
||||
LoginNameIsPrimaryCol = Column{
|
||||
name: projection.LoginNameIsPrimaryCol,
|
||||
table: loginNameTable,
|
||||
@@ -23,4 +27,8 @@ var (
|
||||
name: projection.LoginNameInstanceIDCol,
|
||||
table: loginNameTable,
|
||||
}
|
||||
LoginNameResourceOwnerCol = Column{
|
||||
name: projection.LoginNameResourceOwnerCol,
|
||||
table: loginNameTable,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -22,10 +22,12 @@ const (
|
||||
LoginNamePolicyProjectionTable = LoginNameProjectionTable + "_" + loginNamePolicySuffix
|
||||
LoginNameDomainProjectionTable = LoginNameProjectionTable + "_" + loginNameDomainSuffix
|
||||
|
||||
LoginNameCol = "login_name"
|
||||
LoginNameUserCol = "user_id"
|
||||
LoginNameIsPrimaryCol = "is_primary"
|
||||
LoginNameInstanceIDCol = "instance_id"
|
||||
LoginNameCol = "login_name"
|
||||
LoginNameUserCol = "user_id"
|
||||
LoginNameIsPrimaryCol = "is_primary"
|
||||
LoginNameInstanceIDCol = "instance_id"
|
||||
LoginNameLowerCol = "login_name_lower"
|
||||
LoginNameResourceOwnerCol = "resource_owner"
|
||||
|
||||
usersAlias = "users"
|
||||
policyCustomAlias = "policy_custom"
|
||||
@@ -76,8 +78,7 @@ func (*loginNameProjection) Init() *old_handler.Check {
|
||||
[]*handler.InitColumn{
|
||||
handler.NewColumn(LoginNameUserIDCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(LoginNameUserUserNameCol, handler.ColumnTypeText),
|
||||
// TODO: implement computed columns
|
||||
// handler.NewComputedColumn(loginNameUserUserNameLowerCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(loginNameUserUserNameLowerCol, handler.ColumnTypeText, handler.Generated("lower("+LoginNameUserUserNameCol+")", true)),
|
||||
handler.NewColumn(LoginNameUserResourceOwnerCol, 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.NewIndex("search", []string{LoginNameUserInstanceIDCol, loginNameUserUserNameLowerCol},
|
||||
// handler.WithInclude(LoginNameUserResourceOwnerCol),
|
||||
// ),
|
||||
// ),
|
||||
handler.WithIndex(
|
||||
handler.NewIndex("search", []string{LoginNameUserInstanceIDCol, loginNameUserUserNameLowerCol},
|
||||
handler.WithInclude(LoginNameUserResourceOwnerCol),
|
||||
),
|
||||
),
|
||||
),
|
||||
handler.NewSuffixedTable(
|
||||
[]*handler.InitColumn{
|
||||
handler.NewColumn(LoginNameDomainNameCol, handler.ColumnTypeText),
|
||||
// TODO: implement computed columns
|
||||
// handler.NewComputedColumn(loginNameDomainNameLowerCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(loginNameDomainNameLowerCol, handler.ColumnTypeText, handler.Generated("lower("+LoginNameDomainNameCol+")", true)),
|
||||
handler.NewColumn(LoginNameDomainIsPrimaryCol, handler.ColumnTypeBool, handler.Default(false)),
|
||||
handler.NewColumn(LoginNameDomainResourceOwnerCol, handler.ColumnTypeText),
|
||||
handler.NewColumn(LoginNameDomainInstanceIDCol, handler.ColumnTypeText),
|
||||
},
|
||||
handler.NewPrimaryKey(LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, LoginNameDomainNameCol),
|
||||
loginNameDomainSuffix,
|
||||
// TODO: uncomment the following line when login_names4 will be created
|
||||
// handler.WithIndex(
|
||||
// handler.NewIndex("search", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, loginNameDomainNameLowerCol}),
|
||||
// ),
|
||||
// handler.WithIndex(
|
||||
// handler.NewIndex("search_result", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol},
|
||||
// handler.WithInclude(LoginNameDomainIsPrimaryCol),
|
||||
// ),
|
||||
// ),
|
||||
handler.WithIndex(
|
||||
handler.NewIndex("search", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol, loginNameDomainNameLowerCol}),
|
||||
),
|
||||
handler.WithIndex(
|
||||
handler.NewIndex("search_result", []string{LoginNameDomainInstanceIDCol, LoginNameDomainResourceOwnerCol},
|
||||
handler.WithInclude(LoginNameDomainIsPrimaryCol),
|
||||
),
|
||||
),
|
||||
),
|
||||
handler.NewSuffixedTable(
|
||||
[]*handler.InitColumn{
|
||||
|
||||
@@ -6,6 +6,11 @@ SELECT
|
||||
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 (
|
||||
|
||||
@@ -221,6 +221,7 @@ func Projections() []projection {
|
||||
func Init(ctx context.Context) error {
|
||||
for _, p := range projections {
|
||||
if err := p.Init(ctx); err != nil {
|
||||
logging.WithError(err).WithField("projection", p.ProjectionName()).Error("failed to initialize projection")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -803,13 +803,21 @@ func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (Searc
|
||||
if err != nil {
|
||||
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
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user