mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 13:19:21 +00:00
perf: improve org and org domain creation (#10232)
# Which Problems Are Solved When an organization domain is verified, e.g. also when creating a new organization (incl. generated domain), existing usernames are checked if the domain has been claimed. The query was not optimized for instances with many users and organizations. # How the Problems Are Solved - Replace the query, which was searching over the users projection with (computed loginnames) with a dedicated query checking the loginnames projection directly. - All occurrences have been updated to use the new query. # Additional Changes None # Additional Context - reported through support - requires backport to v3.x
This commit is contained in:
@@ -9,7 +9,6 @@ import (
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
)
|
||||
|
||||
@@ -104,17 +103,5 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (*
|
||||
}
|
||||
|
||||
func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain string) ([]string, error) {
|
||||
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs := make([]string, len(users.Users))
|
||||
for i, user := range users.Users {
|
||||
userIDs[i] = user.ID
|
||||
}
|
||||
return userIDs, nil
|
||||
return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, "")
|
||||
}
|
||||
|
@@ -316,28 +316,7 @@ func (s *Server) RemoveOrgMember(ctx context.Context, req *mgmt_pb.RemoveOrgMemb
|
||||
}
|
||||
|
||||
func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) {
|
||||
queries := make([]query.SearchQuery, 0, 2)
|
||||
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, loginName)
|
||||
if orgID != "" {
|
||||
owner, err := query.NewUserResourceOwnerSearchQuery(orgID, query.TextNotEquals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, owner)
|
||||
}
|
||||
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs := make([]string, len(users.Users))
|
||||
for i, user := range users.Users {
|
||||
userIDs[i] = user.ID
|
||||
}
|
||||
return userIDs, nil
|
||||
return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, orgID)
|
||||
}
|
||||
|
||||
func (s *Server) ListOrgMetadata(ctx context.Context, req *mgmt_pb.ListOrgMetadataRequest) (*mgmt_pb.ListOrgMetadataResponse, error) {
|
||||
|
@@ -1044,6 +1044,70 @@ func TestServer_AddOrganizationDomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_AddOrganizationDomain_ClaimDomain(t *testing.T) {
|
||||
domain := gofakeit.DomainName()
|
||||
|
||||
// create an organization, ensure it has globally unique usernames
|
||||
// and create a user with a loginname that matches the domain later on
|
||||
organization, err := Client.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{
|
||||
Name: gofakeit.AppName(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{
|
||||
OrgId: organization.GetId(),
|
||||
UserLoginMustBeDomain: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
username := gofakeit.Username() + "@" + domain
|
||||
ownUser := Instance.CreateHumanUserVerified(CTX, organization.GetId(), username, "")
|
||||
|
||||
// create another organization, ensure it has globally unique usernames
|
||||
// and create a user with a loginname that matches the domain later on
|
||||
otherOrg, err := Client.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{
|
||||
Name: gofakeit.AppName(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{
|
||||
OrgId: otherOrg.GetId(),
|
||||
UserLoginMustBeDomain: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
otherUsername := gofakeit.Username() + "@" + domain
|
||||
otherUser := Instance.CreateHumanUserVerified(CTX, otherOrg.GetId(), otherUsername, "")
|
||||
|
||||
// if we add the domain now to the first organization, it should be claimed on the second organization, resp. its user(s)
|
||||
_, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{
|
||||
OrganizationId: organization.GetId(),
|
||||
Domain: domain,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// check both users: the first one must be untouched, the second one must be updated
|
||||
users, err := Instance.Client.UserV2.ListUsers(CTX, &user.ListUsersRequest{
|
||||
Queries: []*user.SearchQuery{
|
||||
{
|
||||
Query: &user.SearchQuery_InUserIdsQuery{
|
||||
InUserIdsQuery: &user.InUserIDQuery{UserIds: []string{ownUser.GetUserId(), otherUser.GetUserId()}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, users.GetResult(), 2)
|
||||
|
||||
for _, u := range users.GetResult() {
|
||||
if u.GetUserId() == ownUser.GetUserId() {
|
||||
assert.Equal(t, username, u.GetPreferredLoginName())
|
||||
continue
|
||||
}
|
||||
if u.GetUserId() == otherUser.GetUserId() {
|
||||
assert.NotEqual(t, otherUsername, u.GetPreferredLoginName())
|
||||
assert.Contains(t, u.GetPreferredLoginName(), "@temporary.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_ListOrganizationDomains(t *testing.T) {
|
||||
domain := gofakeit.URL()
|
||||
tests := []struct {
|
||||
|
@@ -250,26 +250,5 @@ func createOrganizationRequestAdminToCommand(admin *v2beta_org.CreateOrganizatio
|
||||
}
|
||||
|
||||
func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) {
|
||||
queries := make([]query.SearchQuery, 0, 2)
|
||||
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, loginName)
|
||||
if orgID != "" {
|
||||
owner, err := query.NewUserResourceOwnerSearchQuery(orgID, query.TextNotEquals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
queries = append(queries, owner)
|
||||
}
|
||||
users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: queries}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs := make([]string, len(users.Users))
|
||||
for i, user := range users.Users {
|
||||
userIDs[i] = user.ID
|
||||
}
|
||||
return userIDs, nil
|
||||
return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, orgID)
|
||||
}
|
||||
|
@@ -178,19 +178,7 @@ func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs := make([]string, len(users.Users))
|
||||
for i, user := range users.Users {
|
||||
userIDs[i] = user.ID
|
||||
}
|
||||
return userIDs, nil
|
||||
return l.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, "")
|
||||
}
|
||||
|
||||
func setContext(ctx context.Context, resourceOwner string) context.Context {
|
||||
|
@@ -697,6 +697,35 @@ func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwn
|
||||
return isUnique, err
|
||||
}
|
||||
|
||||
//go:embed user_claimed_user_ids.sql
|
||||
var userClaimedUserIDOfOrgDomain string
|
||||
|
||||
func (q *Queries) SearchClaimedUserIDsOfOrgDomain(ctx context.Context, domain, orgID string) (userIDs []string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
err = q.client.QueryContext(ctx,
|
||||
func(rows *sql.Rows) error {
|
||||
userIDs = make([]string, 0)
|
||||
for rows.Next() {
|
||||
var userID string
|
||||
err := rows.Scan(&userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
userClaimedUserIDOfOrgDomain,
|
||||
authz.GetInstance(ctx).InstanceID(),
|
||||
"%@"+domain,
|
||||
orgID,
|
||||
)
|
||||
|
||||
return userIDs, err
|
||||
}
|
||||
|
||||
func (q *UserSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
||||
query = q.SearchRequest.toQuery(query)
|
||||
for _, q := range q.Queries {
|
||||
|
13
internal/query/user_claimed_user_ids.sql
Normal file
13
internal/query/user_claimed_user_ids.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
SELECT u.id
|
||||
FROM projections.login_names3_users u
|
||||
LEFT JOIN projections.login_names3_policies p_custom
|
||||
ON u.instance_id = p_custom.instance_id
|
||||
AND p_custom.instance_id = $1
|
||||
AND p_custom.resource_owner = u.resource_owner
|
||||
JOIN projections.login_names3_policies p_default
|
||||
ON u.instance_id = p_default.instance_id
|
||||
AND p_default.instance_id = $1 AND p_default.is_default IS TRUE
|
||||
WHERE u.instance_id = $1
|
||||
AND COALESCE(p_custom.must_be_domain, p_default.must_be_domain) = false
|
||||
AND u.user_name_lower like $2
|
||||
AND u.resource_owner <> $3;
|
Reference in New Issue
Block a user