fix(query): user performance (#6537)

* start user by id

* ignore debug bin

* use new user by id

* new sql

* fix(sql): replace STRING with text for psql compatabilit

* some changes

* fix: correct user queries

* fix tests

* unify sql statements

* use specific get user methods

* search login name case insensitive

* refactor: optimise user statements

* add index

* fix queries

* fix: correct domain segregation

* return all login names

* fix queries

* improve readability

* query should be correct now

* cleanup statements

* fix username / loginname handling

* fix: psql doesn't support create view if not exists

* fix: create pre-release

* ignore release comments

* add lower fields

* fix: always to lower

* update to latest projection

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Silvan 2023-12-08 13:14:22 +01:00 committed by GitHub
parent 94e0caa601
commit ddbea119f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 754 additions and 272 deletions

26
cmd/setup/18.go Normal file
View File

@ -0,0 +1,26 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
)
var (
//go:embed 18.sql
addLowerFieldsToLoginNames string
)
type AddLowerFieldsToLoginNames struct {
dbClient *database.DB
}
func (mig *AddLowerFieldsToLoginNames) Execute(ctx context.Context) error {
_, err := mig.dbClient.ExecContext(ctx, addLowerFieldsToLoginNames)
return err
}
func (mig *AddLowerFieldsToLoginNames) String() string {
return "18_add_lower_fields_to_login_names"
}

6
cmd/setup/18.sql Normal file
View File

@ -0,0 +1,6 @@
ALTER TABLE IF EXISTS projections.login_names3_users ADD COLUMN IF NOT EXISTS user_name_lower TEXT GENERATED ALWAYS AS (lower(user_name)) STORED;
CREATE INDEX IF NOT EXISTS login_names3_users_search ON projections.login_names3_users (instance_id, user_name_lower) INCLUDE (resource_owner);
ALTER TABLE IF EXISTS projections.login_names3_domains ADD COLUMN IF NOT EXISTS name_lower TEXT GENERATED ALWAYS AS (lower(name)) STORED;
CREATE INDEX IF NOT EXISTS login_names3_domain_search ON projections.login_names3_domains (instance_id, resource_owner, name_lower);
CREATE INDEX IF NOT EXISTS login_names3_domain_search_result ON projections.login_names3_domains (instance_id, resource_owner) INCLUDE (is_primary);

View File

@ -75,6 +75,7 @@ type Steps struct {
s15CurrentStates *CurrentProjectionState
s16UniqueConstraintsLower *UniqueConstraintToLower
s17AddOffsetToUniqueConstraints *AddOffsetToCurrentStates
s18AddLowerFieldsToLoginNames *AddLowerFieldsToLoginNames
}
type encryptionKeyConfig struct {

View File

@ -105,6 +105,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s15CurrentStates = &CurrentProjectionState{dbClient: zitadelDBClient}
steps.s16UniqueConstraintsLower = &UniqueConstraintToLower{dbClient: zitadelDBClient}
steps.s17AddOffsetToUniqueConstraints = &AddOffsetToCurrentStates{dbClient: zitadelDBClient}
steps.s18AddLowerFieldsToLoginNames = &AddLowerFieldsToLoginNames{dbClient: zitadelDBClient}
err = projection.Create(ctx, zitadelDBClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@ -154,6 +155,10 @@ func Setup(config *Config, steps *Steps, masterKey string) {
err = migration.Migrate(ctx, eventstoreClient, repeatableStep)
logging.OnError(err).Fatalf("unable to migrate repeatable step: %s", repeatableStep.String())
}
// This step is executed after the repeatable steps because it adds fields to the login_names3 projection
err = migration.Migrate(ctx, eventstoreClient, steps.s18AddLowerFieldsToLoginNames)
logging.WithFields("name", steps.s18AddLowerFieldsToLoginNames.String()).OnError(err).Fatal("migration failed")
}
func readStmt(fs embed.FS, folder, typ, filename string) (string, error) {

View File

@ -20,6 +20,7 @@ import (
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/user"
@ -27,13 +28,12 @@ import (
)
func (s *Server) getUserByID(ctx context.Context, id string) (*query.User, error) {
owner, err := query.NewUserResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID, query.TextEquals)
user, err := s.query.GetUserByID(ctx, true, id)
if err != nil {
return nil, err
}
user, err := s.query.GetUserByID(ctx, true, id, owner)
if err != nil {
return nil, err
if user.ResourceOwner != authz.GetCtxData(ctx).OrgID {
return nil, errors.ThrowNotFound(nil, "MANAG-fpo4B", "Errors.User.NotFound")
}
return user, nil
}
@ -49,11 +49,7 @@ func (s *Server) GetUserByID(ctx context.Context, req *mgmt_pb.GetUserByIDReques
}
func (s *Server) GetUserByLoginNameGlobal(ctx context.Context, req *mgmt_pb.GetUserByLoginNameGlobalRequest) (*mgmt_pb.GetUserByLoginNameGlobalResponse, error) {
loginName, err := query.NewUserPreferredLoginNameSearchQuery(req.LoginName, query.TextEquals)
if err != nil {
return nil, err
}
user, err := s.query.GetUser(ctx, true, loginName)
user, err := s.query.GetUserByLoginName(ctx, true, req.LoginName)
if err != nil {
return nil, err
}

View File

@ -474,11 +474,7 @@ func userByID(userID string) userSearch {
}
func userByLoginName(loginName string) (userSearch, error) {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
if err != nil {
return nil, err
}
return userSearchByLoginName{loginNameQuery}, nil
return userSearchByLoginName{loginName}, nil
}
type userSearchByID struct {
@ -490,9 +486,9 @@ func (u userSearchByID) search(ctx context.Context, q *query.Queries) (*query.Us
}
type userSearchByLoginName struct {
loginNameQuery query.SearchQuery
loginName string
}
func (u userSearchByLoginName) search(ctx context.Context, q *query.Queries) (*query.User, error) {
return q.GetUser(ctx, true, u.loginNameQuery)
return q.GetUserByLoginName(ctx, true, u.loginName)
}

View File

@ -586,12 +586,6 @@ func Test_sessionQueryToQuery(t *testing.T) {
}
}
func mustUserLoginNamesSearchQuery(t testing.TB, value string) query.SearchQuery {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery("bar")
require.NoError(t, err)
return loginNameQuery
}
func Test_userCheck(t *testing.T) {
type args struct {
user *session.CheckUser
@ -623,7 +617,7 @@ func Test_userCheck(t *testing.T) {
LoginName: "bar",
},
}},
want: userSearchByLoginName{mustUserLoginNamesSearchQuery(t, "bar")},
want: userSearchByLoginName{"bar"},
},
{
name: "unimplemented error",

View File

@ -197,11 +197,7 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
}
func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (op.TokenRequest, error) {
loginname, err := query.NewUserLoginNamesSearchQuery(clientID)
if err != nil {
return nil, err
}
user, err := o.query.GetUser(ctx, false, loginname)
user, err := o.query.GetUserByLoginName(ctx, false, clientID)
if err != nil {
return nil, err
}

View File

@ -34,11 +34,7 @@ func (c *clientCredentialsRequest) GetScopes() []string {
}
func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) {
searchQuery, err := query.NewUserLoginNamesSearchQuery(clientID)
if err != nil {
return nil, err
}
user, err := s.query.GetUser(ctx, false, searchQuery)
user, err := s.query.GetUserByLoginName(ctx, false, clientID)
if errors.IsNotFound(err) {
return nil, oidc.ErrInvalidClient().WithParent(err).WithDescription("client not found")
}

View File

@ -159,11 +159,7 @@ func (p *Storage) SetUserinfoWithLoginName(ctx context.Context, userinfo models.
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
loginNameSQ, err := query.NewUserLoginNamesSearchQuery(loginName)
if err != nil {
return err
}
user, err := p.query.GetUser(ctx, true, loginNameSQ)
user, err := p.query.GetUserByLoginName(ctx, true, loginName)
if err != nil {
return err
}

View File

@ -7,7 +7,6 @@ import (
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
)
const (
@ -91,7 +90,7 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe
if authReq != nil {
userOrg = authReq.UserOrgID
}
loginName, err := query.NewUserLoginNamesSearchQuery(authReq.LoginName)
user, err := l.query.GetUserByLoginName(setContext(r.Context(), userOrg), false, authReq.LoginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
@ -101,11 +100,6 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
user, err := l.query.GetUser(setContext(r.Context(), userOrg), false, loginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), user.ID, user.ResourceOwner, domain.NotificationTypeEmail, passwordCodeGenerator)
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
}

View File

@ -5,7 +5,6 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query"
)
const (
@ -18,12 +17,7 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
l.renderError(w, r, authReq, err)
return
}
loginName, err := query.NewUserLoginNamesSearchQuery(authReq.LoginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
user, err := l.query.GetUser(setContext(r.Context(), authReq.UserOrgID), true, loginName)
user, err := l.query.GetUserByLoginName(setContext(r.Context(), authReq.UserOrgID), true, authReq.LoginName)
if err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames && errors.IsNotFound(err) {
err = nil

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query"
@ -21,25 +22,31 @@ func (v *View) UserByID(userID, instanceID string) (*model.UserView, error) {
}
func (v *View) UserByLoginName(ctx context.Context, loginName, instanceID string) (*model.UserView, error) {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
queriedUser, err := v.query.GetNotifyUserByLoginName(ctx, true, loginName)
if err != nil {
return nil, err
}
return v.userByID(ctx, instanceID, loginNameQuery)
//nolint: contextcheck // no lint was added because refactor would change too much code
return view.UserByID(v.Db, userTable, queriedUser.ID, instanceID)
}
func (v *View) UserByLoginNameAndResourceOwner(ctx context.Context, loginName, resourceOwner, instanceID string) (*model.UserView, error) {
loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName)
if err != nil {
return nil, err
}
resourceOwnerQuery, err := query.NewUserResourceOwnerSearchQuery(resourceOwner, query.TextEquals)
queriedUser, err := v.query.GetNotifyUserByLoginName(ctx, true, loginName)
if err != nil {
return nil, err
}
return v.userByID(ctx, instanceID, loginNameQuery, resourceOwnerQuery)
//nolint: contextcheck // no lint was added because refactor would change too much code
user, err := view.UserByID(v.Db, userTable, queriedUser.ID, instanceID)
if err != nil {
return nil, err
}
if user.ResourceOwner != resourceOwner {
return nil, errors.ThrowNotFound(nil, "VIEW-qScmi", "Errors.User.NotFound")
}
return user, nil
}
func (v *View) UserByEmail(ctx context.Context, email, instanceID string) (*model.UserView, error) {

View File

@ -101,23 +101,18 @@ func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(arg0 any) *gomock.Cal
}
// GetNotifyUserByID mocks base method.
func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string, arg3 ...query.SearchQuery) (*query.NotifyUser, error) {
func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string) (*query.NotifyUser, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetNotifyUserByID", varargs...)
ret := m.ctrl.Call(m, "GetNotifyUserByID", arg0, arg1, arg2)
ret0, _ := ret[0].(*query.NotifyUser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetNotifyUserByID indicates an expected call of GetNotifyUserByID.
func (mr *MockQueriesMockRecorder) GetNotifyUserByID(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
func (mr *MockQueriesMockRecorder) GetNotifyUserByID(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1, arg2}, arg3...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), varargs...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), arg0, arg1, arg2)
}
// MailTemplateByOrg mocks base method.

View File

@ -13,7 +13,7 @@ import (
type Queries interface {
ActiveLabelPolicyByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.LabelPolicy, error)
MailTemplateByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.MailTemplate, error)
GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string, queries ...query.SearchQuery) (*query.NotifyUser, error)
GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (*query.NotifyUser, error)
CustomTextListByTemplate(ctx context.Context, aggregateID, template string, withOwnerRemoved bool) (*query.CustomTexts, error)
SearchInstanceDomains(ctx context.Context, queries *query.InstanceDomainSearchQueries) (*query.InstanceDomains, error)
SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string) (*query.Session, error)

View File

@ -17,6 +17,7 @@ import (
)
const (
// if the table name of the users or domains table is changed please update setup step 18
LoginNameTableAlias = "login_names3"
LoginNameProjectionTable = "projections." + LoginNameTableAlias
LoginNameUserProjectionTable = LoginNameProjectionTable + "_" + loginNameUserSuffix
@ -35,14 +36,17 @@ const (
domainsAlias = "domains"
domainAlias = "domain"
loginNameUserSuffix = "users"
LoginNameUserIDCol = "id"
LoginNameUserUserNameCol = "user_name"
loginNameUserSuffix = "users"
LoginNameUserIDCol = "id"
LoginNameUserUserNameCol = "user_name"
// internal fields for faster search
loginNameUserUserNameLowerCol = "user_name_lower"
LoginNameUserResourceOwnerCol = "resource_owner"
LoginNameUserInstanceIDCol = "instance_id"
loginNameDomainSuffix = "domains"
LoginNameDomainNameCol = "name"
loginNameDomainNameLowerCol = "name_lower"
LoginNameDomainIsPrimaryCol = "is_primary"
LoginNameDomainResourceOwnerCol = "resource_owner"
LoginNameDomainInstanceIDCol = "instance_id"
@ -171,12 +175,16 @@ 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(LoginNameUserResourceOwnerCol, handler.ColumnTypeText),
handler.NewColumn(LoginNameUserInstanceIDCol, handler.ColumnTypeText),
},
handler.NewPrimaryKey(LoginNameUserInstanceIDCol, LoginNameUserIDCol),
loginNameUserSuffix,
handler.WithIndex(handler.NewIndex("resource_owner", []string{LoginNameUserResourceOwnerCol})),
handler.WithIndex(handler.NewIndex("instance_user_name", []string{LoginNameUserInstanceIDCol, LoginNameUserUserNameCol},
handler.WithInclude(LoginNameUserResourceOwnerCol),
)),
handler.WithIndex(
handler.NewIndex("lnu_instance_ro_id", []string{LoginNameUserInstanceIDCol, LoginNameUserResourceOwnerCol, LoginNameUserIDCol},
handler.WithInclude(
@ -184,16 +192,33 @@ 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.NewSuffixedTable(
[]*handler.InitColumn{
handler.NewColumn(LoginNameDomainNameCol, handler.ColumnTypeText),
// TODO: implement computed columns
// handler.NewComputedColumn(loginNameDomainNameLowerCol, handler.ColumnTypeText),
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.NewSuffixedTable(
[]*handler.InitColumn{

View File

@ -3,6 +3,7 @@ package query
import (
"context"
"database/sql"
_ "embed"
errs "errors"
"strings"
"time"
@ -314,7 +315,10 @@ var (
}
)
func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userID string, queries ...SearchQuery) (user *User, err error) {
//go:embed user_by_id.sql
var userByIDQuery string
func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userID string) (user *User, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -322,26 +326,55 @@ func (q *Queries) GetUserByID(ctx context.Context, shouldTriggerBulk bool, userI
triggerUserProjections(ctx)
}
query, scan := prepareUserQuery(ctx, q.client)
for _, q := range queries {
query = q.toQuery(query)
}
eq := sq.Eq{
UserIDCol.identifier(): userID,
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-FBg21", "Errors.Query.SQLStatment")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
user, err = scan(row)
return err
}, stmt, args...)
err = q.client.QueryRowContext(ctx,
func(row *sql.Row) error {
user, err = scanUser(row)
return err
},
userByIDQuery,
userID,
authz.GetInstance(ctx).InstanceID(),
)
return user, err
}
//go:embed user_by_login_name.sql
var userByLoginNameQuery string
func (q *Queries) GetUserByLoginName(ctx context.Context, shouldTriggered bool, loginName string) (user *User, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggered {
triggerUserProjections(ctx)
}
loginName = strings.ToLower(loginName)
username := loginName
domainIndex := strings.LastIndex(loginName, "@")
var domainSuffix string
// split between the last @ (so ignore it if the login name ends with it)
if domainIndex > 0 && domainIndex != len(loginName)-1 {
domainSuffix = loginName[domainIndex+1:]
username = loginName[:domainIndex]
}
err = q.client.QueryRowContext(ctx,
func(row *sql.Row) error {
user, err = scanUser(row)
return err
},
userByLoginNameQuery,
username,
domainSuffix,
loginName,
authz.GetInstance(ctx).InstanceID(),
)
return user, err
}
// Deprecated: use either GetUserByID or GetUserByLoginName
func (q *Queries) GetUser(ctx context.Context, shouldTriggerBulk bool, queries ...SearchQuery) (user *User, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -441,7 +474,10 @@ func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...S
return phone, err
}
func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string, queries ...SearchQuery) (user *NotifyUser, err error) {
//go:embed user_notify_by_id.sql
var notifyUserByIDQuery string
func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (user *NotifyUser, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -449,23 +485,51 @@ func (q *Queries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, u
triggerUserProjections(ctx)
}
query, scan := prepareNotifyUserQuery(ctx, q.client)
for _, q := range queries {
query = q.toQuery(query)
}
eq := sq.Eq{
UserIDCol.identifier(): userID,
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
stmt, args, err := query.Where(eq).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-Err3g", "Errors.Query.SQLStatment")
err = q.client.QueryRowContext(ctx,
func(row *sql.Row) error {
user, err = scanNotifyUser(row)
return err
},
notifyUserByIDQuery,
userID,
authz.GetInstance(ctx).InstanceID(),
)
return user, err
}
//go:embed user_notify_by_login_name.sql
var notifyUserByLoginNameQuery string
func (q *Queries) GetNotifyUserByLoginName(ctx context.Context, shouldTriggered bool, loginName string) (user *NotifyUser, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if shouldTriggered {
triggerUserProjections(ctx)
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
user, err = scan(row)
return err
}, stmt, args...)
loginName = strings.ToLower(loginName)
username := loginName
domainIndex := strings.LastIndex(loginName, "@")
var domainSuffix string
// split between the last @ (so ignore it if the login name ends with it)
if domainIndex > 0 && domainIndex != len(loginName)-1 {
domainSuffix = loginName[domainIndex+1:]
username = loginName[:domainIndex]
}
err = q.client.QueryRowContext(ctx,
func(row *sql.Row) error {
user, err = scanNotifyUser(row)
return err
},
notifyUserByLoginNameQuery,
username,
domainSuffix,
loginName,
authz.GetInstance(ctx).InstanceID(),
)
return user, err
}
@ -706,6 +770,95 @@ func preparePreferredLoginNamesQuery() (string, []interface{}, error) {
).ToSql()
}
func scanUser(row *sql.Row) (*User, error) {
u := new(User)
var count int
preferredLoginName := sql.NullString{}
humanID := sql.NullString{}
firstName := sql.NullString{}
lastName := sql.NullString{}
nickName := sql.NullString{}
displayName := sql.NullString{}
preferredLanguage := sql.NullString{}
gender := sql.NullInt32{}
avatarKey := sql.NullString{}
email := sql.NullString{}
isEmailVerified := sql.NullBool{}
phone := sql.NullString{}
isPhoneVerified := sql.NullBool{}
machineID := sql.NullString{}
name := sql.NullString{}
description := sql.NullString{}
var secret *crypto.CryptoValue
accessTokenType := sql.NullInt32{}
err := row.Scan(
&u.ID,
&u.CreationDate,
&u.ChangeDate,
&u.ResourceOwner,
&u.Sequence,
&u.State,
&u.Type,
&u.Username,
&u.LoginNames,
&preferredLoginName,
&humanID,
&firstName,
&lastName,
&nickName,
&displayName,
&preferredLanguage,
&gender,
&avatarKey,
&email,
&isEmailVerified,
&phone,
&isPhoneVerified,
&machineID,
&name,
&description,
&secret,
&accessTokenType,
&count,
)
if err != nil || count != 1 {
if errs.Is(err, sql.ErrNoRows) || count != 1 {
return nil, errors.ThrowNotFound(err, "QUERY-Dfbg2", "Errors.User.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-Bgah2", "Errors.Internal")
}
u.PreferredLoginName = preferredLoginName.String
if humanID.Valid {
u.Human = &Human{
FirstName: firstName.String,
LastName: lastName.String,
NickName: nickName.String,
DisplayName: displayName.String,
AvatarKey: avatarKey.String,
PreferredLanguage: language.Make(preferredLanguage.String),
Gender: domain.Gender(gender.Int32),
Email: domain.EmailAddress(email.String),
IsEmailVerified: isEmailVerified.Bool,
Phone: domain.PhoneNumber(phone.String),
IsPhoneVerified: isPhoneVerified.Bool,
}
} else if machineID.Valid {
u.Machine = &Machine{
Name: name.String,
Description: description.String,
Secret: secret,
AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32),
}
}
return u, nil
}
func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*User, error)) {
loginNamesQuery, loginNamesArgs, err := prepareLoginNamesQuery()
if err != nil {
@ -757,94 +910,7 @@ func prepareUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder
userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)),
preferredLoginNameArgs...).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*User, error) {
u := new(User)
var count int
preferredLoginName := sql.NullString{}
humanID := sql.NullString{}
firstName := sql.NullString{}
lastName := sql.NullString{}
nickName := sql.NullString{}
displayName := sql.NullString{}
preferredLanguage := sql.NullString{}
gender := sql.NullInt32{}
avatarKey := sql.NullString{}
email := sql.NullString{}
isEmailVerified := sql.NullBool{}
phone := sql.NullString{}
isPhoneVerified := sql.NullBool{}
machineID := sql.NullString{}
name := sql.NullString{}
description := sql.NullString{}
var secret *crypto.CryptoValue
accessTokenType := sql.NullInt32{}
err := row.Scan(
&u.ID,
&u.CreationDate,
&u.ChangeDate,
&u.ResourceOwner,
&u.Sequence,
&u.State,
&u.Type,
&u.Username,
&u.LoginNames,
&preferredLoginName,
&humanID,
&firstName,
&lastName,
&nickName,
&displayName,
&preferredLanguage,
&gender,
&avatarKey,
&email,
&isEmailVerified,
&phone,
&isPhoneVerified,
&machineID,
&name,
&description,
&secret,
&accessTokenType,
&count,
)
if err != nil || count != 1 {
if errs.Is(err, sql.ErrNoRows) || count != 1 {
return nil, errors.ThrowNotFound(err, "QUERY-Dfbg2", "Errors.User.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-Bgah2", "Errors.Internal")
}
u.PreferredLoginName = preferredLoginName.String
if humanID.Valid {
u.Human = &Human{
FirstName: firstName.String,
LastName: lastName.String,
NickName: nickName.String,
DisplayName: displayName.String,
AvatarKey: avatarKey.String,
PreferredLanguage: language.Make(preferredLanguage.String),
Gender: domain.Gender(gender.Int32),
Email: domain.EmailAddress(email.String),
IsEmailVerified: isEmailVerified.Bool,
Phone: domain.PhoneNumber(phone.String),
IsPhoneVerified: isPhoneVerified.Bool,
}
} else if machineID.Valid {
u.Machine = &Machine{
Name: name.String,
Description: description.String,
Secret: secret,
AccessTokenType: domain.OIDCTokenType(accessTokenType.Int32),
}
}
return u, nil
}
scanUser
}
func prepareProfileQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) {
@ -1055,88 +1121,90 @@ func prepareNotifyUserQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
userPreferredLoginNameInstanceIDCol.identifier()+" = "+UserInstanceIDCol.identifier()+db.Timetravel(call.Took(ctx)),
preferredLoginNameArgs...).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*NotifyUser, error) {
u := new(NotifyUser)
var count int
loginNames := database.TextArray[string]{}
preferredLoginName := sql.NullString{}
scanNotifyUser
}
humanID := sql.NullString{}
firstName := sql.NullString{}
lastName := sql.NullString{}
nickName := sql.NullString{}
displayName := sql.NullString{}
preferredLanguage := sql.NullString{}
gender := sql.NullInt32{}
avatarKey := sql.NullString{}
func scanNotifyUser(row *sql.Row) (*NotifyUser, error) {
u := new(NotifyUser)
var count int
loginNames := database.TextArray[string]{}
preferredLoginName := sql.NullString{}
notifyUserID := sql.NullString{}
notifyEmail := sql.NullString{}
notifyVerifiedEmail := sql.NullString{}
notifyPhone := sql.NullString{}
notifyVerifiedPhone := sql.NullString{}
notifyPasswordSet := sql.NullBool{}
humanID := sql.NullString{}
firstName := sql.NullString{}
lastName := sql.NullString{}
nickName := sql.NullString{}
displayName := sql.NullString{}
preferredLanguage := sql.NullString{}
gender := sql.NullInt32{}
avatarKey := sql.NullString{}
err := row.Scan(
&u.ID,
&u.CreationDate,
&u.ChangeDate,
&u.ResourceOwner,
&u.Sequence,
&u.State,
&u.Type,
&u.Username,
&loginNames,
&preferredLoginName,
&humanID,
&firstName,
&lastName,
&nickName,
&displayName,
&preferredLanguage,
&gender,
&avatarKey,
&notifyUserID,
&notifyEmail,
&notifyVerifiedEmail,
&notifyPhone,
&notifyVerifiedPhone,
&notifyPasswordSet,
&count,
)
notifyUserID := sql.NullString{}
notifyEmail := sql.NullString{}
notifyVerifiedEmail := sql.NullString{}
notifyPhone := sql.NullString{}
notifyVerifiedPhone := sql.NullString{}
notifyPasswordSet := sql.NullBool{}
if err != nil || count != 1 {
if errs.Is(err, sql.ErrNoRows) || count != 1 {
return nil, errors.ThrowNotFound(err, "QUERY-Dgqd2", "Errors.User.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-Dbwsg", "Errors.Internal")
}
err := row.Scan(
&u.ID,
&u.CreationDate,
&u.ChangeDate,
&u.ResourceOwner,
&u.Sequence,
&u.State,
&u.Type,
&u.Username,
&loginNames,
&preferredLoginName,
&humanID,
&firstName,
&lastName,
&nickName,
&displayName,
&preferredLanguage,
&gender,
&avatarKey,
&notifyUserID,
&notifyEmail,
&notifyVerifiedEmail,
&notifyPhone,
&notifyVerifiedPhone,
&notifyPasswordSet,
&count,
)
if !notifyUserID.Valid {
return nil, errors.ThrowPreconditionFailed(nil, "QUERY-Sfw3f", "Errors.User.NotFound")
}
u.LoginNames = loginNames
if preferredLoginName.Valid {
u.PreferredLoginName = preferredLoginName.String
}
if humanID.Valid {
u.FirstName = firstName.String
u.LastName = lastName.String
u.NickName = nickName.String
u.DisplayName = displayName.String
u.AvatarKey = avatarKey.String
u.PreferredLanguage = language.Make(preferredLanguage.String)
u.Gender = domain.Gender(gender.Int32)
}
u.LastEmail = notifyEmail.String
u.VerifiedEmail = notifyVerifiedEmail.String
u.LastPhone = notifyPhone.String
u.VerifiedPhone = notifyVerifiedPhone.String
u.PasswordSet = notifyPasswordSet.Bool
return u, nil
if err != nil || count != 1 {
if errs.Is(err, sql.ErrNoRows) || count != 1 {
return nil, errors.ThrowNotFound(err, "QUERY-Dgqd2", "Errors.User.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-Dbwsg", "Errors.Internal")
}
if !notifyUserID.Valid {
return nil, errors.ThrowPreconditionFailed(nil, "QUERY-Sfw3f", "Errors.User.NotFound")
}
u.LoginNames = loginNames
if preferredLoginName.Valid {
u.PreferredLoginName = preferredLoginName.String
}
if humanID.Valid {
u.FirstName = firstName.String
u.LastName = lastName.String
u.NickName = nickName.String
u.DisplayName = displayName.String
u.AvatarKey = avatarKey.String
u.PreferredLanguage = language.Make(preferredLanguage.String)
u.Gender = domain.Gender(gender.Int32)
}
u.LastEmail = notifyEmail.String
u.VerifiedEmail = notifyVerifiedEmail.String
u.LastPhone = notifyPhone.String
u.VerifiedPhone = notifyVerifiedPhone.String
u.PasswordSet = notifyPasswordSet.Bool
return u, nil
}
func prepareUserUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (bool, error)) {

View File

@ -0,0 +1,81 @@
WITH login_names AS (SELECT
u.id user_id
, u.instance_id
, u.resource_owner
, u.user_name
, d.name domain_name
, d.is_primary
, p.must_be_domain
, CASE WHEN p.must_be_domain
THEN concat(u.user_name, '@', d.name)
ELSE u.user_name
END login_name
FROM
projections.login_names3_users u
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
u.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $2)
OR (p.instance_id = $2 AND p.resource_owner = u.resource_owner)
)
AND
u.id = $1
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
u.instance_id = d.instance_id
AND u.resource_owner = d.resource_owner
)
SELECT
u.id
, u.creation_date
, u.change_date
, u.resource_owner
, u.sequence
, u.state
, u.type
, u.username
, (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name
, h.user_id
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.avatar_key
, h.email
, h.is_email_verified
, h.phone
, h.is_phone_verified
, m.user_id
, m.name
, m.description
, m.secret
, m.access_token_type
, count(*) OVER ()
FROM projections.users10 u
LEFT JOIN
projections.users10_humans h
ON
u.id = h.user_id
AND u.instance_id = h.instance_id
LEFT JOIN
projections.users10_machines m
ON
u.id = m.user_id
AND u.instance_id = m.instance_id
WHERE
u.id = $1
AND u.instance_id = $2
LIMIT 1
;

View File

@ -0,0 +1,115 @@
WITH found_users AS (
SELECT DISTINCT
u.id
, u.instance_id
, u.resource_owner
, u.user_name
FROM
projections.login_names3_users u
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
u.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $4)
OR (p.instance_id = $4 AND p.resource_owner = u.resource_owner)
)
AND (
(p.must_be_domain IS TRUE AND user_name_lower = $1)
OR (p.must_be_domain IS FALSE AND user_name_lower = $3)
)
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
u.instance_id = d.instance_id
AND u.resource_owner = d.resource_owner
AND CASE WHEN p.must_be_domain THEN d.name_lower = $2 ELSE TRUE END
),
login_names AS (SELECT
fu.id user_id
, fu.instance_id
, fu.resource_owner
, fu.user_name
, d.name domain_name
, d.is_primary
, p.must_be_domain
, CASE WHEN p.must_be_domain
THEN concat(fu.user_name, '@', d.name)
ELSE fu.user_name
END login_name
FROM
found_users fu
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
fu.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $4)
OR (p.instance_id = $4 AND p.resource_owner = fu.resource_owner)
)
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
fu.instance_id = d.instance_id
AND fu.resource_owner = d.resource_owner
)
SELECT
u.id
, u.creation_date
, u.change_date
, u.resource_owner
, u.sequence
, u.state
, u.type
, u.username
, (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln WHERE fu.id = ln.user_id GROUP BY ln.user_id, ln.instance_id) login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE fu.id = ln.user_id AND ln.is_primary IS TRUE) preferred_login_name
, h.user_id
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.avatar_key
, h.email
, h.is_email_verified
, h.phone
, h.is_phone_verified
, m.user_id
, m.name
, m.description
, m.secret
, m.access_token_type
, count(*) OVER ()
FROM found_users fu
JOIN
projections.users10 u
ON
fu.id = u.id
AND fu.instance_id = u.instance_id
LEFT JOIN
projections.users10_humans h
ON
fu.id = h.user_id
AND fu.instance_id = h.instance_id
LEFT JOIN
projections.users10_machines m
ON
fu.id = m.user_id
AND fu.instance_id = m.instance_id
WHERE
u.instance_id = $4
;

View File

@ -0,0 +1,79 @@
WITH login_names AS (
SELECT
u.id user_id
, u.instance_id
, u.resource_owner
, u.user_name
, d.name domain_name
, d.is_primary
, p.must_be_domain
, CASE WHEN p.must_be_domain
THEN concat(u.user_name, '@', d.name)
ELSE u.user_name
END login_name
FROM
projections.login_names3_users u
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
u.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $2)
OR (p.instance_id = $2 AND p.resource_owner = u.resource_owner)
)
AND
u.id = $1
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
u.instance_id = d.instance_id
AND u.resource_owner = d.resource_owner
)
SELECT
u.id
, u.creation_date
, u.change_date
, u.resource_owner
, u.sequence
, u.state
, u.type
, u.username
, (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln GROUP BY ln.user_id, ln.instance_id) login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE ln.is_primary IS TRUE) preferred_login_name
, h.user_id
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.avatar_key
, n.user_id
, n.last_email
, n.verified_email
, n.last_phone
, n.verified_phone
, n.password_set
, count(*) OVER ()
FROM projections.users10 u
LEFT JOIN
projections.users10_humans h
ON
u.id = h.user_id
AND u.instance_id = h.instance_id
LEFT JOIN
projections.users10_notifications n
ON
u.id = n.user_id
AND u.instance_id = n.instance_id
WHERE
u.id = $1
AND u.instance_id = $2
LIMIT 1
;

View File

@ -0,0 +1,112 @@
WITH found_users AS (
SELECT DISTINCT
u.id
, u.instance_id
, u.resource_owner
, u.user_name
FROM
projections.login_names3_users u
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
u.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $4)
OR (p.instance_id = $4 AND p.resource_owner = u.resource_owner)
)
AND (
(p.must_be_domain IS TRUE AND u.user_name_lower = $1)
OR (p.must_be_domain IS FALSE AND u.user_name_lower = $3)
)
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
u.instance_id = d.instance_id
AND u.resource_owner = d.resource_owner
AND CASE WHEN p.must_be_domain THEN d.name_lower = $2 ELSE TRUE END
),
login_names AS (SELECT
fu.id user_id
, fu.instance_id
, fu.resource_owner
, fu.user_name
, d.name domain_name
, d.is_primary
, p.must_be_domain
, CASE WHEN p.must_be_domain
THEN concat(fu.user_name, '@', d.name)
ELSE fu.user_name
END login_name
FROM
found_users fu
JOIN lateral (
SELECT
p.must_be_domain
FROM
projections.login_names3_policies p
WHERE
fu.instance_id = p.instance_id
AND (
(p.is_default IS TRUE AND p.instance_id = $4)
OR (p.instance_id = $4 AND p.resource_owner = fu.resource_owner)
)
ORDER BY is_default
LIMIT 1
) p ON TRUE
JOIN
projections.login_names3_domains d
ON
fu.instance_id = d.instance_id
AND fu.resource_owner = d.resource_owner
)
SELECT
u.id
, u.creation_date
, u.change_date
, u.resource_owner
, u.sequence
, u.state
, u.type
, u.username
, (SELECT array_agg(ln.login_name)::TEXT[] login_names FROM login_names ln WHERE fu.id = ln.user_id GROUP BY ln.user_id, ln.instance_id) login_names
, (SELECT ln.login_name login_names_lower FROM login_names ln WHERE fu.id = ln.user_id AND ln.is_primary IS TRUE) preferred_login_name
, h.user_id
, h.first_name
, h.last_name
, h.nick_name
, h.display_name
, h.preferred_language
, h.gender
, h.avatar_key
, n.user_id
, n.last_email
, n.verified_email
, n.last_phone
, n.verified_phone
, n.password_set
, count(*) OVER ()
FROM found_users fu
JOIN
projections.users10 u
ON
fu.id = u.id
AND fu.instance_id = u.instance_id
LEFT JOIN
projections.users10_humans h
ON
fu.id = h.user_id
AND fu.instance_id = h.instance_id
LEFT JOIN
projections.users10_notifications n
ON
fu.id = n.user_id
AND fu.instance_id = n.instance_id
WHERE
u.instance_id = $4
;