diff --git a/cmd/zitadel/main.go b/cmd/zitadel/main.go index 83244266d3..7176ad16b2 100644 --- a/cmd/zitadel/main.go +++ b/cmd/zitadel/main.go @@ -234,10 +234,10 @@ func startAPI(ctx context.Context, conf *Config, verifier *internal_authz.TokenV apis.RegisterServer(ctx, management.CreateServer(command, query, managementRepo, conf.SystemDefaults, conf.Mgmt.APIDomain+"/assets/v1/")) } if *authEnabled { - apis.RegisterServer(ctx, auth.CreateServer(command, query, authRepo, conf.SystemDefaults)) + apis.RegisterServer(ctx, auth.CreateServer(command, query, authRepo, conf.SystemDefaults, conf.Auth.APIDomain+"/assets/v1/")) } if *oidcEnabled { - op := oidc.NewProvider(ctx, conf.API.OIDC, command, query, authRepo, conf.SystemDefaults.KeyConfig, *localDevMode, es, projections, keyChan) + op := oidc.NewProvider(ctx, conf.API.OIDC, command, query, authRepo, conf.SystemDefaults.KeyConfig, *localDevMode, es, projections, keyChan, conf.Mgmt.APIDomain+"/assets/v1/") apis.RegisterHandler("/oauth/v2", op.HttpHandler()) } if *assetsEnabled { diff --git a/internal/admin/repository/eventsourcing/eventstore/user.go b/internal/admin/repository/eventsourcing/eventstore/user.go deleted file mode 100644 index b9b13ef05f..0000000000 --- a/internal/admin/repository/eventsourcing/eventstore/user.go +++ /dev/null @@ -1,45 +0,0 @@ -package eventstore - -import ( - "context" - - "github.com/caos/logging" - - "github.com/caos/zitadel/internal/admin/repository/eventsourcing/view" - "github.com/caos/zitadel/internal/config/systemdefaults" - "github.com/caos/zitadel/internal/eventstore/v1" - "github.com/caos/zitadel/internal/user/model" - usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model" -) - -type UserRepo struct { - SearchLimit uint64 - Eventstore v1.Eventstore - View *view.View - SystemDefaults systemdefaults.SystemDefaults - PrefixAvatarURL string -} - -func (repo *UserRepo) Health(ctx context.Context) error { - return repo.Eventstore.Health(ctx) -} - -func (repo *UserRepo) SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error) { - sequence, sequenceErr := repo.View.GetLatestUserSequence() - logging.Log("EVENT-Gdbgfw").OnError(sequenceErr).Warn("could not read latest user sequence") - users, count, err := repo.View.SearchUsers(request) - if err != nil { - return nil, err - } - result := &model.UserSearchResponse{ - Offset: request.Offset, - Limit: request.Limit, - TotalResult: count, - Result: usr_view_model.UsersToModel(users, repo.PrefixAvatarURL), - } - if sequenceErr == nil { - result.Sequence = sequence.CurrentSequence - result.Timestamp = sequence.LastSuccessfulSpoolerRun - } - return result, nil -} diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go index 4ed8edcb7a..28751489bd 100644 --- a/internal/admin/repository/eventsourcing/handler/handler.go +++ b/internal/admin/repository/eventsourcing/handler/handler.go @@ -32,11 +32,7 @@ func (h *handler) Eventstore() v1.Eventstore { } func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es v1.Eventstore, defaults systemdefaults.SystemDefaults, command *command.Commands, static static.Storage, localDevMode bool) []query.Handler { - handlers := []query.Handler{ - newUser( - handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es}, - defaults), - } + handlers := []query.Handler{} if static != nil { handlers = append(handlers, newStyling( handler{view, bulkLimit, configs.cycleDuration("Styling"), errorCount, es}, diff --git a/internal/admin/repository/eventsourcing/handler/user.go b/internal/admin/repository/eventsourcing/handler/user.go deleted file mode 100644 index 9498955192..0000000000 --- a/internal/admin/repository/eventsourcing/handler/user.go +++ /dev/null @@ -1,311 +0,0 @@ -package handler - -import ( - "context" - - "github.com/caos/logging" - - "github.com/caos/zitadel/internal/config/systemdefaults" - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1" - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - iam_model "github.com/caos/zitadel/internal/iam/model" - "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" - iam_view "github.com/caos/zitadel/internal/iam/repository/view" - org_model "github.com/caos/zitadel/internal/org/model" - org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" - "github.com/caos/zitadel/internal/org/repository/view" - user_repo "github.com/caos/zitadel/internal/repository/user" - es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" - view_model "github.com/caos/zitadel/internal/user/repository/view/model" -) - -const ( - userTable = "adminapi.users" -) - -type User struct { - handler - systemDefaults systemdefaults.SystemDefaults - subscription *v1.Subscription -} - -func newUser( - handler handler, - systemDefaults systemdefaults.SystemDefaults, -) *User { - h := &User{ - handler: handler, - systemDefaults: systemDefaults, - } - - h.subscribe() - - return h -} - -func (u *User) subscribe() { - u.subscription = u.es.Subscribe(u.AggregateTypes()...) - go func() { - for event := range u.subscription.Events { - query.ReduceEvent(u, event) - } - }() -} - -func (u *User) ViewModel() string { - return userTable -} - -func (u *User) Subscription() *v1.Subscription { - return u.subscription -} - -func (u *User) AggregateTypes() []es_models.AggregateType { - return []es_models.AggregateType{es_model.UserAggregate, org_es_model.OrgAggregate} -} - -func (u *User) CurrentSequence() (uint64, error) { - sequence, err := u.view.GetLatestUserSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (u *User) EventQuery() (*es_models.SearchQuery, error) { - sequence, err := u.view.GetLatestUserSequence() - if err != nil { - return nil, err - } - return es_models.NewSearchQuery(). - AggregateTypeFilter(u.AggregateTypes()...). - LatestSequenceFilter(sequence.CurrentSequence), nil -} - -func (u *User) Reduce(event *es_models.Event) (err error) { - switch event.AggregateType { - case es_model.UserAggregate: - return u.ProcessUser(event) - case org_es_model.OrgAggregate: - return u.ProcessOrg(event) - default: - return nil - } -} - -func (u *User) ProcessUser(event *es_models.Event) (err error) { - user := new(view_model.UserView) - switch event.Type { - case es_model.UserAdded, - es_model.UserRegistered, - es_model.HumanRegistered, - es_model.MachineAdded, - es_model.HumanAdded: - err = user.AppendEvent(event) - if err != nil { - return err - } - err = u.fillLoginNames(user) - case es_model.UserProfileChanged, - es_model.UserEmailChanged, - es_model.UserEmailVerified, - es_model.UserPhoneChanged, - es_model.UserPhoneVerified, - es_model.UserPhoneRemoved, - es_model.UserAddressChanged, - es_model.UserDeactivated, - es_model.UserReactivated, - es_model.UserLocked, - es_model.UserUnlocked, - es_model.MFAOTPAdded, - es_model.MFAOTPVerified, - es_model.MFAOTPRemoved, - es_model.HumanProfileChanged, - es_model.HumanEmailChanged, - es_model.HumanEmailVerified, - es_model.HumanPhoneChanged, - es_model.HumanPhoneVerified, - es_model.HumanPhoneRemoved, - es_model.HumanAddressChanged, - es_model.HumanMFAOTPAdded, - es_model.HumanMFAOTPVerified, - es_model.HumanMFAOTPRemoved, - es_model.HumanMFAU2FTokenAdded, - es_model.HumanMFAU2FTokenVerified, - es_model.HumanMFAU2FTokenRemoved, - es_model.HumanPasswordlessTokenAdded, - es_model.HumanPasswordlessTokenVerified, - es_model.HumanPasswordlessTokenRemoved, - es_model.MachineChanged, - es_models.EventType(user_repo.HumanPasswordlessInitCodeAddedType), - es_models.EventType(user_repo.HumanPasswordlessInitCodeRequestedType): - user, err = u.view.UserByID(event.AggregateID) - if err != nil { - return err - } - err = user.AppendEvent(event) - case es_model.DomainClaimed, - es_model.UserUserNameChanged: - user, err = u.view.UserByID(event.AggregateID) - if err != nil { - return err - } - err = user.AppendEvent(event) - if err != nil { - return err - } - err = u.fillLoginNames(user) - case es_model.UserRemoved: - return u.view.DeleteUser(event.AggregateID, event) - default: - return u.view.ProcessedUserSequence(event) - } - if err != nil { - return err - } - return u.view.PutUser(user, event) -} - -func (u *User) ProcessOrg(event *es_models.Event) (err error) { - switch event.Type { - case org_es_model.OrgDomainVerified, - org_es_model.OrgDomainRemoved, - org_es_model.OrgIAMPolicyAdded, - org_es_model.OrgIAMPolicyChanged, - org_es_model.OrgIAMPolicyRemoved: - return u.fillLoginNamesOnOrgUsers(event) - case org_es_model.OrgDomainPrimarySet: - return u.fillPreferredLoginNamesOnOrgUsers(event) - default: - return u.view.ProcessedUserSequence(event) - } -} - -func (u *User) fillLoginNamesOnOrgUsers(event *es_models.Event) error { - org, err := u.getOrgByID(context.Background(), event.ResourceOwner) - if err != nil { - return err - } - policy := org.OrgIamPolicy - if policy == nil { - policy, err = u.getDefaultOrgIAMPolicy(context.Background()) - if err != nil { - return err - } - } - users, err := u.view.UsersByOrgID(event.AggregateID) - if err != nil { - return err - } - for _, user := range users { - user.SetLoginNames(policy, org.Domains) - } - return u.view.PutUsers(users, event) -} - -func (u *User) fillPreferredLoginNamesOnOrgUsers(event *es_models.Event) error { - org, err := u.getOrgByID(context.Background(), event.ResourceOwner) - if err != nil { - return err - } - policy := org.OrgIamPolicy - if policy == nil { - policy, err = u.getDefaultOrgIAMPolicy(context.Background()) - if err != nil { - return err - } - } - if !policy.UserLoginMustBeDomain { - return nil - } - users, err := u.view.UsersByOrgID(event.AggregateID) - if err != nil { - return err - } - for _, user := range users { - user.PreferredLoginName = user.GenerateLoginName(org.GetPrimaryDomain().Domain, policy.UserLoginMustBeDomain) - } - return u.view.PutUsers(users, event) -} - -func (u *User) fillLoginNames(user *view_model.UserView) (err error) { - org, err := u.getOrgByID(context.Background(), user.ResourceOwner) - if err != nil { - return err - } - policy := org.OrgIamPolicy - if policy == nil { - policy, err = u.getDefaultOrgIAMPolicy(context.Background()) - if err != nil { - return err - } - } - - user.SetLoginNames(policy, org.Domains) - user.PreferredLoginName = user.GenerateLoginName(org.GetPrimaryDomain().Domain, policy.UserLoginMustBeDomain) - return nil -} - -func (u *User) OnError(event *es_models.Event, err error) error { - logging.LogWithFields("SPOOL-vLmwQ", "id", event.AggregateID).WithError(err).Warn("something went wrong in user handler") - return spooler.HandleError(event, err, u.view.GetLatestUserFailedEvent, u.view.ProcessedUserFailedEvent, u.view.ProcessedUserSequence, u.errorCountUntilSkip) -} - -func (u *User) OnSuccess() error { - return spooler.HandleSuccess(u.view.UpdateUserSpoolerRunTimestamp) -} - -func (u *User) getOrgByID(ctx context.Context, orgID string) (*org_model.Org, error) { - query, err := view.OrgByIDQuery(orgID, 0) - if err != nil { - return nil, err - } - - esOrg := &org_es_model.Org{ - ObjectRoot: es_models.ObjectRoot{ - AggregateID: orgID, - }, - } - err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query) - if err != nil && !caos_errs.IsNotFound(err) { - return nil, err - } - if esOrg.Sequence == 0 { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-kVLb2", "Errors.Org.NotFound") - } - - return org_es_model.OrgToModel(esOrg), nil -} - -func (u *User) getIAMByID(ctx context.Context) (*iam_model.IAM, error) { - query, err := iam_view.IAMByIDQuery(domain.IAMID, 0) - if err != nil { - return nil, err - } - iam := &model.IAM{ - ObjectRoot: es_models.ObjectRoot{ - AggregateID: domain.IAMID, - }, - } - err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, iam.AppendEvents, query) - if err != nil && caos_errs.IsNotFound(err) && iam.Sequence == 0 { - return nil, err - } - return model.IAMToModel(iam), nil -} - -func (u *User) getDefaultOrgIAMPolicy(ctx context.Context) (*iam_model.OrgIAMPolicy, error) { - existingIAM, err := u.getIAMByID(ctx) - if err != nil { - return nil, err - } - if existingIAM.DefaultOrgIAMPolicy == nil { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-2Fj8s", "Errors.IAM.OrgIAMPolicy.NotExisting") - } - return existingIAM.DefaultOrgIAMPolicy, nil -} diff --git a/internal/admin/repository/eventsourcing/repository.go b/internal/admin/repository/eventsourcing/repository.go index 43ec7ad143..053b09f7ef 100644 --- a/internal/admin/repository/eventsourcing/repository.go +++ b/internal/admin/repository/eventsourcing/repository.go @@ -30,7 +30,6 @@ type EsRepository struct { spooler *es_spol.Spooler eventstore.IAMRepository eventstore.AdministratorRepo - eventstore.UserRepo } func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, static static.Storage, roles []string, localDevMode bool) (*EsRepository, error) { @@ -73,13 +72,6 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c AdministratorRepo: eventstore.AdministratorRepo{ View: view, }, - UserRepo: eventstore.UserRepo{ - Eventstore: es, - View: view, - SearchLimit: conf.SearchLimit, - SystemDefaults: systemDefaults, - PrefixAvatarURL: assetsAPI, - }, }, nil } diff --git a/internal/admin/repository/eventsourcing/view/user.go b/internal/admin/repository/eventsourcing/view/user.go deleted file mode 100644 index a0c9122793..0000000000 --- a/internal/admin/repository/eventsourcing/view/user.go +++ /dev/null @@ -1,82 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - usr_model "github.com/caos/zitadel/internal/user/model" - "github.com/caos/zitadel/internal/user/repository/view" - "github.com/caos/zitadel/internal/user/repository/view/model" - "github.com/caos/zitadel/internal/view/repository" -) - -const ( - userTable = "adminapi.users" -) - -func (v *View) UserByID(userID string) (*model.UserView, error) { - return view.UserByID(v.Db, userTable, userID) -} - -func (v *View) SearchUsers(request *usr_model.UserSearchRequest) ([]*model.UserView, uint64, error) { - return view.SearchUsers(v.Db, userTable, request) -} - -func (v *View) GetGlobalUserByLoginName(loginName string) (*model.UserView, error) { - return view.GetGlobalUserByLoginName(v.Db, userTable, loginName) -} - -func (v *View) UsersByOrgID(orgID string) ([]*model.UserView, error) { - return view.UsersByOrgID(v.Db, userTable, orgID) -} - -func (v *View) UserIDsByDomain(domain string) ([]string, error) { - return view.UserIDsByDomain(v.Db, userTable, domain) -} - -func (v *View) UserMFAs(userID string) ([]*usr_model.MultiFactor, error) { - return view.UserMFAs(v.Db, userTable, userID) -} - -func (v *View) PutUsers(user []*model.UserView, event *models.Event) error { - err := view.PutUsers(v.Db, userTable, user...) - if err != nil { - return err - } - return v.ProcessedUserSequence(event) -} - -func (v *View) PutUser(user *model.UserView, event *models.Event) error { - err := view.PutUser(v.Db, userTable, user) - if err != nil { - return err - } - return v.ProcessedUserSequence(event) -} - -func (v *View) DeleteUser(userID string, event *models.Event) error { - err := view.DeleteUser(v.Db, userTable, userID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedUserSequence(event) -} - -func (v *View) GetLatestUserSequence() (*repository.CurrentSequence, error) { - return v.latestSequence(userTable) -} - -func (v *View) ProcessedUserSequence(event *models.Event) error { - return v.saveCurrentSequence(userTable, event) -} - -func (v *View) UpdateUserSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(userTable) -} - -func (v *View) GetLatestUserFailedEvent(sequence uint64) (*repository.FailedEvent, error) { - return v.latestFailedEvent(userTable, sequence) -} - -func (v *View) ProcessedUserFailedEvent(failedEvent *repository.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/admin/repository/repository.go b/internal/admin/repository/repository.go index 8d39417778..36142985be 100644 --- a/internal/admin/repository/repository.go +++ b/internal/admin/repository/repository.go @@ -6,5 +6,4 @@ type Repository interface { Health(ctx context.Context) error IAMRepository AdministratorRepository - UserRepository } diff --git a/internal/admin/repository/user.go b/internal/admin/repository/user.go deleted file mode 100644 index bd9b5f52b0..0000000000 --- a/internal/admin/repository/user.go +++ /dev/null @@ -1,11 +0,0 @@ -package repository - -import ( - "context" - - "github.com/caos/zitadel/internal/user/model" -) - -type UserRepository interface { - SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error) -} diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 73de7ac03f..9a9daa889c 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -8,7 +8,7 @@ import ( "github.com/caos/zitadel/internal/api/grpc/object" org_grpc "github.com/caos/zitadel/internal/api/grpc/org" "github.com/caos/zitadel/internal/domain" - usr_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/query" admin_pb "github.com/caos/zitadel/pkg/grpc/admin" obj_pb "github.com/caos/zitadel/pkg/grpc/object" ) @@ -63,20 +63,19 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* } func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain string) ([]string, error) { - users, err := s.users.SearchUsers(ctx, &usr_model.UserSearchRequest{ - Queries: []*usr_model.UserSearchQuery{ - { - Key: usr_model.UserSearchKeyPreferredLoginName, - Method: domain.SearchMethodEndsWithIgnoreCase, - Value: "@" + orgDomain, - }, - }, - }) + loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase) if err != nil { return nil, err } - userIDs := make([]string, len(users.Result)) - for i, user := range users.Result { + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}) + if err != nil { + return nil, err + } + 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 diff --git a/internal/api/grpc/admin/server.go b/internal/api/grpc/admin/server.go index fcc0f8df2a..cc24ae1aeb 100644 --- a/internal/api/grpc/admin/server.go +++ b/internal/api/grpc/admin/server.go @@ -24,8 +24,6 @@ type Server struct { query *query.Queries iam repository.IAMRepository administrator repository.AdministratorRepository - repo repository.Repository - users repository.UserRepository iamDomain string assetsAPIDomain string } @@ -40,8 +38,6 @@ func CreateServer(command *command.Commands, query *query.Queries, repo reposito query: query, iam: repo, administrator: repo, - repo: repo, - users: repo, iamDomain: iamDomain, assetsAPIDomain: assetsAPIDomain, } diff --git a/internal/api/grpc/auth/email.go b/internal/api/grpc/auth/email.go index 2f5c4ef7b6..6636c64850 100644 --- a/internal/api/grpc/auth/email.go +++ b/internal/api/grpc/auth/email.go @@ -10,7 +10,7 @@ import ( ) func (s *Server) GetMyEmail(ctx context.Context, _ *auth_pb.GetMyEmailRequest) (*auth_pb.GetMyEmailResponse, error) { - email, err := s.repo.MyEmail(ctx) + email, err := s.query.GetHumanEmail(ctx, authz.GetCtxData(ctx).UserID) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/metadata_converter.go b/internal/api/grpc/auth/metadata_converter.go index aa0e06ec6d..363dfbe597 100644 --- a/internal/api/grpc/auth/metadata_converter.go +++ b/internal/api/grpc/auth/metadata_converter.go @@ -4,6 +4,7 @@ import ( "github.com/caos/zitadel/internal/api/grpc/metadata" "github.com/caos/zitadel/internal/api/grpc/object" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/pkg/grpc/auth" ) @@ -18,12 +19,18 @@ func BulkSetMetadataToDomain(req *auth.BulkSetMyMetadataRequest) []*domain.Metad return metadata } -func ListUserMetadataToDomain(req *auth.ListMyMetadataRequest) *domain.MetadataSearchRequest { +func ListUserMetadataToQuery(req *auth.ListMyMetadataRequest) (*query.UserMetadataSearchQueries, error) { offset, limit, asc := object.ListQueryToModel(req.Query) - return &domain.MetadataSearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - Queries: metadata.MetadataQueriesToModel(req.Queries), + queries, err := metadata.MetadataQueriesToQuery(req.Queries) + if err != nil { + return nil, err } + return &query.UserMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil } diff --git a/internal/api/grpc/auth/phone.go b/internal/api/grpc/auth/phone.go index e78c15d11d..dc1b1fa23a 100644 --- a/internal/api/grpc/auth/phone.go +++ b/internal/api/grpc/auth/phone.go @@ -10,7 +10,7 @@ import ( ) func (s *Server) GetMyPhone(ctx context.Context, _ *auth_pb.GetMyPhoneRequest) (*auth_pb.GetMyPhoneResponse, error) { - phone, err := s.repo.MyPhone(ctx) + phone, err := s.query.GetHumanPhone(ctx, authz.GetCtxData(ctx).UserID) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/profile.go b/internal/api/grpc/auth/profile.go index 144f7fdaf3..4cd4e5fc7c 100644 --- a/internal/api/grpc/auth/profile.go +++ b/internal/api/grpc/auth/profile.go @@ -3,18 +3,19 @@ package auth import ( "context" + "github.com/caos/zitadel/internal/api/authz" object_grpc "github.com/caos/zitadel/internal/api/grpc/object" user_grpc "github.com/caos/zitadel/internal/api/grpc/user" auth_pb "github.com/caos/zitadel/pkg/grpc/auth" ) func (s *Server) GetMyProfile(ctx context.Context, req *auth_pb.GetMyProfileRequest) (*auth_pb.GetMyProfileResponse, error) { - profile, err := s.repo.MyProfile(ctx) + profile, err := s.query.GetHumanProfile(ctx, authz.GetCtxData(ctx).UserID) if err != nil { return nil, err } return &auth_pb.GetMyProfileResponse{ - Profile: user_grpc.ProfileToPb(profile), + Profile: user_grpc.ProfileToPb(profile, s.assetsAPIDomain), Details: object_grpc.ToViewDetailsPb( profile.Sequence, profile.CreationDate, diff --git a/internal/api/grpc/auth/server.go b/internal/api/grpc/auth/server.go index 96c03109cc..b027ef797f 100644 --- a/internal/api/grpc/auth/server.go +++ b/internal/api/grpc/auth/server.go @@ -21,22 +21,24 @@ const ( type Server struct { auth.UnimplementedAuthServiceServer - command *command.Commands - query *query.Queries - repo repository.Repository - defaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + repo repository.Repository + defaults systemdefaults.SystemDefaults + assetsAPIDomain string } type Config struct { Repository eventsourcing.Config } -func CreateServer(command *command.Commands, query *query.Queries, authRepo repository.Repository, defaults systemdefaults.SystemDefaults) *Server { +func CreateServer(command *command.Commands, query *query.Queries, authRepo repository.Repository, defaults systemdefaults.SystemDefaults, assetsAPIDomain string) *Server { return &Server{ - command: command, - query: query, - repo: authRepo, - defaults: defaults, + command: command, + query: query, + repo: authRepo, + defaults: defaults, + assetsAPIDomain: assetsAPIDomain, } } diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index bf5dd92745..24a7e6ea0f 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -17,11 +17,11 @@ import ( ) func (s *Server) GetMyUser(ctx context.Context, _ *auth_pb.GetMyUserRequest) (*auth_pb.GetMyUserResponse, error) { - user, err := s.repo.MyUser(ctx) + user, err := s.query.GetUserByID(ctx, authz.GetCtxData(ctx).UserID) if err != nil { return nil, err } - return &auth_pb.GetMyUserResponse{User: user_grpc.UserToPb(user)}, nil + return &auth_pb.GetMyUserResponse{User: user_grpc.UserToPb(user, s.assetsAPIDomain)}, nil } func (s *Server) RemoveMyUser(ctx context.Context, _ *auth_pb.RemoveMyUserRequest) (*auth_pb.RemoveMyUserResponse, error) { @@ -66,14 +66,18 @@ func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserC } func (s *Server) ListMyMetadata(ctx context.Context, req *auth_pb.ListMyMetadataRequest) (*auth_pb.ListMyMetadataResponse, error) { - res, err := s.repo.SearchMyMetadata(ctx, ListUserMetadataToDomain(req)) + queries, err := ListUserMetadataToQuery(req) + if err != nil { + return nil, err + } + res, err := s.query.SearchUserMetadata(ctx, authz.GetCtxData(ctx).UserID, queries) if err != nil { return nil, err } return &auth_pb.ListMyMetadataResponse{ - Result: metadata.MetadataListToPb(res.Result), + Result: metadata.MetadataListToPb(res.Metadata), Details: obj_grpc.ToListDetails( - res.TotalResult, + res.Count, res.Sequence, res.Timestamp, ), @@ -81,7 +85,7 @@ func (s *Server) ListMyMetadata(ctx context.Context, req *auth_pb.ListMyMetadata } func (s *Server) GetMyMetadata(ctx context.Context, req *auth_pb.GetMyMetadataRequest) (*auth_pb.GetMyMetadataResponse, error) { - data, err := s.repo.GetMyMetadataByKey(ctx, req.Key) + data, err := s.query.GetUserMetadataByKey(ctx, authz.GetCtxData(ctx).UserID, req.Key) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index 0abda8ee47..2252486ee3 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -12,7 +12,6 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/query" - usr_model "github.com/caos/zitadel/internal/user/model" mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" ) @@ -276,29 +275,25 @@ func (s *Server) RemoveOrgMember(ctx context.Context, req *mgmt_pb.RemoveOrgMemb } func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) { - queries := []*usr_model.UserSearchQuery{ - { - Key: usr_model.UserSearchKeyPreferredLoginName, - Method: domain.SearchMethodEndsWithIgnoreCase, - Value: "@" + orgDomain, - }, - } - if orgID != "" { - queries = append(queries, - &usr_model.UserSearchQuery{ - Key: usr_model.UserSearchKeyResourceOwner, - Method: domain.SearchMethodNotEquals, - Value: orgID, - }) - } - users, err := s.user.SearchUsers(ctx, &usr_model.UserSearchRequest{ - Queries: queries, - }, false) + queries := make([]query.SearchQuery, 0, 2) + loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+orgDomain, query.TextEndsWithIgnoreCase) if err != nil { return nil, err } - userIDs := make([]string, len(users.Result)) - for i, user := range users.Result { + 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}) + 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 diff --git a/internal/api/grpc/management/server.go b/internal/api/grpc/management/server.go index 548a579563..ddf8c8ce44 100644 --- a/internal/api/grpc/management/server.go +++ b/internal/api/grpc/management/server.go @@ -27,7 +27,6 @@ type Server struct { org repository.OrgRepository user repository.UserRepository iam repository.IamRepository - authZ authz.Config systemDefaults systemdefaults.SystemDefaults assetAPIPrefix string } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 9f04a8cda3..3231143532 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -18,35 +18,51 @@ import ( ) func (s *Server) GetUserByID(ctx context.Context, req *mgmt_pb.GetUserByIDRequest) (*mgmt_pb.GetUserByIDResponse, error) { - user, err := s.user.UserByIDAndResourceOwner(ctx, req.Id, authz.GetCtxData(ctx).OrgID) + owner, err := query.NewUserResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID, query.TextEquals) + if err != nil { + return nil, err + } + user, err := s.query.GetUserByID(ctx, req.Id, owner) if err != nil { return nil, err } return &mgmt_pb.GetUserByIDResponse{ - User: user_grpc.UserToPb(user), + User: user_grpc.UserToPb(user, s.assetAPIPrefix), }, nil } func (s *Server) GetUserByLoginNameGlobal(ctx context.Context, req *mgmt_pb.GetUserByLoginNameGlobalRequest) (*mgmt_pb.GetUserByLoginNameGlobalResponse, error) { - user, err := s.user.GetUserByLoginNameGlobal(ctx, req.LoginName) + loginName, err := query.NewUserPreferredLoginNameSearchQuery(req.LoginName, query.TextEquals) + if err != nil { + return nil, err + } + user, err := s.query.GetUser(ctx, loginName) if err != nil { return nil, err } return &mgmt_pb.GetUserByLoginNameGlobalResponse{ - User: user_grpc.UserToPb(user), + User: user_grpc.UserToPb(user, s.assetAPIPrefix), }, nil } func (s *Server) ListUsers(ctx context.Context, req *mgmt_pb.ListUsersRequest) (*mgmt_pb.ListUsersResponse, error) { - r := ListUsersRequestToModel(ctx, req) - res, err := s.user.SearchUsers(ctx, r, true) + queries, err := ListUsersRequestToModel(req) + if err != nil { + return nil, err + } + + err = queries.AppendMyResourceOwnerQuery(authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + res, err := s.query.SearchUsers(ctx, queries) if err != nil { return nil, err } return &mgmt_pb.ListUsersResponse{ - Result: user_grpc.UsersToPb(res.Result), + Result: user_grpc.UsersToPb(res.Users, s.assetAPIPrefix), Details: obj_grpc.ToListDetails( - res.TotalResult, + res.Count, res.Sequence, res.Timestamp, ), @@ -77,7 +93,7 @@ func (s *Server) IsUserUnique(ctx context.Context, req *mgmt_pb.IsUserUniqueRequ if !policy.UserLoginMustBeDomain { orgID = "" } - unique, err := s.user.IsUserUnique(ctx, req.UserName, req.Email, orgID) + unique, err := s.query.IsUserUnique(ctx, req.UserName, req.Email, orgID) if err != nil { return nil, err } @@ -87,14 +103,22 @@ func (s *Server) IsUserUnique(ctx context.Context, req *mgmt_pb.IsUserUniqueRequ } func (s *Server) ListUserMetadata(ctx context.Context, req *mgmt_pb.ListUserMetadataRequest) (*mgmt_pb.ListUserMetadataResponse, error) { - res, err := s.user.SearchMetadata(ctx, req.Id, authz.GetCtxData(ctx).OrgID, ListUserMetadataToDomain(req)) + metadataQueries, err := ListUserMetadataToDomain(req) + if err != nil { + return nil, err + } + err = metadataQueries.AppendMyResourceOwnerQuery(authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + res, err := s.query.SearchUserMetadata(ctx, req.Id, metadataQueries) if err != nil { return nil, err } return &mgmt_pb.ListUserMetadataResponse{ - Result: metadata.MetadataListToPb(res.Result), + Result: metadata.MetadataListToPb(res.Metadata), Details: obj_grpc.ToListDetails( - res.TotalResult, + res.Count, res.Sequence, res.Timestamp, ), @@ -102,7 +126,11 @@ func (s *Server) ListUserMetadata(ctx context.Context, req *mgmt_pb.ListUserMeta } func (s *Server) GetUserMetadata(ctx context.Context, req *mgmt_pb.GetUserMetadataRequest) (*mgmt_pb.GetUserMetadataResponse, error) { - data, err := s.user.GetMetadataByKey(ctx, req.Id, authz.GetCtxData(ctx).OrgID, req.Key) + owner, err := query.NewUserMetadataResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + data, err := s.query.GetUserMetadataByKey(ctx, req.Id, req.Key, owner) if err != nil { return nil, err } @@ -302,12 +330,16 @@ func (s *Server) UpdateUserName(ctx context.Context, req *mgmt_pb.UpdateUserName } func (s *Server) GetHumanProfile(ctx context.Context, req *mgmt_pb.GetHumanProfileRequest) (*mgmt_pb.GetHumanProfileResponse, error) { - profile, err := s.user.ProfileByID(ctx, req.UserId) + owner, err := query.NewUserResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID, query.TextEquals) + if err != nil { + return nil, err + } + profile, err := s.query.GetHumanProfile(ctx, req.UserId, owner) if err != nil { return nil, err } return &mgmt_pb.GetHumanProfileResponse{ - Profile: user_grpc.ProfileToPb(profile), + Profile: user_grpc.ProfileToPb(profile, s.assetAPIPrefix), Details: obj_grpc.ToViewDetailsPb( profile.Sequence, profile.CreationDate, @@ -332,7 +364,11 @@ func (s *Server) UpdateHumanProfile(ctx context.Context, req *mgmt_pb.UpdateHuma } func (s *Server) GetHumanEmail(ctx context.Context, req *mgmt_pb.GetHumanEmailRequest) (*mgmt_pb.GetHumanEmailResponse, error) { - email, err := s.user.EmailByID(ctx, req.UserId) + owner, err := query.NewUserResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID, query.TextEquals) + if err != nil { + return nil, err + } + email, err := s.query.GetHumanEmail(ctx, req.UserId, owner) if err != nil { return nil, err } @@ -382,7 +418,11 @@ func (s *Server) ResendHumanEmailVerification(ctx context.Context, req *mgmt_pb. } func (s *Server) GetHumanPhone(ctx context.Context, req *mgmt_pb.GetHumanPhoneRequest) (*mgmt_pb.GetHumanPhoneResponse, error) { - phone, err := s.user.PhoneByID(ctx, req.UserId) + owner, err := query.NewUserResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID, query.TextEquals) + if err != nil { + return nil, err + } + phone, err := s.query.GetHumanPhone(ctx, req.UserId, owner) if err != nil { return nil, err } @@ -514,7 +554,7 @@ func (s *Server) RemoveHumanAuthFactorU2F(ctx context.Context, req *mgmt_pb.Remo func (s *Server) ListHumanPasswordless(ctx context.Context, req *mgmt_pb.ListHumanPasswordlessRequest) (*mgmt_pb.ListHumanPasswordlessResponse, error) { query := new(query.UserAuthMethodSearchQueries) err := query.AppendUserIDQuery(req.UserId) -if err != nil { + if err != nil { return nil, err } err = query.AppendAuthMethodQuery(domain.UserAuthMethodTypePasswordless) diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 66e916aea1..8aab55a718 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -17,25 +17,22 @@ import ( "github.com/caos/zitadel/internal/query" user_model "github.com/caos/zitadel/internal/user/model" mgmt_pb "github.com/caos/zitadel/pkg/grpc/management" - user_pb "github.com/caos/zitadel/pkg/grpc/user" ) -func ListUsersRequestToModel(ctx context.Context, req *mgmt_pb.ListUsersRequest) *user_model.UserSearchRequest { +func ListUsersRequestToModel(req *mgmt_pb.ListUsersRequest) (*query.UserSearchQueries, error) { offset, limit, asc := object.ListQueryToModel(req.Query) - req.Queries = append(req.Queries, &user_pb.SearchQuery{ - Query: &user_pb.SearchQuery_ResourceOwner{ - ResourceOwner: &user_pb.ResourceOwnerQuery{ - OrgID: authz.GetCtxData(ctx).OrgID, - }, - }, - }) - - return &user_model.UserSearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - Queries: user_grpc.UserQueriesToModel(req.Queries), + queries, err := user_grpc.UserQueriesToQuery(req.Queries) + if err != nil { + return nil, err } + return &query.UserSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil } func BulkSetMetadataToDomain(req *mgmt_pb.BulkSetUserMetadataRequest) []*domain.Metadata { @@ -49,14 +46,20 @@ func BulkSetMetadataToDomain(req *mgmt_pb.BulkSetUserMetadataRequest) []*domain. return metadata } -func ListUserMetadataToDomain(req *mgmt_pb.ListUserMetadataRequest) *domain.MetadataSearchRequest { +func ListUserMetadataToDomain(req *mgmt_pb.ListUserMetadataRequest) (*query.UserMetadataSearchQueries, error) { offset, limit, asc := object.ListQueryToModel(req.Query) - return &domain.MetadataSearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, - Queries: metadata.MetadataQueriesToModel(req.Queries), + queries, err := metadata.MetadataQueriesToQuery(req.Queries) + if err != nil { + return nil, err } + return &query.UserMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil } func AddHumanUserRequestToDomain(req *mgmt_pb.AddHumanUserRequest) *domain.Human { diff --git a/internal/api/grpc/metadata/metadata.go b/internal/api/grpc/metadata/metadata.go index 4ca6d466e5..64dae5888d 100644 --- a/internal/api/grpc/metadata/metadata.go +++ b/internal/api/grpc/metadata/metadata.go @@ -2,11 +2,12 @@ package metadata import ( "github.com/caos/zitadel/internal/api/grpc/object" - "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query" meta_pb "github.com/caos/zitadel/pkg/grpc/metadata" ) -func MetadataListToPb(dataList []*domain.Metadata) []*meta_pb.Metadata { +func MetadataListToPb(dataList []*query.UserMetadata) []*meta_pb.Metadata { mds := make([]*meta_pb.Metadata, len(dataList)) for i, data := range dataList { mds[i] = DomainMetadataToPb(data) @@ -14,7 +15,7 @@ func MetadataListToPb(dataList []*domain.Metadata) []*meta_pb.Metadata { return mds } -func DomainMetadataToPb(data *domain.Metadata) *meta_pb.Metadata { +func DomainMetadataToPb(data *query.UserMetadata) *meta_pb.Metadata { return &meta_pb.Metadata{ Key: data.Key, Value: data.Value, @@ -27,27 +28,26 @@ func DomainMetadataToPb(data *domain.Metadata) *meta_pb.Metadata { } } -func MetadataQueriesToModel(queries []*meta_pb.MetadataQuery) []*domain.MetadataSearchQuery { - q := make([]*domain.MetadataSearchQuery, len(queries)) +func MetadataQueriesToQuery(queries []*meta_pb.MetadataQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) for i, query := range queries { - q[i] = MetadataQueryToModel(query) + q[i], err = MetadataQueryToQuery(query) + if err != nil { + return nil, err + } } - return q + return q, nil } -func MetadataQueryToModel(query *meta_pb.MetadataQuery) *domain.MetadataSearchQuery { +func MetadataQueryToQuery(query *meta_pb.MetadataQuery) (query.SearchQuery, error) { switch q := query.Query.(type) { case *meta_pb.MetadataQuery_KeyQuery: - return MetadataKeyQueryToModel(q.KeyQuery) + return MetadataKeyQueryToQuery(q.KeyQuery) default: - return nil + return nil, errors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") } } -func MetadataKeyQueryToModel(q *meta_pb.MetadataKeyQuery) *domain.MetadataSearchQuery { - return &domain.MetadataSearchQuery{ - Key: domain.MetadataSearchKeyKey, - Method: object.TextMethodToModel(q.Method), - Value: q.Key, - } +func MetadataKeyQueryToQuery(q *meta_pb.MetadataKeyQuery) (query.SearchQuery, error) { + return query.NewUserMetadataKeySearchQuery(q.Key, object.TextMethodToQuery(q.Method)) } diff --git a/internal/api/grpc/user/converter.go b/internal/api/grpc/user/converter.go index e4ea7684ca..ef57c0311c 100644 --- a/internal/api/grpc/user/converter.go +++ b/internal/api/grpc/user/converter.go @@ -5,26 +5,25 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/query" - "github.com/caos/zitadel/internal/user/model" usr_grant_model "github.com/caos/zitadel/internal/usergrant/model" user_pb "github.com/caos/zitadel/pkg/grpc/user" ) -func UsersToPb(users []*model.UserView) []*user_pb.User { +func UsersToPb(users []*query.User, assetPrefix string) []*user_pb.User { u := make([]*user_pb.User, len(users)) for i, user := range users { - u[i] = UserToPb(user) + u[i] = UserToPb(user, assetPrefix) } return u } -func UserToPb(user *model.UserView) *user_pb.User { +func UserToPb(user *query.User, assetPrefix string) *user_pb.User { return &user_pb.User{ Id: user.ID, - State: ModelUserStateToPb(user.State), - UserName: user.UserName, + State: UserStateToPb(user.State), + UserName: user.Username, LoginNames: user.LoginNames, PreferredLoginName: user.PreferredLoginName, - Type: UserTypeToPb(user), + Type: UserTypeToPb(user, assetPrefix), Details: object.ToViewDetailsPb( user.Sequence, user.CreationDate, @@ -34,30 +33,30 @@ func UserToPb(user *model.UserView) *user_pb.User { } } -func UserTypeToPb(user *model.UserView) user_pb.UserType { - if user.HumanView != nil { +func UserTypeToPb(user *query.User, assetPrefix string) user_pb.UserType { + if user.Human != nil { return &user_pb.User_Human{ - Human: HumanToPb(user.HumanView), + Human: HumanToPb(user.Human, assetPrefix, user.ResourceOwner), } } - if user.MachineView != nil { + if user.Machine != nil { return &user_pb.User_Machine{ - Machine: MachineToPb(user.MachineView), + Machine: MachineToPb(user.Machine), } } return nil } -func HumanToPb(view *model.HumanView) *user_pb.Human { +func HumanToPb(view *query.Human, assetPrefix, owner string) *user_pb.Human { return &user_pb.Human{ Profile: &user_pb.Profile{ FirstName: view.FirstName, LastName: view.LastName, NickName: view.NickName, DisplayName: view.DisplayName, - PreferredLanguage: view.PreferredLanguage, + PreferredLanguage: view.PreferredLanguage.String(), Gender: GenderToPb(view.Gender), - AvatarUrl: view.AvatarURL, + AvatarUrl: domain.AvatarURL(assetPrefix, owner, view.AvatarKey), }, Email: &user_pb.Email{ Email: view.Email, @@ -70,14 +69,14 @@ func HumanToPb(view *model.HumanView) *user_pb.Human { } } -func MachineToPb(view *model.MachineView) *user_pb.Machine { +func MachineToPb(view *query.Machine) *user_pb.Machine { return &user_pb.Machine{ Name: view.Name, Description: view.Description, } } -func ProfileToPb(profile *model.Profile) *user_pb.Profile { +func ProfileToPb(profile *query.Profile, assetPrefix string) *user_pb.Profile { return &user_pb.Profile{ FirstName: profile.FirstName, LastName: profile.LastName, @@ -85,35 +84,35 @@ func ProfileToPb(profile *model.Profile) *user_pb.Profile { DisplayName: profile.DisplayName, PreferredLanguage: profile.PreferredLanguage.String(), Gender: GenderToPb(profile.Gender), - AvatarUrl: profile.AvatarURL, + AvatarUrl: domain.AvatarURL(assetPrefix, profile.ResourceOwner, profile.AvatarKey), } } -func EmailToPb(email *model.Email) *user_pb.Email { +func EmailToPb(email *query.Email) *user_pb.Email { return &user_pb.Email{ - Email: email.EmailAddress, - IsEmailVerified: email.IsEmailVerified, + Email: email.Email, + IsEmailVerified: email.IsVerified, } } -func PhoneToPb(phone *model.Phone) *user_pb.Phone { +func PhoneToPb(phone *query.Phone) *user_pb.Phone { return &user_pb.Phone{ - Phone: phone.PhoneNumber, - IsPhoneVerified: phone.IsPhoneVerified, + Phone: phone.Phone, + IsPhoneVerified: phone.IsVerified, } } -func ModelEmailToPb(email *model.Email) *user_pb.Email { +func ModelEmailToPb(email *query.Email) *user_pb.Email { return &user_pb.Email{ - Email: email.EmailAddress, - IsEmailVerified: email.IsEmailVerified, + Email: email.Email, + IsEmailVerified: email.IsVerified, } } -func ModelPhoneToPb(phone *model.Phone) *user_pb.Phone { +func ModelPhoneToPb(phone *query.Phone) *user_pb.Phone { return &user_pb.Phone{ - Phone: phone.PhoneNumber, - IsPhoneVerified: phone.IsPhoneVerified, + Phone: phone.Phone, + IsPhoneVerified: phone.IsVerified, } } @@ -130,19 +129,19 @@ func GenderToDomain(gender user_pb.Gender) domain.Gender { } } -func ModelUserStateToPb(state model.UserState) user_pb.UserState { +func UserStateToPb(state domain.UserState) user_pb.UserState { switch state { - case model.UserStateActive: + case domain.UserStateActive: return user_pb.UserState_USER_STATE_ACTIVE - case model.UserStateInactive: + case domain.UserStateInactive: return user_pb.UserState_USER_STATE_INACTIVE - case model.UserStateDeleted: + case domain.UserStateDeleted: return user_pb.UserState_USER_STATE_DELETED - case model.UserStateInitial: + case domain.UserStateInitial: return user_pb.UserState_USER_STATE_INITIAL - case model.UserStateLocked: + case domain.UserStateLocked: return user_pb.UserState_USER_STATE_LOCKED - case model.UserStateSuspend: + case domain.UserStateSuspend: return user_pb.UserState_USER_STATE_SUSPEND default: return user_pb.UserState_USER_STATE_UNSPECIFIED @@ -160,13 +159,13 @@ func ModelUserGrantStateToPb(state usr_grant_model.UserGrantState) user_pb.UserG } } -func GenderToPb(gender model.Gender) user_pb.Gender { +func GenderToPb(gender domain.Gender) user_pb.Gender { switch gender { - case model.GenderDiverse: + case domain.GenderDiverse: return user_pb.Gender_GENDER_DIVERSE - case model.GenderFemale: + case domain.GenderFemale: return user_pb.Gender_GENDER_FEMALE - case model.GenderMale: + case domain.GenderMale: return user_pb.Gender_GENDER_MALE default: return user_pb.Gender_GENDER_UNSPECIFIED diff --git a/internal/api/grpc/user/query.go b/internal/api/grpc/user/query.go index 3367cc70e6..d6d9e456d5 100644 --- a/internal/api/grpc/user/query.go +++ b/internal/api/grpc/user/query.go @@ -2,123 +2,79 @@ package user import ( "github.com/caos/zitadel/internal/api/grpc/object" - "github.com/caos/zitadel/internal/domain" - user_model "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query" user_pb "github.com/caos/zitadel/pkg/grpc/user" ) -func UserQueriesToModel(queries []*user_pb.SearchQuery) []*user_model.UserSearchQuery { - q := make([]*user_model.UserSearchQuery, len(queries)) +func UserQueriesToQuery(queries []*user_pb.SearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) for i, query := range queries { - q[i] = UserQueryToModel(query) + q[i], err = UserQueryToQuery(query) + if err != nil { + return nil, err + } } - return q + return q, nil } -func UserQueryToModel(query *user_pb.SearchQuery) *user_model.UserSearchQuery { +func UserQueryToQuery(query *user_pb.SearchQuery) (query.SearchQuery, error) { switch q := query.Query.(type) { case *user_pb.SearchQuery_UserNameQuery: - return UserNameQueryToModel(q.UserNameQuery) + return UserNameQueryToQuery(q.UserNameQuery) case *user_pb.SearchQuery_FirstNameQuery: - return FirstNameQueryToModel(q.FirstNameQuery) + return FirstNameQueryToQuery(q.FirstNameQuery) case *user_pb.SearchQuery_LastNameQuery: - return LastNameQueryToModel(q.LastNameQuery) + return LastNameQueryToQuery(q.LastNameQuery) case *user_pb.SearchQuery_NickNameQuery: - return NickNameQueryToModel(q.NickNameQuery) + return NickNameQueryToQuery(q.NickNameQuery) case *user_pb.SearchQuery_DisplayNameQuery: - return DisplayNameQueryToModel(q.DisplayNameQuery) + return DisplayNameQueryToQuery(q.DisplayNameQuery) case *user_pb.SearchQuery_EmailQuery: - return EmailQueryToModel(q.EmailQuery) + return EmailQueryToQuery(q.EmailQuery) case *user_pb.SearchQuery_StateQuery: - return StateQueryToModel(q.StateQuery) + return StateQueryToQuery(q.StateQuery) case *user_pb.SearchQuery_TypeQuery: - return TypeQueryToModel(q.TypeQuery) + return TypeQueryToQuery(q.TypeQuery) case *user_pb.SearchQuery_ResourceOwner: - return ResourceOwnerQueryToModel(q.ResourceOwner) + return ResourceOwnerQueryToQuery(q.ResourceOwner) default: - return nil + return nil, errors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") } } -func UserNameQueryToModel(q *user_pb.UserNameQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyUserName, - Method: object.TextMethodToModel(q.Method), - Value: q.UserName, - } +func UserNameQueryToQuery(q *user_pb.UserNameQuery) (query.SearchQuery, error) { + return query.NewUserUsernameSearchQuery(q.UserName, object.TextMethodToQuery(q.Method)) } -func FirstNameQueryToModel(q *user_pb.FirstNameQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyFirstName, - Method: object.TextMethodToModel(q.Method), - Value: q.FirstName, - } +func FirstNameQueryToQuery(q *user_pb.FirstNameQuery) (query.SearchQuery, error) { + return query.NewUserFirstNameSearchQuery(q.FirstName, object.TextMethodToQuery(q.Method)) } -func LastNameQueryToModel(q *user_pb.LastNameQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyLastName, - Method: object.TextMethodToModel(q.Method), - Value: q.LastName, - } +func LastNameQueryToQuery(q *user_pb.LastNameQuery) (query.SearchQuery, error) { + return query.NewUserLastNameSearchQuery(q.LastName, object.TextMethodToQuery(q.Method)) } -func NickNameQueryToModel(q *user_pb.NickNameQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyNickName, - Method: object.TextMethodToModel(q.Method), - Value: q.NickName, - } +func NickNameQueryToQuery(q *user_pb.NickNameQuery) (query.SearchQuery, error) { + return query.NewUserNickNameSearchQuery(q.NickName, object.TextMethodToQuery(q.Method)) } -func DisplayNameQueryToModel(q *user_pb.DisplayNameQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyDisplayName, - Method: object.TextMethodToModel(q.Method), - Value: q.DisplayName, - } +func DisplayNameQueryToQuery(q *user_pb.DisplayNameQuery) (query.SearchQuery, error) { + return query.NewUserDisplayNameSearchQuery(q.DisplayName, object.TextMethodToQuery(q.Method)) } -func EmailQueryToModel(q *user_pb.EmailQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyEmail, - Method: object.TextMethodToModel(q.Method), - Value: q.EmailAddress, - } +func EmailQueryToQuery(q *user_pb.EmailQuery) (query.SearchQuery, error) { + return query.NewUserEmailSearchQuery(q.EmailAddress, object.TextMethodToQuery(q.Method)) } -func StateQueryToModel(q *user_pb.StateQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyState, - Method: domain.SearchMethodEquals, - Value: q.State, - } +func StateQueryToQuery(q *user_pb.StateQuery) (query.SearchQuery, error) { + return query.NewUserStateSearchQuery(int32(q.State)) } -func TypeQueryToModel(q *user_pb.TypeQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyType, - Method: domain.SearchMethodEquals, - Value: UserTypeToModel(q.Type), - } +func TypeQueryToQuery(q *user_pb.TypeQuery) (query.SearchQuery, error) { + return query.NewUserTypeSearchQuery(int32(q.Type)) } -func UserTypeToModel(t user_pb.Type) string { - switch t { - case user_pb.Type_TYPE_HUMAN: - return "human" - case user_pb.Type_TYPE_MACHINE: - return "machine" - default: - return "" - } -} - -func ResourceOwnerQueryToModel(q *user_pb.ResourceOwnerQuery) *user_model.UserSearchQuery { - return &user_model.UserSearchQuery{ - Key: user_model.UserSearchKeyResourceOwner, - Method: domain.SearchMethodEquals, - Value: q.OrgID, - } +func ResourceOwnerQueryToQuery(q *user_pb.ResourceOwnerQuery) (query.SearchQuery, error) { + return query.NewUserResourceOwnerSearchQuery(q.OrgID, query.TextEquals) } diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index f99534376a..375e55af74 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -7,7 +7,6 @@ import ( "github.com/caos/oidc/pkg/oidc" "github.com/caos/oidc/pkg/op" - "golang.org/x/text/language" "gopkg.in/square/go-jose.v2" "github.com/caos/zitadel/internal/api/authz" @@ -81,7 +80,7 @@ func (o *OPStorage) GetKeyByIDAndIssuer(ctx context.Context, keyID, issuer strin } func (o *OPStorage) ValidateJWTProfileScopes(ctx context.Context, subject string, scopes []string) ([]string, error) { - user, err := o.repo.UserByID(ctx, subject) + user, err := o.query.GetUserByID(ctx, subject) if err != nil { return nil, err } @@ -176,7 +175,7 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID, applicationID string, scopes []string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - user, err := o.repo.UserByID(ctx, userID) + user, err := o.query.GetUserByID(ctx, userID) if err != nil { return err } @@ -186,38 +185,31 @@ func (o *OPStorage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSette case oidc.ScopeOpenID: userInfo.SetSubject(user.ID) case oidc.ScopeEmail: - if user.HumanView == nil { + if user.Human == nil { continue } - userInfo.SetEmail(user.Email, user.IsEmailVerified) + userInfo.SetEmail(user.Human.Email, user.Human.IsEmailVerified) case oidc.ScopeProfile: userInfo.SetPreferredUsername(user.PreferredLoginName) userInfo.SetUpdatedAt(user.ChangeDate) - if user.HumanView != nil { - userInfo.SetName(user.DisplayName) - userInfo.SetFamilyName(user.LastName) - userInfo.SetGivenName(user.FirstName) - userInfo.SetNickname(user.NickName) - userInfo.SetGender(oidc.Gender(getGender(user.Gender))) - locale, _ := language.Parse(user.PreferredLanguage) - userInfo.SetLocale(locale) - userInfo.SetPicture(user.AvatarURL) + if user.Human != nil { + userInfo.SetName(user.Human.DisplayName) + userInfo.SetFamilyName(user.Human.LastName) + userInfo.SetGivenName(user.Human.FirstName) + userInfo.SetNickname(user.Human.NickName) + userInfo.SetGender(oidc.Gender(user.Human.Gender)) + userInfo.SetLocale(user.Human.PreferredLanguage) + userInfo.SetPicture(domain.AvatarURL(o.assetAPIPrefix, user.ResourceOwner, user.Human.AvatarKey)) } else { - userInfo.SetName(user.MachineView.Name) + userInfo.SetName(user.Machine.Name) } case oidc.ScopePhone: - if user.HumanView == nil { + if user.Human == nil { continue } - userInfo.SetPhone(user.Phone, user.IsPhoneVerified) + userInfo.SetPhone(user.Human.Phone, user.Human.IsPhoneVerified) case oidc.ScopeAddress: - if user.HumanView == nil { - continue - } - if user.StreetAddress == "" && user.Locality == "" && user.Region == "" && user.PostalCode == "" && user.Country == "" { - continue - } - userInfo.SetAddress(oidc.NewUserInfoAddress(user.StreetAddress, user.Locality, user.Region, user.PostalCode, user.Country, "")) + //TODO: handle address for human users as soon as implemented case ScopeUserMetaData: userMetaData, err := o.assertUserMetaData(ctx, userID) if err != nil { @@ -316,20 +308,20 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin } func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[string]string, error) { - metaData, err := o.repo.SearchUserMetadata(ctx, userID) + metaData, err := o.query.SearchUserMetadata(ctx, userID, &query.UserMetadataSearchQueries{}) if err != nil { return nil, err } userMetaData := make(map[string]string) - for _, md := range metaData.Result { + for _, md := range metaData.Metadata { userMetaData[md.Key] = base64.RawURLEncoding.EncodeToString(md.Value) } return userMetaData, nil } func (o *OPStorage) assertUserResourceOwner(ctx context.Context, userID string) (map[string]string, error) { - user, err := o.repo.UserByID(ctx, userID) + user, err := o.query.GetUserByID(ctx, userID) if err != nil { return nil, err } diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 5b8c117da0..4c4f45c33f 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -74,9 +74,10 @@ type OPStorage struct { signingKeyRotationCheck time.Duration signingKeyGracefulPeriod time.Duration locker crdb.Locker + assetAPIPrefix string } -func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, localDevMode bool, es *eventstore.Eventstore, projections types.SQL, keyChan <-chan interface{}) op.OpenIDProvider { +func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, localDevMode bool, es *eventstore.Eventstore, projections types.SQL, keyChan <-chan interface{}, assetAPIPrefix string) op.OpenIDProvider { cookieHandler, err := middleware.NewUserAgentHandler(config.UserAgentCookieConfig, id.SonyFlakeGenerator, localDevMode) logging.Log("OIDC-sd4fd").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("cannot user agent handler") tokenKey, err := crypto.LoadKey(keyConfig.EncryptionConfig, keyConfig.EncryptionConfig.EncryptionKeyID) @@ -94,7 +95,7 @@ func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.C logging.Log("OIDC-GBd3t").OnError(err).Panic("cannot get supported languages") config.OPConfig.SupportedUILocales = supportedLanguages metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount} - storage, err := newStorage(config.StorageConfig, command, query, repo, keyConfig, es, projections, keyChan) + storage, err := newStorage(config.StorageConfig, command, query, repo, keyConfig, es, projections, keyChan, assetAPIPrefix) logging.Log("OIDC-Jdg2k").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("cannot create storage") provider, err := op.NewOpenIDProvider( ctx, @@ -119,7 +120,7 @@ func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.C return provider } -func newStorage(config StorageConfig, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, es *eventstore.Eventstore, projections types.SQL, keyChan <-chan interface{}) (*OPStorage, error) { +func newStorage(config StorageConfig, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, es *eventstore.Eventstore, projections types.SQL, keyChan <-chan interface{}, assetAPIPrefix string) (*OPStorage, error) { encAlg, err := crypto.NewAESCrypto(keyConfig.EncryptionConfig) if err != nil { return nil, err @@ -144,6 +145,7 @@ func newStorage(config StorageConfig, command *command.Commands, query *query.Qu signingKeyRotationCheck: keyConfig.SigningKeyRotationCheck.Duration, locker: crdb.NewLocker(sqlClient, locksTable, signingKey), keyChan: keyChan, + assetAPIPrefix: assetAPIPrefix, }, nil } diff --git a/internal/auth/repository/eventsourcing/eventstore/user.go b/internal/auth/repository/eventsourcing/eventstore/user.go index c0a8ee6fc0..97b6a2431b 100644 --- a/internal/auth/repository/eventsourcing/eventstore/user.go +++ b/internal/auth/repository/eventsourcing/eventstore/user.go @@ -7,6 +7,8 @@ import ( "github.com/caos/logging" "github.com/golang/protobuf/ptypes" + "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" "github.com/caos/zitadel/internal/config/systemdefaults" @@ -14,17 +16,15 @@ import ( "github.com/caos/zitadel/internal/errors" v1 "github.com/caos/zitadel/internal/eventstore/v1" "github.com/caos/zitadel/internal/eventstore/v1/models" - iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" - "github.com/caos/zitadel/internal/telemetry/tracing" - "github.com/caos/zitadel/internal/user/model" + "github.com/caos/zitadel/internal/query" usr_view "github.com/caos/zitadel/internal/user/repository/view" - usr_view_model "github.com/caos/zitadel/internal/user/repository/view/model" ) type UserRepo struct { SearchLimit uint64 Eventstore v1.Eventstore View *view.View + Query *query.Queries SystemDefaults systemdefaults.SystemDefaults PrefixAvatarURL string } @@ -33,105 +33,6 @@ func (repo *UserRepo) Health(ctx context.Context) error { return repo.Eventstore.Health(ctx) } -func (repo *UserRepo) MyUser(ctx context.Context) (*model.UserView, error) { - return repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) -} - -func (repo *UserRepo) MyProfile(ctx context.Context) (*model.Profile, error) { - user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-H2JIT", "Errors.User.NotHuman") - } - return user.GetProfile() -} - -func (repo *UserRepo) SearchMyExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) { - err := request.EnsureLimit(repo.SearchLimit) - if err != nil { - return nil, err - } - sequence, seqErr := repo.View.GetLatestExternalIDPSequence() - logging.Log("EVENT-5Jsi8").OnError(seqErr).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Warn("could not read latest user sequence") - request.AppendUserQuery(authz.GetCtxData(ctx).UserID) - externalIDPS, count, err := repo.View.SearchExternalIDPs(request) - if err != nil { - return nil, err - } - result := &model.ExternalIDPSearchResponse{ - Offset: request.Offset, - Limit: request.Limit, - TotalResult: count, - Result: usr_view_model.ExternalIDPViewsToModel(externalIDPS), - } - if seqErr == nil { - result.Sequence = sequence.CurrentSequence - result.Timestamp = sequence.LastSuccessfulSpoolerRun - } - return result, nil -} - -func (repo *UserRepo) MyEmail(ctx context.Context) (*model.Email, error) { - user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-oGRpc", "Errors.User.NotHuman") - } - return user.GetEmail() -} - -func (repo *UserRepo) MyPhone(ctx context.Context) (*model.Phone, error) { - user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-DTWJb", "Errors.User.NotHuman") - } - return user.GetPhone() -} - -func (repo *UserRepo) MyAddress(ctx context.Context) (*model.Address, error) { - user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-Ok9nI", "Errors.User.NotHuman") - } - return user.GetAddress() -} - -func (repo *UserRepo) MyUserMFAs(ctx context.Context) ([]*model.MultiFactor, error) { - user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) - if err != nil { - return nil, err - } - mfas := make([]*model.MultiFactor, 0) - if user.OTPState != model.MFAStateUnspecified { - mfas = append(mfas, &model.MultiFactor{Type: model.MFATypeOTP, State: user.OTPState}) - } - for _, u2f := range user.U2FTokens { - mfas = append(mfas, &model.MultiFactor{Type: model.MFATypeU2F, State: u2f.State, Attribute: u2f.Name, ID: u2f.TokenID}) - } - return mfas, nil -} - -func (repo *UserRepo) GetMyPasswordless(ctx context.Context) ([]*model.WebAuthNView, error) { - user, err := repo.UserByID(ctx, authz.GetCtxData(ctx).UserID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "USER-9kF98", "Errors.User.NotHuman") - } - return user.HumanView.PasswordlessTokens, nil -} - func (repo *UserRepo) UserSessionUserIDsByAgentID(ctx context.Context, agentID string) ([]string, error) { userSessions, err := repo.View.UserSessionsByAgentID(agentID) if err != nil { @@ -146,53 +47,10 @@ func (repo *UserRepo) UserSessionUserIDsByAgentID(ctx context.Context, agentID s return userIDs, nil } -func (repo *UserRepo) UserByID(ctx context.Context, id string) (*model.UserView, error) { - user, err := repo.View.UserByID(id) - if err != nil { - return nil, err - } - events, err := repo.getUserEvents(ctx, id, user.Sequence) - if err != nil { - logging.Log("EVENT-PSoc3").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error retrieving new events") - return usr_view_model.UserToModel(user, repo.PrefixAvatarURL), nil - } - userCopy := *user - for _, event := range events { - if err := userCopy.AppendEvent(event); err != nil { - return usr_view_model.UserToModel(user, repo.PrefixAvatarURL), nil - } - } - if userCopy.State == int32(model.UserStateDeleted) { - return nil, errors.ThrowNotFound(nil, "EVENT-vZ8us", "Errors.User.NotFound") - } - return usr_view_model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil -} - func (repo *UserRepo) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*models.Event, error) { return repo.getUserEvents(ctx, id, sequence) } -func (repo *UserRepo) UserByLoginName(ctx context.Context, loginname string) (*model.UserView, error) { - user, err := repo.View.UserByLoginName(loginname) - if err != nil { - return nil, err - } - events, err := repo.getUserEvents(ctx, user.ID, user.Sequence) - if err != nil { - logging.Log("EVENT-PSoc3").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("error retrieving new events") - return usr_view_model.UserToModel(user, repo.PrefixAvatarURL), nil - } - userCopy := *user - for _, event := range events { - if err := userCopy.AppendEvent(event); err != nil { - return usr_view_model.UserToModel(user, repo.PrefixAvatarURL), nil - } - } - if userCopy.State == int32(model.UserStateDeleted) { - return nil, errors.ThrowNotFound(nil, "EVENT-vZ8us", "Errors.User.NotFound") - } - return usr_view_model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil -} func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) { changes, err := repo.getUserChanges(ctx, authz.GetCtxData(ctx).UserID, lastSequence, limit, sortAscending, retention) if err != nil { @@ -201,41 +59,21 @@ func (repo *UserRepo) MyUserChanges(ctx context.Context, lastSequence uint64, li for _, change := range changes.Changes { change.ModifierName = change.ModifierID change.ModifierLoginName = change.ModifierID - user, _ := repo.UserByID(ctx, change.ModifierID) + user, _ := repo.Query.GetUserByID(ctx, change.ModifierID) if user != nil { change.ModifierLoginName = user.PreferredLoginName - if user.HumanView != nil { - change.ModifierName = user.HumanView.DisplayName - change.ModifierAvatarURL = user.HumanView.AvatarURL + if user.Human != nil { + change.ModifierName = user.Human.DisplayName + change.ModifierAvatarURL = domain.AvatarURL(repo.PrefixAvatarURL, user.ResourceOwner, user.Human.AvatarKey) } - if user.MachineView != nil { - change.ModifierName = user.MachineView.Name + if user.Machine != nil { + change.ModifierName = user.Machine.Name } } } return changes, nil } -func (repo *UserRepo) SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error) { - sequence, sequenceErr := repo.View.GetLatestUserSequence() - logging.Log("EVENT-Gdgsw").OnError(sequenceErr).Warn("could not read latest user sequence") - users, count, err := repo.View.SearchUsers(request) - if err != nil { - return nil, err - } - result := &model.UserSearchResponse{ - Offset: request.Offset, - Limit: request.Limit, - TotalResult: count, - Result: usr_view_model.UsersToModel(users, repo.PrefixAvatarURL), - } - if sequenceErr == nil { - result.Sequence = sequence.CurrentSequence - result.Timestamp = sequence.LastSuccessfulSpoolerRun - } - return result, nil -} - func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) { query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending, retention) @@ -287,50 +125,3 @@ func (r *UserRepo) getUserEvents(ctx context.Context, userID string, sequence ui } return r.Eventstore.FilterEvents(ctx, query) } - -func (repo *UserRepo) GetMyMetadataByKey(ctx context.Context, key string) (*domain.Metadata, error) { - ctxData := authz.GetCtxData(ctx) - data, err := repo.View.MetadataByKeyAndResourceOwner(ctxData.UserID, ctxData.ResourceOwner, key) - if err != nil { - return nil, err - } - return iam_model.MetadataViewToDomain(data), nil -} - -func (repo *UserRepo) SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error) { - req := new(domain.MetadataSearchRequest) - return repo.searchUserMetadata(userID, "", req) -} - -func (repo *UserRepo) SearchMyMetadata(ctx context.Context, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) { - ctxData := authz.GetCtxData(ctx) - err := req.EnsureLimit(repo.SearchLimit) - if err != nil { - return nil, err - } - return repo.searchUserMetadata(ctxData.UserID, ctxData.ResourceOwner, req) -} - -func (repo *UserRepo) searchUserMetadata(userID, resourceOwner string, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) { - sequence, sequenceErr := repo.View.GetLatestUserSequence() - logging.Log("EVENT-N9fsd").OnError(sequenceErr).Warn("could not read latest user sequence") - req.AppendAggregateIDQuery(userID) - if resourceOwner != "" { - req.AppendResourceOwnerQuery(resourceOwner) - } - metadata, count, err := repo.View.SearchMetadata(req) - if err != nil { - return nil, err - } - result := &domain.MetadataSearchResponse{ - Offset: req.Offset, - Limit: req.Limit, - TotalResult: count, - Result: iam_model.MetadataViewsToDomain(metadata), - } - if sequenceErr == nil { - result.Sequence = sequence.CurrentSequence - result.Timestamp = sequence.LastSuccessfulSpoolerRun - } - return result, nil -} diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index c847b29810..6535520872 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -50,7 +50,6 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es handler{view, bulkLimit, configs.cycleDuration("ExternalIDP"), errorCount, es}, systemDefaults), newRefreshToken(handler{view, bulkLimit, configs.cycleDuration("RefreshToken"), errorCount, es}), - newMetadata(handler{view, bulkLimit, configs.cycleDuration("Metadata"), errorCount, es}), newOrgProjectMapping(handler{view, bulkLimit, configs.cycleDuration("OrgProjectMapping"), errorCount, es}), } } diff --git a/internal/auth/repository/eventsourcing/handler/metadata.go b/internal/auth/repository/eventsourcing/handler/metadata.go deleted file mode 100644 index 3f951b51a8..0000000000 --- a/internal/auth/repository/eventsourcing/handler/metadata.go +++ /dev/null @@ -1,126 +0,0 @@ -package handler - -import ( - "github.com/caos/logging" - - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1" - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" - usr_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" -) - -type Metadata struct { - handler - subscription *v1.Subscription -} - -func newMetadata(handler handler) *Metadata { - h := &Metadata{ - handler: handler, - } - - h.subscribe() - - return h -} - -func (m *Metadata) subscribe() { - m.subscription = m.es.Subscribe(m.AggregateTypes()...) - go func() { - for event := range m.subscription.Events { - query.ReduceEvent(m, event) - } - }() -} - -const ( - metadataTable = "auth.metadata" -) - -func (m *Metadata) ViewModel() string { - return metadataTable -} - -func (m *Metadata) Subscription() *v1.Subscription { - return m.subscription -} - -func (_ *Metadata) AggregateTypes() []es_models.AggregateType { - return []es_models.AggregateType{usr_model.UserAggregate} -} - -func (p *Metadata) CurrentSequence() (uint64, error) { - sequence, err := p.view.GetLatestMetadataSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (m *Metadata) EventQuery() (*es_models.SearchQuery, error) { - sequence, err := m.view.GetLatestMetadataSequence() - if err != nil { - return nil, err - } - return es_models.NewSearchQuery(). - AggregateTypeFilter(m.AggregateTypes()...). - LatestSequenceFilter(sequence.CurrentSequence), nil -} - -func (m *Metadata) Reduce(event *es_models.Event) (err error) { - switch event.AggregateType { - case usr_model.UserAggregate: - err = m.processMetadata(event) - } - return err -} - -func (m *Metadata) processMetadata(event *es_models.Event) (err error) { - metadata := new(iam_model.MetadataView) - switch event.Type { - case usr_model.UserMetadataSet: - err = metadata.SetData(event) - if err != nil { - return err - } - metadata, err = m.view.MetadataByKey(event.AggregateID, metadata.Key) - if err != nil && !caos_errs.IsNotFound(err) { - return err - } - if caos_errs.IsNotFound(err) { - err = nil - metadata = new(iam_model.MetadataView) - metadata.CreationDate = event.CreationDate - } - err = metadata.AppendEvent(event) - case usr_model.UserMetadataRemoved: - data := new(iam_model.MetadataView) - err = data.SetData(event) - if err != nil { - return err - } - return m.view.DeleteMetadata(event.AggregateID, data.Key, event) - case usr_model.UserMetadataRemovedAll: - return m.view.DeleteMetadataByAggregateID(event.AggregateID, event) - case usr_model.UserRemoved: - return m.view.DeleteMetadataByAggregateID(event.AggregateID, event) - default: - return m.view.ProcessedMetadataSequence(event) - } - if err != nil { - return err - } - return m.view.PutMetadata(metadata, event) -} - -func (m *Metadata) OnError(event *es_models.Event, err error) error { - logging.LogWithFields("SPOOL-miJJs", "id", event.AggregateID).WithError(err).Warn("something went wrong in custom text handler") - return spooler.HandleError(event, err, m.view.GetLatestMetadataFailedEvent, m.view.ProcessedMetadataFailedEvent, m.view.ProcessedMetadataSequence, m.errorCountUntilSkip) -} - -func (m *Metadata) OnSuccess() error { - return spooler.HandleSuccess(m.view.UpdateMetadataSpoolerRunTimestamp) -} diff --git a/internal/auth/repository/eventsourcing/view/metadata.go b/internal/auth/repository/eventsourcing/view/metadata.go deleted file mode 100644 index e9edbb72e2..0000000000 --- a/internal/auth/repository/eventsourcing/view/metadata.go +++ /dev/null @@ -1,73 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/iam/repository/view" - "github.com/caos/zitadel/internal/iam/repository/view/model" - global_view "github.com/caos/zitadel/internal/view/repository" -) - -const ( - metadataTable = "auth.metadata" -) - -func (v *View) MetadataByKey(aggregateID, key string) (*model.MetadataView, error) { - return view.MetadataByKey(v.Db, metadataTable, aggregateID, key) -} - -func (v *View) MetadataListByAggregateID(aggregateID string) ([]*model.MetadataView, error) { - return view.GetMetadataList(v.Db, metadataTable, aggregateID) -} - -func (v *View) MetadataByKeyAndResourceOwner(aggregateID, resourceOwner, key string) (*model.MetadataView, error) { - return view.MetadataByKeyAndResourceOwner(v.Db, metadataTable, aggregateID, resourceOwner, key) -} - -func (v *View) SearchMetadata(request *domain.MetadataSearchRequest) ([]*model.MetadataView, uint64, error) { - return view.SearchMetadata(v.Db, metadataTable, request) -} - -func (v *View) PutMetadata(template *model.MetadataView, event *models.Event) error { - err := view.PutMetadata(v.Db, metadataTable, template) - if err != nil { - return err - } - return v.ProcessedMetadataSequence(event) -} - -func (v *View) DeleteMetadata(aggregateID, key string, event *models.Event) error { - err := view.DeleteMetadata(v.Db, metadataTable, aggregateID, key) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedMetadataSequence(event) -} - -func (v *View) DeleteMetadataByAggregateID(aggregateID string, event *models.Event) error { - err := view.DeleteMetadataByAggregateID(v.Db, metadataTable, aggregateID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedMetadataSequence(event) -} -func (v *View) GetLatestMetadataSequence() (*global_view.CurrentSequence, error) { - return v.latestSequence(metadataTable) -} - -func (v *View) ProcessedMetadataSequence(event *models.Event) error { - return v.saveCurrentSequence(metadataTable, event) -} - -func (v *View) UpdateMetadataSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(metadataTable) -} - -func (v *View) GetLatestMetadataFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { - return v.latestFailedEvent(metadataTable, sequence) -} - -func (v *View) ProcessedMetadataFailedEvent(failedEvent *global_view.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/auth/repository/user.go b/internal/auth/repository/user.go index c5e90098a5..945965e49d 100644 --- a/internal/auth/repository/user.go +++ b/internal/auth/repository/user.go @@ -4,7 +4,6 @@ import ( "context" "time" - "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/user/model" ) @@ -12,34 +11,8 @@ type UserRepository interface { myUserRepo UserSessionUserIDsByAgentID(ctx context.Context, agentID string) ([]string, error) - - UserByID(ctx context.Context, userID string) (*model.UserView, error) - UserByLoginName(ctx context.Context, loginName string) (*model.UserView, error) - - SearchUsers(ctx context.Context, request *model.UserSearchRequest) (*model.UserSearchResponse, error) - - SearchUserMetadata(ctx context.Context, userID string) (*domain.MetadataSearchResponse, error) } type myUserRepo interface { - MyUser(ctx context.Context) (*model.UserView, error) - - MyProfile(ctx context.Context) (*model.Profile, error) - - MyEmail(ctx context.Context) (*model.Email, error) - - MyPhone(ctx context.Context) (*model.Phone, error) - - MyAddress(ctx context.Context) (*model.Address, error) - - SearchMyExternalIDPs(ctx context.Context, request *model.ExternalIDPSearchRequest) (*model.ExternalIDPSearchResponse, error) - - MyUserMFAs(ctx context.Context) ([]*model.MultiFactor, error) - - GetMyPasswordless(ctx context.Context) ([]*model.WebAuthNView, error) - MyUserChanges(ctx context.Context, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) - - GetMyMetadataByKey(ctx context.Context, key string) (*domain.Metadata, error) - SearchMyMetadata(ctx context.Context, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) } diff --git a/internal/management/repository/eventsourcing/eventstore/org.go b/internal/management/repository/eventsourcing/eventstore/org.go index 56a14f28c2..591c57e00e 100644 --- a/internal/management/repository/eventsourcing/eventstore/org.go +++ b/internal/management/repository/eventsourcing/eventstore/org.go @@ -16,23 +16,17 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" v1 "github.com/caos/zitadel/internal/eventstore/v1" - "github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/i18n" - mgmt_view "github.com/caos/zitadel/internal/management/repository/eventsourcing/view" org_model "github.com/caos/zitadel/internal/org/model" org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" org_view "github.com/caos/zitadel/internal/org/repository/view" "github.com/caos/zitadel/internal/query" - usr_model "github.com/caos/zitadel/internal/user/model" - "github.com/caos/zitadel/internal/user/repository/view" - usr_es_model "github.com/caos/zitadel/internal/user/repository/view/model" ) type OrgRepository struct { Query *query.Queries SearchLimit uint64 Eventstore v1.Eventstore - View *mgmt_view.View Roles []string SystemDefaults systemdefaults.SystemDefaults PrefixAvatarURL string @@ -64,15 +58,15 @@ func (repo *OrgRepository) OrgChanges(ctx context.Context, id string, lastSequen for _, change := range changes.Changes { change.ModifierName = change.ModifierId change.ModifierLoginName = change.ModifierId - user, _ := repo.userByID(ctx, change.ModifierId) + user, _ := repo.Query.GetUserByID(ctx, change.ModifierId) if user != nil { change.ModifierLoginName = user.PreferredLoginName - if user.HumanView != nil { - change.ModifierName = user.HumanView.DisplayName - change.ModifierAvatarURL = user.HumanView.AvatarURL + if user.Human != nil { + change.ModifierName = user.Human.DisplayName + change.ModifierAvatarURL = domain.AvatarURL(repo.PrefixAvatarURL, user.ResourceOwner, user.Human.AvatarKey) } - if user.MachineView != nil { - change.ModifierName = user.MachineView.Name + if user.Machine != nil { + change.ModifierName = user.Machine.Name } } } @@ -134,40 +128,3 @@ func (repo *OrgRepository) getOrgChanges(ctx context.Context, orgID string, last LastSequence: lastSequence, }, nil } - -func (repo *OrgRepository) userByID(ctx context.Context, id string) (*usr_model.UserView, error) { - user, viewErr := repo.View.UserByID(id) - if viewErr != nil && !errors.IsNotFound(viewErr) { - return nil, viewErr - } - if errors.IsNotFound(viewErr) { - user = new(usr_es_model.UserView) - } - events, esErr := repo.getUserEvents(ctx, id, user.Sequence) - if errors.IsNotFound(viewErr) && len(events) == 0 { - return nil, errors.ThrowNotFound(nil, "EVENT-3nF8s", "Errors.User.NotFound") - } - if esErr != nil { - logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events") - return usr_es_model.UserToModel(user, repo.PrefixAvatarURL), nil - } - userCopy := *user - for _, event := range events { - if err := userCopy.AppendEvent(event); err != nil { - return usr_es_model.UserToModel(user, repo.PrefixAvatarURL), nil - } - } - if userCopy.State == int32(usr_es_model.UserStateDeleted) { - return nil, errors.ThrowNotFound(nil, "EVENT-3n8Fs", "Errors.User.NotFound") - } - return usr_es_model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil -} - -func (r *OrgRepository) getUserEvents(ctx context.Context, userID string, sequence uint64) ([]*models.Event, error) { - query, err := view.UserByIDQuery(userID, sequence) - if err != nil { - return nil, err - } - - return r.Eventstore.FilterEvents(ctx, query) -} diff --git a/internal/management/repository/eventsourcing/eventstore/permissions.go b/internal/management/repository/eventsourcing/eventstore/permissions.go deleted file mode 100644 index 653792a534..0000000000 --- a/internal/management/repository/eventsourcing/eventstore/permissions.go +++ /dev/null @@ -1,9 +0,0 @@ -package eventstore - -const ( - projectReadPerm = "project.read" - orgMemberReadPerm = "org.member.read" - iamMemberReadPerm = "iam.member.read" - projectMemberReadPerm = "project.member.read" - projectGrantMemberReadPerm = "project.grant.member.read" -) diff --git a/internal/management/repository/eventsourcing/eventstore/project.go b/internal/management/repository/eventsourcing/eventstore/project.go index 8a4b26a906..9c79333680 100644 --- a/internal/management/repository/eventsourcing/eventstore/project.go +++ b/internal/management/repository/eventsourcing/eventstore/project.go @@ -10,22 +10,17 @@ import ( "github.com/golang/protobuf/ptypes" "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" v1 "github.com/caos/zitadel/internal/eventstore/v1" "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/management/repository/eventsourcing/view" proj_model "github.com/caos/zitadel/internal/project/model" proj_view "github.com/caos/zitadel/internal/project/repository/view" "github.com/caos/zitadel/internal/query" - usr_model "github.com/caos/zitadel/internal/user/model" - usr_view "github.com/caos/zitadel/internal/user/repository/view" - usr_es_model "github.com/caos/zitadel/internal/user/repository/view/model" ) type ProjectRepo struct { v1.Eventstore - SearchLimit uint64 - View *view.View Roles []string IAMID string PrefixAvatarURL string @@ -40,15 +35,15 @@ func (repo *ProjectRepo) ProjectChanges(ctx context.Context, id string, lastSequ for _, change := range changes.Changes { change.ModifierName = change.ModifierId change.ModifierLoginName = change.ModifierId - user, _ := repo.userByID(ctx, change.ModifierId) + user, _ := repo.Query.GetUserByID(ctx, change.ModifierId) if user != nil { change.ModifierLoginName = user.PreferredLoginName - if user.HumanView != nil { - change.ModifierName = user.HumanView.DisplayName - change.ModifierAvatarURL = user.HumanView.AvatarURL + if user.Human != nil { + change.ModifierName = user.Human.DisplayName + change.ModifierAvatarURL = domain.AvatarURL(repo.PrefixAvatarURL, user.ResourceOwner, user.Human.AvatarKey) } - if user.MachineView != nil { - change.ModifierName = user.MachineView.Name + if user.Machine != nil { + change.ModifierName = user.Machine.Name } } } @@ -63,15 +58,15 @@ func (repo *ProjectRepo) ApplicationChanges(ctx context.Context, projectID strin for _, change := range changes.Changes { change.ModifierName = change.ModifierId change.ModifierLoginName = change.ModifierId - user, _ := repo.userByID(ctx, change.ModifierId) + user, _ := repo.Query.GetUserByID(ctx, change.ModifierId) if user != nil { change.ModifierLoginName = user.PreferredLoginName - if user.HumanView != nil { - change.ModifierName = user.HumanView.DisplayName - change.ModifierAvatarURL = user.HumanView.AvatarURL + if user.Human != nil { + change.ModifierName = user.Human.DisplayName + change.ModifierAvatarURL = domain.AvatarURL(repo.PrefixAvatarURL, user.ResourceOwner, user.Human.AvatarKey) } - if user.MachineView != nil { - change.ModifierName = user.MachineView.Name + if user.Machine != nil { + change.ModifierName = user.Machine.Name } } } @@ -106,42 +101,6 @@ func (repo *ProjectRepo) GetProjectGrantMemberRoles() []string { return roles } -func (repo *ProjectRepo) userByID(ctx context.Context, id string) (*usr_model.UserView, error) { - user, viewErr := repo.View.UserByID(id) - if viewErr != nil && !caos_errs.IsNotFound(viewErr) { - return nil, viewErr - } - if caos_errs.IsNotFound(viewErr) { - user = new(usr_es_model.UserView) - } - events, esErr := repo.getUserEvents(ctx, id, user.Sequence) - if caos_errs.IsNotFound(viewErr) && len(events) == 0 { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-4n8Fs", "Errors.User.NotFound") - } - if esErr != nil { - logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events") - return usr_es_model.UserToModel(user, repo.PrefixAvatarURL), nil - } - userCopy := *user - for _, event := range events { - if err := userCopy.AppendEvent(event); err != nil { - return usr_es_model.UserToModel(user, repo.PrefixAvatarURL), nil - } - } - if userCopy.State == int32(usr_model.UserStateDeleted) { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-2m0Fs", "Errors.User.NotFound") - } - return usr_es_model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil -} - -func (r *ProjectRepo) getUserEvents(ctx context.Context, userID string, sequence uint64) ([]*models.Event, error) { - query, err := usr_view.UserByIDQuery(userID, sequence) - if err != nil { - return nil, err - } - return r.Eventstore.FilterEvents(ctx, query) -} - func (repo *ProjectRepo) getProjectChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*proj_model.ProjectChanges, error) { query := proj_view.ChangesQuery(id, lastSequence, limit, sortAscending, retention) diff --git a/internal/management/repository/eventsourcing/eventstore/user.go b/internal/management/repository/eventsourcing/eventstore/user.go index 0094beaebc..946ead3630 100644 --- a/internal/management/repository/eventsourcing/eventstore/user.go +++ b/internal/management/repository/eventsourcing/eventstore/user.go @@ -7,117 +7,24 @@ import ( "github.com/caos/logging" "github.com/golang/protobuf/ptypes" - "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/query" + "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" v1 "github.com/caos/zitadel/internal/eventstore/v1" "github.com/caos/zitadel/internal/eventstore/v1/models" - iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" - "github.com/caos/zitadel/internal/management/repository/eventsourcing/view" usr_model "github.com/caos/zitadel/internal/user/model" usr_view "github.com/caos/zitadel/internal/user/repository/view" - "github.com/caos/zitadel/internal/user/repository/view/model" - "github.com/caos/zitadel/internal/view/repository" ) type UserRepo struct { v1.Eventstore - SearchLimit uint64 - View *view.View + Query *query.Queries SystemDefaults systemdefaults.SystemDefaults PrefixAvatarURL string } -func (repo *UserRepo) UserByID(ctx context.Context, id string) (*usr_model.UserView, error) { - user, viewErr := repo.View.UserByID(id) - if viewErr != nil && !errors.IsNotFound(viewErr) { - return nil, viewErr - } - if errors.IsNotFound(viewErr) { - user = new(model.UserView) - } - - events, esErr := repo.getUserEvents(ctx, id, user.Sequence) - if errors.IsNotFound(viewErr) && len(events) == 0 { - return nil, errors.ThrowNotFound(nil, "EVENT-Lsoj7", "Errors.User.NotFound") - } - if esErr != nil { - logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events") - return model.UserToModel(user, repo.PrefixAvatarURL), nil - } - userCopy := *user - for _, event := range events { - if err := userCopy.AppendEvent(event); err != nil { - return model.UserToModel(user, repo.PrefixAvatarURL), nil - } - } - if userCopy.State == int32(usr_model.UserStateDeleted) { - return nil, errors.ThrowNotFound(nil, "EVENT-4Fm9s", "Errors.User.NotFound") - } - return model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil -} - -func (repo *UserRepo) UserByIDAndResourceOwner(ctx context.Context, id, resourceOwner string) (*usr_model.UserView, error) { - user, viewErr := repo.View.UserByIDAndResourceOwner(id, resourceOwner) - if viewErr != nil && !errors.IsNotFound(viewErr) { - return nil, viewErr - } - if errors.IsNotFound(viewErr) { - user = new(model.UserView) - } - - events, esErr := repo.getUserEvents(ctx, id, user.Sequence) - if errors.IsNotFound(viewErr) && len(events) == 0 { - return nil, errors.ThrowNotFound(nil, "EVENT-Lsoj7", "Errors.User.NotFound") - } - if esErr != nil { - logging.Log("EVENT-PSoc3").WithError(esErr).Debug("error retrieving new events") - return model.UserToModel(user, repo.PrefixAvatarURL), nil - } - userCopy := *user - for _, event := range events { - if err := userCopy.AppendEvent(event); err != nil { - return model.UserToModel(user, repo.PrefixAvatarURL), nil - } - } - if userCopy.State == int32(usr_model.UserStateDeleted) || userCopy.ResourceOwner != resourceOwner { - return nil, errors.ThrowNotFound(nil, "EVENT-4Fm9s", "Errors.User.NotFound") - } - return model.UserToModel(&userCopy, repo.PrefixAvatarURL), nil -} - -func (repo *UserRepo) SearchUsers(ctx context.Context, request *usr_model.UserSearchRequest, ensureLimit bool) (*usr_model.UserSearchResponse, error) { - if ensureLimit { - err := request.EnsureLimit(repo.SearchLimit) - - if err != nil { - return nil, err - } - } - sequence, sequenceErr := repo.View.GetLatestUserSequence() - logging.Log("EVENT-Lcn7d").OnError(sequenceErr).Warn("could not read latest user sequence") - users, count, err := repo.View.SearchUsers(request) - if err != nil { - return nil, err - } - result := &usr_model.UserSearchResponse{ - Offset: request.Offset, - Limit: request.Limit, - TotalResult: count, - Result: model.UsersToModel(users, repo.PrefixAvatarURL), - } - if sequenceErr == nil { - result.Sequence = sequence.CurrentSequence - result.Timestamp = sequence.LastSuccessfulSpoolerRun - } - return result, nil -} - -func (repo *UserRepo) UserIDsByDomain(ctx context.Context, domain string) ([]string, error) { - return repo.View.UserIDsByDomain(domain) -} - func (repo *UserRepo) UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*usr_model.UserChanges, error) { changes, err := repo.getUserChanges(ctx, id, lastSequence, limit, sortAscending, retention) if err != nil { @@ -126,111 +33,21 @@ func (repo *UserRepo) UserChanges(ctx context.Context, id string, lastSequence u for _, change := range changes.Changes { change.ModifierName = change.ModifierID change.ModifierLoginName = change.ModifierID - user, _ := repo.UserByID(ctx, change.ModifierID) + user, _ := repo.Query.GetUserByID(ctx, change.ModifierID) if user != nil { change.ModifierLoginName = user.PreferredLoginName - if user.HumanView != nil { - change.ModifierName = user.HumanView.DisplayName - change.ModifierAvatarURL = user.HumanView.AvatarURL + if user.Human != nil { + change.ModifierName = user.Human.DisplayName + change.ModifierAvatarURL = domain.AvatarURL(repo.PrefixAvatarURL, user.ResourceOwner, user.Human.AvatarKey) } - if user.MachineView != nil { - change.ModifierName = user.MachineView.Name + if user.Machine != nil { + change.ModifierName = user.Machine.Name } } } return changes, nil } -func (repo *UserRepo) GetUserByLoginNameGlobal(ctx context.Context, loginName string) (*usr_model.UserView, error) { - user, err := repo.View.GetGlobalUserByLoginName(loginName) - if err != nil { - return nil, err - } - return model.UserToModel(user, repo.PrefixAvatarURL), nil -} - -func (repo *UserRepo) IsUserUnique(ctx context.Context, userName, email, orgID string) (bool, error) { - return repo.View.IsUserUnique(userName, email, orgID) -} - -func (repo *UserRepo) GetMetadataByKey(ctx context.Context, userID, resourceOwner, key string) (*domain.Metadata, error) { - data, err := repo.View.MetadataByKeyAndResourceOwner(userID, resourceOwner, key) - if err != nil { - return nil, err - } - return iam_model.MetadataViewToDomain(data), nil -} - -func (repo *UserRepo) SearchMetadata(ctx context.Context, userID, resourceOwner string, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) { - err := req.EnsureLimit(repo.SearchLimit) - if err != nil { - return nil, err - } - sequence, sequenceErr := repo.View.GetLatestUserSequence() - logging.Log("EVENT-m0ds3").OnError(sequenceErr).Warn("could not read latest user sequence") - req.AppendAggregateIDQuery(userID) - req.AppendResourceOwnerQuery(resourceOwner) - metadata, count, err := repo.View.SearchMetadata(req) - if err != nil { - return nil, err - } - result := &domain.MetadataSearchResponse{ - Offset: req.Offset, - Limit: req.Limit, - TotalResult: count, - Result: iam_model.MetadataViewsToDomain(metadata), - } - if sequenceErr == nil { - result.Sequence = sequence.CurrentSequence - result.Timestamp = sequence.LastSuccessfulSpoolerRun - } - return result, nil -} - -func (repo *UserRepo) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) { - user, err := repo.UserByID(ctx, userID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-gDFC2", "Errors.User.NotHuman") - } - return user.GetProfile() -} - -func (repo *UserRepo) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) { - user, err := repo.UserByID(ctx, userID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-pt7HY", "Errors.User.NotHuman") - } - return user.GetEmail() -} - -func (repo *UserRepo) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) { - user, err := repo.UserByID(ctx, userID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-hliQl", "Errors.User.NotHuman") - } - return user.GetPhone() -} - -func (repo *UserRepo) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) { - user, err := repo.UserByID(ctx, userID) - if err != nil { - return nil, err - } - if user.HumanView == nil { - return nil, errors.ThrowPreconditionFailed(nil, "EVENT-LQh4I", "Errors.User.NotHuman") - } - return user.GetAddress() -} - func (r *UserRepo) getUserChanges(ctx context.Context, userID string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*usr_model.UserChanges, error) { query := usr_view.ChangesQuery(userID, lastSequence, limit, sortAscending, retention) @@ -282,47 +99,3 @@ func (r *UserRepo) getUserEvents(ctx context.Context, userID string, sequence ui } return r.Eventstore.FilterEvents(ctx, query) } - -func handleSearchUserMembershipsPermissions(ctx context.Context, request *usr_model.UserMembershipSearchRequest, sequence *repository.CurrentSequence) *usr_model.UserMembershipSearchResponse { - permissions := authz.GetAllPermissionsFromCtx(ctx) - iamPerm := authz.HasGlobalExplicitPermission(permissions, iamMemberReadPerm) - orgPerm := authz.HasGlobalExplicitPermission(permissions, orgMemberReadPerm) - projectPerm := authz.HasGlobalExplicitPermission(permissions, projectMemberReadPerm) - projectGrantPerm := authz.HasGlobalExplicitPermission(permissions, projectGrantMemberReadPerm) - if iamPerm && orgPerm && projectPerm && projectGrantPerm { - return nil - } - if !iamPerm { - request.Queries = append(request.Queries, &usr_model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyMemberType, Method: domain.SearchMethodNotEquals, Value: usr_model.MemberTypeIam}) - } - if !orgPerm { - request.Queries = append(request.Queries, &usr_model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyMemberType, Method: domain.SearchMethodNotEquals, Value: usr_model.MemberTypeOrganisation}) - } - - ids := authz.GetExplicitPermissionCtxIDs(permissions, projectMemberReadPerm) - ids = append(ids, authz.GetExplicitPermissionCtxIDs(permissions, projectGrantMemberReadPerm)...) - if _, q := request.GetSearchQuery(usr_model.UserMembershipSearchKeyObjectID); q != nil { - containsID := false - for _, id := range ids { - if id == q.Value { - containsID = true - break - } - } - if !containsID { - result := &usr_model.UserMembershipSearchResponse{ - Offset: request.Offset, - Limit: request.Limit, - TotalResult: uint64(0), - Result: []*usr_model.UserMembershipView{}, - } - if sequence != nil { - result.Sequence = sequence.CurrentSequence - result.Timestamp = sequence.LastSuccessfulSpoolerRun - } - return result - } - } - request.Queries = append(request.Queries, &usr_model.UserMembershipSearchQuery{Key: usr_model.UserMembershipSearchKeyObjectID, Method: domain.SearchMethodIsOneOf, Value: ids}) - return nil -} diff --git a/internal/management/repository/eventsourcing/handler/handler.go b/internal/management/repository/eventsourcing/handler/handler.go deleted file mode 100644 index 6a2de48ffe..0000000000 --- a/internal/management/repository/eventsourcing/handler/handler.go +++ /dev/null @@ -1,61 +0,0 @@ -package handler - -import ( - "time" - - v1 "github.com/caos/zitadel/internal/eventstore/v1" - "github.com/caos/zitadel/internal/static" - - "github.com/caos/zitadel/internal/config/systemdefaults" - "github.com/caos/zitadel/internal/config/types" - "github.com/caos/zitadel/internal/eventstore/v1/query" - "github.com/caos/zitadel/internal/management/repository/eventsourcing/view" -) - -type Configs map[string]*Config - -type Config struct { - MinimumCycleDuration types.Duration -} - -type handler struct { - view *view.View - bulkLimit uint64 - cycleDuration time.Duration - errorCountUntilSkip uint64 - - es v1.Eventstore -} - -func (h *handler) Eventstore() v1.Eventstore { - return h.es -} - -func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, es v1.Eventstore, defaults systemdefaults.SystemDefaults, staticStorage static.Storage) []query.Handler { - return []query.Handler{ - newUser(handler{view, bulkLimit, configs.cycleDuration("User"), errorCount, es}, - defaults.IamID), - newMetadata( - handler{view, bulkLimit, configs.cycleDuration("Metadata"), errorCount, es}), - } -} - -func (configs Configs) cycleDuration(viewModel string) time.Duration { - c, ok := configs[viewModel] - if !ok { - return 3 * time.Minute - } - return c.MinimumCycleDuration.Duration -} - -func (h *handler) MinimumCycleDuration() time.Duration { - return h.cycleDuration -} - -func (h *handler) LockDuration() time.Duration { - return h.cycleDuration / 3 -} - -func (h *handler) QueryLimit() uint64 { - return h.bulkLimit -} diff --git a/internal/management/repository/eventsourcing/handler/metadata.go b/internal/management/repository/eventsourcing/handler/metadata.go deleted file mode 100644 index 8c4fcdcaaf..0000000000 --- a/internal/management/repository/eventsourcing/handler/metadata.go +++ /dev/null @@ -1,126 +0,0 @@ -package handler - -import ( - "github.com/caos/logging" - - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1" - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - iam_model "github.com/caos/zitadel/internal/iam/repository/view/model" - usr_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" -) - -type Metadata struct { - handler - subscription *v1.Subscription -} - -func newMetadata(handler handler) *Metadata { - h := &Metadata{ - handler: handler, - } - - h.subscribe() - - return h -} - -func (m *Metadata) subscribe() { - m.subscription = m.es.Subscribe(m.AggregateTypes()...) - go func() { - for event := range m.subscription.Events { - query.ReduceEvent(m, event) - } - }() -} - -const ( - metadataTable = "management.metadata" -) - -func (m *Metadata) ViewModel() string { - return metadataTable -} - -func (m *Metadata) Subscription() *v1.Subscription { - return m.subscription -} - -func (_ *Metadata) AggregateTypes() []es_models.AggregateType { - return []es_models.AggregateType{usr_model.UserAggregate} -} - -func (p *Metadata) CurrentSequence() (uint64, error) { - sequence, err := p.view.GetLatestMetadataSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (m *Metadata) EventQuery() (*es_models.SearchQuery, error) { - sequence, err := m.view.GetLatestMetadataSequence() - if err != nil { - return nil, err - } - return es_models.NewSearchQuery(). - AggregateTypeFilter(m.AggregateTypes()...). - LatestSequenceFilter(sequence.CurrentSequence), nil -} - -func (m *Metadata) Reduce(event *es_models.Event) (err error) { - switch event.AggregateType { - case usr_model.UserAggregate: - err = m.processMetadata(event) - } - return err -} - -func (m *Metadata) processMetadata(event *es_models.Event) (err error) { - metadata := new(iam_model.MetadataView) - switch event.Type { - case usr_model.UserMetadataSet: - err = metadata.SetData(event) - if err != nil { - return err - } - metadata, err = m.view.MetadataByKey(event.AggregateID, metadata.Key) - if err != nil && !caos_errs.IsNotFound(err) { - return err - } - if caos_errs.IsNotFound(err) { - err = nil - metadata = new(iam_model.MetadataView) - metadata.CreationDate = event.CreationDate - } - err = metadata.AppendEvent(event) - case usr_model.UserMetadataRemoved: - data := new(iam_model.MetadataView) - err = data.SetData(event) - if err != nil { - return err - } - return m.view.DeleteMetadata(event.AggregateID, data.Key, event) - case usr_model.UserMetadataRemovedAll: - return m.view.DeleteMetadataByAggregateID(event.AggregateID, event) - case usr_model.UserRemoved: - return m.view.DeleteMetadataByAggregateID(event.AggregateID, event) - default: - return m.view.ProcessedMetadataSequence(event) - } - if err != nil { - return err - } - return m.view.PutMetadata(metadata, event) -} - -func (m *Metadata) OnError(event *es_models.Event, err error) error { - logging.LogWithFields("SPOOL-3m912", "id", event.AggregateID).WithError(err).Warn("something went wrong in custom text handler") - return spooler.HandleError(event, err, m.view.GetLatestMetadataFailedEvent, m.view.ProcessedMetadataFailedEvent, m.view.ProcessedMetadataSequence, m.errorCountUntilSkip) -} - -func (o *Metadata) OnSuccess() error { - return spooler.HandleSuccess(o.view.UpdateMetadataSpoolerRunTimestamp) -} diff --git a/internal/management/repository/eventsourcing/handler/user.go b/internal/management/repository/eventsourcing/handler/user.go deleted file mode 100644 index 77f911b556..0000000000 --- a/internal/management/repository/eventsourcing/handler/user.go +++ /dev/null @@ -1,311 +0,0 @@ -package handler - -import ( - "context" - - "github.com/caos/logging" - - "github.com/caos/zitadel/internal/domain" - caos_errs "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1" - es_models "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/eventstore/v1/query" - es_sdk "github.com/caos/zitadel/internal/eventstore/v1/sdk" - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - iam_model "github.com/caos/zitadel/internal/iam/model" - "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model" - iam_view "github.com/caos/zitadel/internal/iam/repository/view" - org_model "github.com/caos/zitadel/internal/org/model" - org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model" - "github.com/caos/zitadel/internal/org/repository/view" - user_repo "github.com/caos/zitadel/internal/repository/user" - es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" - view_model "github.com/caos/zitadel/internal/user/repository/view/model" -) - -const ( - userTable = "management.users" -) - -type User struct { - handler - iamID string - subscription *v1.Subscription -} - -func newUser( - handler handler, - iamID string, -) *User { - h := &User{ - handler: handler, - iamID: iamID, - } - - h.subscribe() - - return h -} - -func (m *User) subscribe() { - m.subscription = m.es.Subscribe(m.AggregateTypes()...) - go func() { - for event := range m.subscription.Events { - query.ReduceEvent(m, event) - } - }() -} - -func (u *User) ViewModel() string { - return userTable -} - -func (u *User) Subscription() *v1.Subscription { - return u.subscription -} - -func (_ *User) AggregateTypes() []es_models.AggregateType { - return []es_models.AggregateType{es_model.UserAggregate, org_es_model.OrgAggregate} -} - -func (u *User) CurrentSequence() (uint64, error) { - sequence, err := u.view.GetLatestUserSequence() - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (u *User) EventQuery() (*es_models.SearchQuery, error) { - sequence, err := u.view.GetLatestUserSequence() - if err != nil { - return nil, err - } - return es_models.NewSearchQuery(). - AggregateTypeFilter(u.AggregateTypes()...). - LatestSequenceFilter(sequence.CurrentSequence), nil -} - -func (u *User) Reduce(event *es_models.Event) (err error) { - switch event.AggregateType { - case es_model.UserAggregate: - return u.ProcessUser(event) - case org_es_model.OrgAggregate: - return u.ProcessOrg(event) - default: - return nil - } -} - -func (u *User) ProcessUser(event *es_models.Event) (err error) { - user := new(view_model.UserView) - switch event.Type { - case es_model.UserAdded, - es_model.UserRegistered, - es_model.HumanRegistered, - es_model.MachineAdded, - es_model.HumanAdded: - err = user.AppendEvent(event) - if err != nil { - return err - } - err = u.fillLoginNames(user) - case es_model.UserProfileChanged, - es_model.UserEmailChanged, - es_model.UserEmailVerified, - es_model.UserPhoneChanged, - es_model.UserPhoneVerified, - es_model.UserPhoneRemoved, - es_model.UserAddressChanged, - es_model.UserDeactivated, - es_model.UserReactivated, - es_model.UserLocked, - es_model.UserUnlocked, - es_model.MFAOTPAdded, - es_model.MFAOTPVerified, - es_model.MFAOTPRemoved, - es_model.HumanProfileChanged, - es_model.HumanEmailChanged, - es_model.HumanEmailVerified, - es_model.HumanAvatarAdded, - es_model.HumanAvatarRemoved, - es_model.HumanPhoneChanged, - es_model.HumanPhoneVerified, - es_model.HumanPhoneRemoved, - es_model.HumanAddressChanged, - es_model.HumanMFAOTPAdded, - es_model.HumanMFAOTPVerified, - es_model.HumanMFAOTPRemoved, - es_model.HumanMFAU2FTokenAdded, - es_model.HumanMFAU2FTokenVerified, - es_model.HumanMFAU2FTokenRemoved, - es_model.HumanPasswordlessTokenAdded, - es_model.HumanPasswordlessTokenVerified, - es_model.HumanPasswordlessTokenRemoved, - es_model.MachineChanged, - es_models.EventType(user_repo.HumanPasswordlessInitCodeAddedType), - es_models.EventType(user_repo.HumanPasswordlessInitCodeRequestedType): - user, err = u.view.UserByID(event.AggregateID) - if err != nil { - return err - } - err = user.AppendEvent(event) - case es_model.DomainClaimed, - es_model.UserUserNameChanged: - user, err = u.view.UserByID(event.AggregateID) - if err != nil { - return err - } - err = user.AppendEvent(event) - if err != nil { - return err - } - err = u.fillLoginNames(user) - case es_model.UserRemoved: - return u.view.DeleteUser(event.AggregateID, event) - default: - return u.view.ProcessedUserSequence(event) - } - if err != nil { - return err - } - return u.view.PutUser(user, event) -} - -func (u *User) ProcessOrg(event *es_models.Event) (err error) { - switch event.Type { - case org_es_model.OrgDomainVerified, - org_es_model.OrgDomainRemoved, - org_es_model.OrgIAMPolicyAdded, - org_es_model.OrgIAMPolicyChanged, - org_es_model.OrgIAMPolicyRemoved: - return u.fillLoginNamesOnOrgUsers(event) - case org_es_model.OrgDomainPrimarySet: - return u.fillPreferredLoginNamesOnOrgUsers(event) - default: - return u.view.ProcessedUserSequence(event) - } -} - -func (u *User) fillLoginNamesOnOrgUsers(event *es_models.Event) error { - org, err := u.getOrgByID(context.Background(), event.ResourceOwner) - if err != nil { - return err - } - policy := org.OrgIamPolicy - if policy == nil { - policy, err = u.getDefaultOrgIAMPolicy(context.Background()) - if err != nil { - return err - } - } - users, err := u.view.UsersByOrgID(event.AggregateID) - if err != nil { - return err - } - for _, user := range users { - user.SetLoginNames(policy, org.Domains) - } - return u.view.PutUsers(users, event) -} - -func (u *User) fillPreferredLoginNamesOnOrgUsers(event *es_models.Event) error { - org, err := u.getOrgByID(context.Background(), event.ResourceOwner) - if err != nil { - return err - } - policy := org.OrgIamPolicy - if policy == nil { - policy, err = u.getDefaultOrgIAMPolicy(context.Background()) - if err != nil { - return err - } - } - if !policy.UserLoginMustBeDomain { - return u.view.ProcessedUserSequence(event) - } - users, err := u.view.UsersByOrgID(event.AggregateID) - if err != nil { - return err - } - for _, user := range users { - user.PreferredLoginName = user.GenerateLoginName(org.GetPrimaryDomain().Domain, policy.UserLoginMustBeDomain) - } - return u.view.PutUsers(users, event) -} - -func (u *User) fillLoginNames(user *view_model.UserView) (err error) { - org, err := u.getOrgByID(context.Background(), user.ResourceOwner) - if err != nil { - return err - } - policy := org.OrgIamPolicy - if policy == nil { - policy, err = u.getDefaultOrgIAMPolicy(context.TODO()) - if err != nil { - return err - } - } - user.SetLoginNames(policy, org.Domains) - user.PreferredLoginName = user.GenerateLoginName(org.GetPrimaryDomain().Domain, policy.UserLoginMustBeDomain) - return nil -} - -func (u *User) OnError(event *es_models.Event, err error) error { - logging.LogWithFields("SPOOL-is8wa", "id", event.AggregateID).WithError(err).Warn("something went wrong in user handler") - return spooler.HandleError(event, err, u.view.GetLatestUserFailedEvent, u.view.ProcessedUserFailedEvent, u.view.ProcessedUserSequence, u.errorCountUntilSkip) -} - -func (u *User) OnSuccess() error { - return spooler.HandleSuccess(u.view.UpdateUserSpoolerRunTimestamp) -} - -func (u *User) getOrgByID(ctx context.Context, orgID string) (*org_model.Org, error) { - query, err := view.OrgByIDQuery(orgID, 0) - if err != nil { - return nil, err - } - - esOrg := &org_es_model.Org{ - ObjectRoot: es_models.ObjectRoot{ - AggregateID: orgID, - }, - } - err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query) - if err != nil && !caos_errs.IsNotFound(err) { - return nil, err - } - if esOrg.Sequence == 0 { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-kVLb2", "Errors.Org.NotFound") - } - - return org_es_model.OrgToModel(esOrg), nil -} - -func (u *User) getIAMByID(ctx context.Context) (*iam_model.IAM, error) { - query, err := iam_view.IAMByIDQuery(domain.IAMID, 0) - if err != nil { - return nil, err - } - iam := &model.IAM{ - ObjectRoot: es_models.ObjectRoot{ - AggregateID: domain.IAMID, - }, - } - err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, iam.AppendEvents, query) - if err != nil && caos_errs.IsNotFound(err) && iam.Sequence == 0 { - return nil, err - } - return model.IAMToModel(iam), nil -} - -func (u *User) getDefaultOrgIAMPolicy(ctx context.Context) (*iam_model.OrgIAMPolicy, error) { - existingIAM, err := u.getIAMByID(ctx) - if err != nil { - return nil, err - } - if existingIAM.DefaultOrgIAMPolicy == nil { - return nil, caos_errs.ThrowNotFound(nil, "EVENT-2Fj8s", "Errors.IAM.OrgIAMPolicy.NotExisting") - } - return existingIAM.DefaultOrgIAMPolicy, nil -} diff --git a/internal/management/repository/eventsourcing/repository.go b/internal/management/repository/eventsourcing/repository.go index 51ca363dd1..4ba6037c89 100644 --- a/internal/management/repository/eventsourcing/repository.go +++ b/internal/management/repository/eventsourcing/repository.go @@ -7,10 +7,7 @@ import ( sd "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/config/types" v1 "github.com/caos/zitadel/internal/eventstore/v1" - es_spol "github.com/caos/zitadel/internal/eventstore/v1/spooler" "github.com/caos/zitadel/internal/management/repository/eventsourcing/eventstore" - "github.com/caos/zitadel/internal/management/repository/eventsourcing/spooler" - mgmt_view "github.com/caos/zitadel/internal/management/repository/eventsourcing/view" "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/static" ) @@ -21,16 +18,13 @@ type Config struct { APIDomain string Eventstore v1.Config View types.SQL - Spooler spooler.SpoolerConfig } type EsRepository struct { - spooler *es_spol.Spooler eventstore.OrgRepository eventstore.ProjectRepo eventstore.UserRepo eventstore.IAMRepository - view *mgmt_view.View } func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, queries *query.Queries, staticStorage static.Storage) (*EsRepository, error) { @@ -40,16 +34,6 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, querie return nil, err } - sqlClient, err := conf.View.Start() - if err != nil { - return nil, err - } - view, err := mgmt_view.StartView(sqlClient) - if err != nil { - return nil, err - } - - spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, systemDefaults, staticStorage) assetsAPI := conf.APIDomain + "/assets/v1/" statikLoginFS, err := fs.NewWithNamespace("login") @@ -59,11 +43,9 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, querie logging.Log("CONFI-7usEW").OnError(err).Panic("unable to start notification statik dir") return &EsRepository{ - spooler: spool, OrgRepository: eventstore.OrgRepository{ SearchLimit: conf.SearchLimit, Eventstore: es, - View: view, Roles: roles, SystemDefaults: systemDefaults, PrefixAvatarURL: assetsAPI, @@ -73,13 +55,8 @@ func Start(conf Config, systemDefaults sd.SystemDefaults, roles []string, querie NotificationTranslationFileContents: make(map[string][]byte), Query: queries, }, - ProjectRepo: eventstore.ProjectRepo{es, conf.SearchLimit, view, roles, systemDefaults.IamID, assetsAPI, queries}, - UserRepo: eventstore.UserRepo{es, conf.SearchLimit, view, systemDefaults, assetsAPI}, + ProjectRepo: eventstore.ProjectRepo{es, roles, systemDefaults.IamID, assetsAPI, queries}, + UserRepo: eventstore.UserRepo{es, queries, systemDefaults, assetsAPI}, IAMRepository: eventstore.IAMRepository{IAMV2Query: queries}, - view: view, }, nil } - -func (repo *EsRepository) Health() error { - return repo.view.Health() -} diff --git a/internal/management/repository/eventsourcing/spooler/lock.go b/internal/management/repository/eventsourcing/spooler/lock.go deleted file mode 100644 index 62e543a86f..0000000000 --- a/internal/management/repository/eventsourcing/spooler/lock.go +++ /dev/null @@ -1,19 +0,0 @@ -package spooler - -import ( - "database/sql" - es_locker "github.com/caos/zitadel/internal/eventstore/v1/locker" - "time" -) - -const ( - lockTable = "management.locks" -) - -type locker struct { - dbClient *sql.DB -} - -func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error { - return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime) -} diff --git a/internal/management/repository/eventsourcing/spooler/spooler.go b/internal/management/repository/eventsourcing/spooler/spooler.go deleted file mode 100644 index 3c76a43a0a..0000000000 --- a/internal/management/repository/eventsourcing/spooler/spooler.go +++ /dev/null @@ -1,31 +0,0 @@ -package spooler - -import ( - "database/sql" - "github.com/caos/zitadel/internal/config/systemdefaults" - "github.com/caos/zitadel/internal/eventstore/v1" - "github.com/caos/zitadel/internal/static" - - "github.com/caos/zitadel/internal/eventstore/v1/spooler" - "github.com/caos/zitadel/internal/management/repository/eventsourcing/handler" - "github.com/caos/zitadel/internal/management/repository/eventsourcing/view" -) - -type SpoolerConfig struct { - BulkLimit uint64 - FailureCountUntilSkip uint64 - ConcurrentWorkers int - Handlers handler.Configs -} - -func StartSpooler(c SpoolerConfig, es v1.Eventstore, view *view.View, sql *sql.DB, defaults systemdefaults.SystemDefaults, staticStorage static.Storage) *spooler.Spooler { - spoolerConfig := spooler.Config{ - Eventstore: es, - Locker: &locker{dbClient: sql}, - ConcurrentWorkers: c.ConcurrentWorkers, - ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, defaults, staticStorage), - } - spool := spoolerConfig.New() - spool.Start() - return spool -} diff --git a/internal/management/repository/eventsourcing/view/error_event.go b/internal/management/repository/eventsourcing/view/error_event.go deleted file mode 100644 index 50440dc164..0000000000 --- a/internal/management/repository/eventsourcing/view/error_event.go +++ /dev/null @@ -1,17 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/view/repository" -) - -const ( - errTable = "management.failed_events" -) - -func (v *View) saveFailedEvent(failedEvent *repository.FailedEvent) error { - return repository.SaveFailedEvent(v.Db, errTable, failedEvent) -} - -func (v *View) latestFailedEvent(viewName string, sequence uint64) (*repository.FailedEvent, error) { - return repository.LatestFailedEvent(v.Db, errTable, viewName, sequence) -} diff --git a/internal/management/repository/eventsourcing/view/metadata.go b/internal/management/repository/eventsourcing/view/metadata.go deleted file mode 100644 index 3fd5b9d5f5..0000000000 --- a/internal/management/repository/eventsourcing/view/metadata.go +++ /dev/null @@ -1,73 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/domain" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/iam/repository/view" - "github.com/caos/zitadel/internal/iam/repository/view/model" - global_view "github.com/caos/zitadel/internal/view/repository" -) - -const ( - metadataTable = "management.metadata" -) - -func (v *View) MetadataByKey(aggregateID, key string) (*model.MetadataView, error) { - return view.MetadataByKey(v.Db, metadataTable, aggregateID, key) -} - -func (v *View) MetadataByKeyAndResourceOwner(aggregateID, resourceOwner, key string) (*model.MetadataView, error) { - return view.MetadataByKeyAndResourceOwner(v.Db, metadataTable, aggregateID, resourceOwner, key) -} - -func (v *View) MetadataListByAggregateID(aggregateID string) ([]*model.MetadataView, error) { - return view.GetMetadataList(v.Db, metadataTable, aggregateID) -} - -func (v *View) SearchMetadata(request *domain.MetadataSearchRequest) ([]*model.MetadataView, uint64, error) { - return view.SearchMetadata(v.Db, metadataTable, request) -} - -func (v *View) PutMetadata(template *model.MetadataView, event *models.Event) error { - err := view.PutMetadata(v.Db, metadataTable, template) - if err != nil { - return err - } - return v.ProcessedMetadataSequence(event) -} - -func (v *View) DeleteMetadata(aggregateID, key string, event *models.Event) error { - err := view.DeleteMetadata(v.Db, metadataTable, aggregateID, key) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedMetadataSequence(event) -} - -func (v *View) DeleteMetadataByAggregateID(aggregateID string, event *models.Event) error { - err := view.DeleteMetadataByAggregateID(v.Db, metadataTable, aggregateID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedMetadataSequence(event) -} -func (v *View) GetLatestMetadataSequence() (*global_view.CurrentSequence, error) { - return v.latestSequence(metadataTable) -} - -func (v *View) ProcessedMetadataSequence(event *models.Event) error { - return v.saveCurrentSequence(metadataTable, event) -} - -func (v *View) UpdateMetadataSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(metadataTable) -} - -func (v *View) GetLatestMetadataFailedEvent(sequence uint64) (*global_view.FailedEvent, error) { - return v.latestFailedEvent(metadataTable, sequence) -} - -func (v *View) ProcessedMetadataFailedEvent(failedEvent *global_view.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/management/repository/eventsourcing/view/sequence.go b/internal/management/repository/eventsourcing/view/sequence.go deleted file mode 100644 index 58f792f94a..0000000000 --- a/internal/management/repository/eventsourcing/view/sequence.go +++ /dev/null @@ -1,32 +0,0 @@ -package view - -import ( - "time" - - "github.com/caos/zitadel/internal/eventstore/v1/models" - "github.com/caos/zitadel/internal/view/repository" -) - -const ( - sequencesTable = "management.current_sequences" -) - -func (v *View) saveCurrentSequence(viewName string, event *models.Event) error { - return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.Sequence, event.CreationDate) -} - -func (v *View) latestSequence(viewName string) (*repository.CurrentSequence, error) { - return repository.LatestSequence(v.Db, sequencesTable, viewName) -} - -func (v *View) updateSpoolerRunSequence(viewName string) error { - currentSequence, err := repository.LatestSequence(v.Db, sequencesTable, viewName) - if err != nil { - return err - } - if currentSequence.ViewName == "" { - currentSequence.ViewName = viewName - } - currentSequence.LastSuccessfulSpoolerRun = time.Now() - return repository.UpdateCurrentSequence(v.Db, sequencesTable, currentSequence) -} diff --git a/internal/management/repository/eventsourcing/view/user.go b/internal/management/repository/eventsourcing/view/user.go deleted file mode 100644 index 17fbf15535..0000000000 --- a/internal/management/repository/eventsourcing/view/user.go +++ /dev/null @@ -1,90 +0,0 @@ -package view - -import ( - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/eventstore/v1/models" - usr_model "github.com/caos/zitadel/internal/user/model" - "github.com/caos/zitadel/internal/user/repository/view" - "github.com/caos/zitadel/internal/user/repository/view/model" - "github.com/caos/zitadel/internal/view/repository" -) - -const ( - userTable = "management.users" -) - -func (v *View) UserByID(userID string) (*model.UserView, error) { - return view.UserByID(v.Db, userTable, userID) -} - -func (v *View) UserByIDAndResourceOwner(userID, resourceOwner string) (*model.UserView, error) { - return view.UserByIDAndResourceOwner(v.Db, userTable, userID, resourceOwner) -} - -func (v *View) SearchUsers(request *usr_model.UserSearchRequest) ([]*model.UserView, uint64, error) { - return view.SearchUsers(v.Db, userTable, request) -} - -func (v *View) GetGlobalUserByLoginName(loginName string) (*model.UserView, error) { - return view.GetGlobalUserByLoginName(v.Db, userTable, loginName) -} - -func (v *View) UsersByOrgID(orgID string) ([]*model.UserView, error) { - return view.UsersByOrgID(v.Db, userTable, orgID) -} - -func (v *View) UserIDsByDomain(domain string) ([]string, error) { - return view.UserIDsByDomain(v.Db, userTable, domain) -} - -func (v *View) IsUserUnique(userName, email, orgID string) (bool, error) { - return view.IsUserUnique(v.Db, userTable, userName, email, orgID) -} - -func (v *View) UserMFAs(userID string) ([]*usr_model.MultiFactor, error) { - return view.UserMFAs(v.Db, userTable, userID) -} - -func (v *View) PutUsers(user []*model.UserView, event *models.Event) error { - err := view.PutUsers(v.Db, userTable, user...) - if err != nil { - return err - } - return v.ProcessedUserSequence(event) -} - -func (v *View) PutUser(user *model.UserView, event *models.Event) error { - err := view.PutUser(v.Db, userTable, user) - if err != nil { - return err - } - return v.ProcessedUserSequence(event) -} - -func (v *View) DeleteUser(userID string, event *models.Event) error { - err := view.DeleteUser(v.Db, userTable, userID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedUserSequence(event) -} - -func (v *View) GetLatestUserSequence() (*repository.CurrentSequence, error) { - return v.latestSequence(userTable) -} - -func (v *View) ProcessedUserSequence(event *models.Event) error { - return v.saveCurrentSequence(userTable, event) -} - -func (v *View) UpdateUserSpoolerRunTimestamp() error { - return v.updateSpoolerRunSequence(userTable) -} - -func (v *View) GetLatestUserFailedEvent(sequence uint64) (*repository.FailedEvent, error) { - return v.latestFailedEvent(userTable, sequence) -} - -func (v *View) ProcessedUserFailedEvent(failedEvent *repository.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/management/repository/eventsourcing/view/view.go b/internal/management/repository/eventsourcing/view/view.go deleted file mode 100644 index 4b8c52392d..0000000000 --- a/internal/management/repository/eventsourcing/view/view.go +++ /dev/null @@ -1,25 +0,0 @@ -package view - -import ( - "database/sql" - - "github.com/jinzhu/gorm" -) - -type View struct { - Db *gorm.DB -} - -func StartView(sqlClient *sql.DB) (*View, error) { - gorm, err := gorm.Open("postgres", sqlClient) - if err != nil { - return nil, err - } - return &View{ - Db: gorm, - }, nil -} - -func (v *View) Health() (err error) { - return v.Db.DB().Ping() -} diff --git a/internal/management/repository/repository.go b/internal/management/repository/repository.go index de30796981..496a80ed9b 100644 --- a/internal/management/repository/repository.go +++ b/internal/management/repository/repository.go @@ -1,7 +1,6 @@ package repository type Repository interface { - Health() error ProjectRepository OrgRepository UserRepository diff --git a/internal/management/repository/user.go b/internal/management/repository/user.go index d0933b3a9f..bc55066afc 100644 --- a/internal/management/repository/user.go +++ b/internal/management/repository/user.go @@ -4,29 +4,9 @@ import ( "context" "time" - "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/user/model" ) type UserRepository interface { - UserByID(ctx context.Context, id string) (*model.UserView, error) - UserByIDAndResourceOwner(ctx context.Context, id, resourceOwner string) (*model.UserView, error) - SearchUsers(ctx context.Context, request *model.UserSearchRequest, ensureLimit bool) (*model.UserSearchResponse, error) - UserIDsByDomain(ctx context.Context, domain string) ([]string, error) - - GetUserByLoginNameGlobal(ctx context.Context, email string) (*model.UserView, error) - IsUserUnique(ctx context.Context, userName, email, orgID string) (bool, error) - - GetMetadataByKey(ctx context.Context, userID, resourceOwner, key string) (*domain.Metadata, error) - SearchMetadata(ctx context.Context, userID, resourceOwner string, req *domain.MetadataSearchRequest) (*domain.MetadataSearchResponse, error) - UserChanges(ctx context.Context, id string, lastSequence uint64, limit uint64, sortAscending bool, retention time.Duration) (*model.UserChanges, error) - - ProfileByID(ctx context.Context, userID string) (*model.Profile, error) - - EmailByID(ctx context.Context, userID string) (*model.Email, error) - - PhoneByID(ctx context.Context, userID string) (*model.Phone, error) - - AddressByID(ctx context.Context, userID string) (*model.Address, error) } diff --git a/internal/query/iam_member.go b/internal/query/iam_member.go index e0237ea617..251081868d 100644 --- a/internal/query/iam_member.go +++ b/internal/query/iam_member.go @@ -5,9 +5,10 @@ import ( "database/sql" sq "github.com/Masterminds/squirrel" + "github.com/lib/pq" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/query/projection" - "github.com/lib/pq" ) var ( @@ -92,7 +93,7 @@ func prepareIAMMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, erro HumanLastNameCol.identifier(), HumanDisplayNameCol.identifier(), MachineNameCol.identifier(), - HumanAvaterURLCol.identifier(), + HumanAvatarURLCol.identifier(), countColumn.identifier(), ).From(iamMemberTable.identifier()). LeftJoin(join(HumanUserIDCol, IAMMemberUserID)). diff --git a/internal/query/org_member.go b/internal/query/org_member.go index f59f821417..2803244e31 100644 --- a/internal/query/org_member.go +++ b/internal/query/org_member.go @@ -95,7 +95,7 @@ func prepareOrgMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, erro HumanLastNameCol.identifier(), HumanDisplayNameCol.identifier(), MachineNameCol.identifier(), - HumanAvaterURLCol.identifier(), + HumanAvatarURLCol.identifier(), countColumn.identifier(), ).From(orgMemberTable.identifier()). LeftJoin(join(HumanUserIDCol, OrgMemberUserID)). diff --git a/internal/query/project_grant_member.go b/internal/query/project_grant_member.go index b3c03fdaf3..3cf1bd7cf3 100644 --- a/internal/query/project_grant_member.go +++ b/internal/query/project_grant_member.go @@ -5,9 +5,10 @@ import ( "database/sql" sq "github.com/Masterminds/squirrel" + "github.com/lib/pq" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/query/projection" - "github.com/lib/pq" ) var ( @@ -101,7 +102,7 @@ func prepareProjectGrantMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Memb HumanLastNameCol.identifier(), HumanDisplayNameCol.identifier(), MachineNameCol.identifier(), - HumanAvaterURLCol.identifier(), + HumanAvatarURLCol.identifier(), countColumn.identifier(), ).From(projectGrantMemberTable.identifier()). LeftJoin(join(HumanUserIDCol, ProjectGrantMemberUserID)). diff --git a/internal/query/project_member.go b/internal/query/project_member.go index 16ee79ab53..9489f0f445 100644 --- a/internal/query/project_member.go +++ b/internal/query/project_member.go @@ -5,9 +5,10 @@ import ( "database/sql" sq "github.com/Masterminds/squirrel" + "github.com/lib/pq" + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/query/projection" - "github.com/lib/pq" ) var ( @@ -94,7 +95,7 @@ func prepareProjectMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Members, HumanLastNameCol.identifier(), HumanDisplayNameCol.identifier(), MachineNameCol.identifier(), - HumanAvaterURLCol.identifier(), + HumanAvatarURLCol.identifier(), countColumn.identifier(), ).From(projectMemberTable.identifier()). LeftJoin(join(HumanUserIDCol, ProjectMemberUserID)). diff --git a/internal/query/projection/user.go b/internal/query/projection/user.go index 75ee0d8358..b442d6b7d6 100644 --- a/internal/query/projection/user.go +++ b/internal/query/projection/user.go @@ -54,7 +54,7 @@ const ( HumanDisplayNameCol = "display_name" HumanPreferredLanguageCol = "preferred_language" HumanGenderCol = "gender" - HumanAvaterURLCol = "avatar_key" + HumanAvatarURLCol = "avatar_key" // email HumanEmailCol = "email" @@ -601,7 +601,7 @@ func (p *UserProjection) reduceHumanAvatarAdded(event eventstore.Event) (*handle ), crdb.AddUpdateStatement( []handler.Column{ - handler.NewCol(HumanAvaterURLCol, e.StoreKey), + handler.NewCol(HumanAvatarURLCol, e.StoreKey), }, []handler.Condition{ handler.NewCond(HumanUserIDCol, e.Aggregate().ID), @@ -631,7 +631,7 @@ func (p *UserProjection) reduceHumanAvatarRemoved(event eventstore.Event) (*hand ), crdb.AddUpdateStatement( []handler.Column{ - handler.NewCol(HumanAvaterURLCol, nil), + handler.NewCol(HumanAvatarURLCol, nil), }, []handler.Condition{ handler.NewCond(HumanUserIDCol, e.Aggregate().ID), diff --git a/internal/query/search_query.go b/internal/query/search_query.go index 175c48ce84..4a958565d5 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -5,8 +5,9 @@ import ( "reflect" sq "github.com/Masterminds/squirrel" - "github.com/caos/zitadel/internal/domain" "github.com/lib/pq" + + "github.com/caos/zitadel/internal/domain" ) type SearchResponse struct { @@ -157,6 +158,7 @@ const ( TextContains TextContainsIgnoreCase TextListContains + TextNotEquals textCompareMax ) diff --git a/internal/query/user.go b/internal/query/user.go index 1b00d892d8..d2128cccc0 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -2,11 +2,100 @@ package query import ( "context" + "database/sql" + errs "errors" + "time" + sq "github.com/Masterminds/squirrel" + "github.com/lib/pq" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + + "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/query/projection" ) +type Users struct { + SearchResponse + Users []*User +} + +type User struct { + ID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + State domain.UserState + Type domain.UserType + Username string + LoginNames []string + PreferredLoginName string + Human *Human + Machine *Machine +} + +type Human struct { + FirstName string + LastName string + NickName string + DisplayName string + AvatarKey string + PreferredLanguage language.Tag + Gender domain.Gender + Email string + IsEmailVerified bool + Phone string + IsPhoneVerified bool +} + +type Profile struct { + ID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + FirstName string + LastName string + NickName string + DisplayName string + AvatarKey string + PreferredLanguage language.Tag + Gender domain.Gender +} + +type Email struct { + ID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + Email string + IsVerified bool +} + +type Phone struct { + ID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + Phone string + IsVerified bool +} + +type Machine struct { + Name string + Description string +} + +type UserSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + var ( userTable = table{ name: projection.UserTable, @@ -43,6 +132,13 @@ var ( name: projection.UserTypeCol, table: userTable, } + userLoginNamesTable = loginNameTable.setAlias("login_names") + userLoginNamesUserIDCol = LoginNameUserIDCol.setTable(userLoginNamesTable) + userLoginNamesCol = LoginNameNameCol.setTable(userLoginNamesTable) + userPreferredLoginNameTable = loginNameTable.setAlias("preferred_login_name") + userPreferredLoginNameUserIDCol = LoginNameUserIDCol.setTable(userPreferredLoginNameTable) + userPreferredLoginNameCol = LoginNameNameCol.setTable(userPreferredLoginNameTable) + userPreferredLoginNameIsPrimaryCol = LoginNameIsPrimaryCol.setTable(userPreferredLoginNameTable) ) var ( @@ -78,8 +174,8 @@ var ( name: projection.HumanGenderCol, table: humanTable, } - HumanAvaterURLCol = Column{ - name: projection.HumanAvaterURLCol, + HumanAvatarURLCol = Column{ + name: projection.HumanAvatarURLCol, table: humanTable, } @@ -122,7 +218,693 @@ var ( } ) +func (q *Queries) GetUserByID(ctx context.Context, userID string, queries ...SearchQuery) (*User, error) { + query, scan := prepareUserQuery() + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.Where( + sq.Eq{ + UserIDCol.identifier(): userID, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-FBg21", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + +func (q *Queries) GetUser(ctx context.Context, queries ...SearchQuery) (*User, error) { + query, scan := prepareUserQuery() + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Dnhr2", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + +func (q *Queries) GetHumanProfile(ctx context.Context, userID string, queries ...SearchQuery) (*Profile, error) { + query, scan := prepareProfileQuery() + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.Where( + sq.Eq{ + UserIDCol.identifier(): userID, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + +func (q *Queries) GetHumanEmail(ctx context.Context, userID string, queries ...SearchQuery) (*Email, error) { + query, scan := prepareEmailQuery() + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.Where( + sq.Eq{ + UserIDCol.identifier(): userID, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-BHhj3", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + +func (q *Queries) GetHumanPhone(ctx context.Context, userID string, queries ...SearchQuery) (*Phone, error) { + query, scan := preparePhoneQuery() + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.Where( + sq.Eq{ + UserIDCol.identifier(): userID, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + +func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries) (*Users, error) { + query, scan := prepareUsersQuery() + stmt, args, err := queries.toQuery(query).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment") + } + + rows, err := q.client.QueryContext(ctx, stmt, args...) + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-AG4gs", "Errors.Internal") + } + users, err := scan(rows) + if err != nil { + return nil, err + } + users.LatestSequence, err = q.latestSequence(ctx, userTable) + return users, err +} + +func (q *Queries) IsUserUnique(ctx context.Context, username, email, resourceOwner string) (bool, error) { + query, scan := prepareUserUniqueQuery() + queries := make([]SearchQuery, 0, 3) + if username != "" { + usernameQuery, err := NewUserUsernameSearchQuery(username, TextEquals) + if err != nil { + return false, err + } + queries = append(queries, usernameQuery) + } + if email != "" { + emailQuery, err := NewUserEmailSearchQuery(email, TextEquals) + if err != nil { + return false, err + } + queries = append(queries, emailQuery) + } + if resourceOwner != "" { + resourceOwnerQuery, err := NewUserResourceOwnerSearchQuery(resourceOwner, TextEquals) + if err != nil { + return false, err + } + queries = append(queries, resourceOwnerQuery) + } + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.ToSql() + if err != nil { + return false, errors.ThrowInternal(err, "QUERY-Dg43g", "Errors.Query.SQLStatment") + } + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + func (q *Queries) UserEvents(ctx context.Context, orgID, userID string, sequence uint64) ([]eventstore.Event, error) { query := NewUserEventSearchQuery(userID, orgID, sequence) return q.eventstore.Filter(ctx, query) } + +func (q *UserSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func (r *UserSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { + query, err := NewUserResourceOwnerSearchQuery(orgID, TextEquals) + if err != nil { + return err + } + r.Queries = append(r.Queries, query) + return nil +} + +func NewUserResourceOwnerSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(UserResourceOwnerCol, value, comparison) +} + +func NewUserUsernameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(UserUsernameCol, value, comparison) +} + +func NewUserFirstNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(HumanFirstNameCol, value, comparison) +} + +func NewUserLastNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(HumanLastNameCol, value, comparison) +} + +func NewUserNickNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(HumanNickNameCol, value, comparison) +} + +func NewUserDisplayNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(HumanDisplayNameCol, value, comparison) +} + +func NewUserEmailSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(HumanEmailCol, value, comparison) +} + +func NewUserStateSearchQuery(value int32) (SearchQuery, error) { + return NewNumberQuery(UserStateCol, value, NumberEquals) +} + +func NewUserTypeSearchQuery(value int32) (SearchQuery, error) { + return NewNumberQuery(UserTypeCol, value, NumberEquals) +} + +func NewUserPreferredLoginNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(userPreferredLoginNameCol, value, comparison) +} + +func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(userLoginNamesCol, value, TextListContains) +} + +func prepareUserQuery() (sq.SelectBuilder, func(*sql.Row) (*User, error)) { + loginNamesQuery, _, err := sq.Select( + userLoginNamesUserIDCol.identifier(), + "ARRAY_AGG("+userLoginNamesCol.identifier()+") as login_names"). + From(userLoginNamesTable.identifier()). + GroupBy(userLoginNamesUserIDCol.identifier()). + ToSql() + if err != nil { + return sq.SelectBuilder{}, nil + } + preferredLoginNameQuery, preferredLoginNameArgs, err := sq.Select( + userPreferredLoginNameUserIDCol.identifier(), + userPreferredLoginNameCol.identifier()). + From(userPreferredLoginNameTable.identifier()). + Where( + sq.Eq{ + userPreferredLoginNameIsPrimaryCol.identifier(): true, + }).ToSql() + if err != nil { + return sq.SelectBuilder{}, nil + } + return sq.Select( + UserIDCol.identifier(), + UserCreationDateCol.identifier(), + UserChangeDateCol.identifier(), + UserResourceOwnerCol.identifier(), + UserSequenceCol.identifier(), + UserStateCol.identifier(), + UserTypeCol.identifier(), + UserUsernameCol.identifier(), + "login_names.login_names", + userPreferredLoginNameCol.identifier(), + HumanUserIDCol.identifier(), + HumanFirstNameCol.identifier(), + HumanLastNameCol.identifier(), + HumanNickNameCol.identifier(), + HumanDisplayNameCol.identifier(), + HumanPreferredLanguageCol.identifier(), + HumanGenderCol.identifier(), + HumanAvatarURLCol.identifier(), + HumanEmailCol.identifier(), + HumanIsEmailVerifiedCol.identifier(), + HumanPhoneCol.identifier(), + HumanIsPhoneVerifiedCol.identifier(), + MachineUserIDCol.identifier(), + MachineNameCol.identifier(), + MachineDescriptionCol.identifier(), + ). + From(userTable.identifier()). + LeftJoin(join(HumanUserIDCol, UserIDCol)). + LeftJoin(join(MachineUserIDCol, UserIDCol)). + LeftJoin("("+loginNamesQuery+") as login_names on "+userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()). + LeftJoin("("+preferredLoginNameQuery+") as preferred_login_name on "+userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier(), preferredLoginNameArgs...). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*User, error) { + u := new(User) + loginNames := pq.StringArray{} + 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{} + + 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, + &email, + &isEmailVerified, + &phone, + &isPhoneVerified, + &machineID, + &name, + &description, + ) + + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-Dfbg2", "Errors.User.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-Bgah2", "Errors.Internal") + } + + u.LoginNames = loginNames + if preferredLoginName.Valid { + 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: email.String, + IsEmailVerified: isEmailVerified.Bool, + Phone: phone.String, + IsPhoneVerified: isPhoneVerified.Bool, + } + } else if machineID.Valid { + u.Machine = &Machine{ + Name: name.String, + Description: description.String, + } + } + return u, nil + } +} + +func prepareProfileQuery() (sq.SelectBuilder, func(*sql.Row) (*Profile, error)) { + return sq.Select( + UserIDCol.identifier(), + UserCreationDateCol.identifier(), + UserChangeDateCol.identifier(), + UserResourceOwnerCol.identifier(), + UserSequenceCol.identifier(), + HumanUserIDCol.identifier(), + HumanFirstNameCol.identifier(), + HumanLastNameCol.identifier(), + HumanNickNameCol.identifier(), + HumanDisplayNameCol.identifier(), + HumanPreferredLanguageCol.identifier(), + HumanGenderCol.identifier(), + HumanAvatarURLCol.identifier()). + From(userTable.identifier()). + LeftJoin(join(HumanUserIDCol, UserIDCol)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Profile, error) { + p := new(Profile) + + 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( + &p.ID, + &p.CreationDate, + &p.ChangeDate, + &p.ResourceOwner, + &p.Sequence, + &humanID, + &firstName, + &lastName, + &nickName, + &displayName, + &preferredLanguage, + &gender, + &avatarKey, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-HNhb3", "Errors.User.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-Rfheq", "Errors.Internal") + } + if !humanID.Valid { + return nil, errors.ThrowPreconditionFailed(nil, "QUERY-WLTce", "Errors.User.NotHuman") + } + + p.FirstName = firstName.String + p.LastName = lastName.String + p.NickName = nickName.String + p.DisplayName = displayName.String + p.AvatarKey = avatarKey.String + p.PreferredLanguage = language.Make(preferredLanguage.String) + p.Gender = domain.Gender(gender.Int32) + + return p, nil + } +} + +func prepareEmailQuery() (sq.SelectBuilder, func(*sql.Row) (*Email, error)) { + return sq.Select( + UserIDCol.identifier(), + UserCreationDateCol.identifier(), + UserChangeDateCol.identifier(), + UserResourceOwnerCol.identifier(), + UserSequenceCol.identifier(), + HumanUserIDCol.identifier(), + HumanEmailCol.identifier(), + HumanIsEmailVerifiedCol.identifier()). + From(userTable.identifier()). + LeftJoin(join(HumanUserIDCol, UserIDCol)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Email, error) { + e := new(Email) + + humanID := sql.NullString{} + email := sql.NullString{} + isEmailVerified := sql.NullBool{} + + err := row.Scan( + &e.ID, + &e.CreationDate, + &e.ChangeDate, + &e.ResourceOwner, + &e.Sequence, + &humanID, + &email, + &isEmailVerified, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-Hms2s", "Errors.User.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-Nu42d", "Errors.Internal") + } + if !humanID.Valid { + return nil, errors.ThrowPreconditionFailed(nil, "QUERY-pt7HY", "Errors.User.NotHuman") + } + + e.Email = email.String + e.IsVerified = isEmailVerified.Bool + + return e, nil + } +} + +func preparePhoneQuery() (sq.SelectBuilder, func(*sql.Row) (*Phone, error)) { + return sq.Select( + UserIDCol.identifier(), + UserCreationDateCol.identifier(), + UserChangeDateCol.identifier(), + UserResourceOwnerCol.identifier(), + UserSequenceCol.identifier(), + HumanUserIDCol.identifier(), + HumanPhoneCol.identifier(), + HumanIsPhoneVerifiedCol.identifier()). + From(userTable.identifier()). + LeftJoin(join(HumanUserIDCol, UserIDCol)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Phone, error) { + e := new(Phone) + + humanID := sql.NullString{} + phone := sql.NullString{} + isPhoneVerified := sql.NullBool{} + + err := row.Scan( + &e.ID, + &e.CreationDate, + &e.ChangeDate, + &e.ResourceOwner, + &e.Sequence, + &humanID, + &phone, + &isPhoneVerified, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-DAvb3", "Errors.User.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-Bmf2h", "Errors.Internal") + } + if !humanID.Valid { + return nil, errors.ThrowPreconditionFailed(nil, "QUERY-hliQl", "Errors.User.NotHuman") + } + + e.Phone = phone.String + e.IsVerified = isPhoneVerified.Bool + + return e, nil + } +} + +func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) { + return sq.Select( + UserIDCol.identifier(), + UserStateCol.identifier(), + UserUsernameCol.identifier(), + HumanUserIDCol.identifier(), + HumanEmailCol.identifier(), + HumanIsEmailVerifiedCol.identifier()). + From(userTable.identifier()). + LeftJoin(join(HumanUserIDCol, UserIDCol)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (bool, error) { + userID := sql.NullString{} + state := sql.NullInt32{} + username := sql.NullString{} + humanID := sql.NullString{} + email := sql.NullString{} + isEmailVerified := sql.NullBool{} + + err := row.Scan( + &userID, + &state, + &username, + &humanID, + &email, + &isEmailVerified, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return true, nil + } + return false, errors.ThrowInternal(err, "QUERY-Cxces", "Errors.Internal") + } + return !userID.Valid, nil + } +} + +func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) { + loginNamesQuery, _, err := sq.Select( + userLoginNamesUserIDCol.identifier(), + "ARRAY_AGG("+userLoginNamesCol.identifier()+") as login_names"). + From(userLoginNamesTable.identifier()). + GroupBy(userLoginNamesUserIDCol.identifier()). + ToSql() + if err != nil { + return sq.SelectBuilder{}, nil + } + preferredLoginNameQuery, preferredLoginNameArgs, err := sq.Select( + userPreferredLoginNameUserIDCol.identifier(), + userPreferredLoginNameCol.identifier()). + From(userPreferredLoginNameTable.identifier()). + Where( + sq.Eq{ + userPreferredLoginNameIsPrimaryCol.identifier(): true, + }).ToSql() + if err != nil { + return sq.SelectBuilder{}, nil + } + return sq.Select( + UserIDCol.identifier(), + UserCreationDateCol.identifier(), + UserChangeDateCol.identifier(), + UserResourceOwnerCol.identifier(), + UserSequenceCol.identifier(), + UserStateCol.identifier(), + UserTypeCol.identifier(), + UserUsernameCol.identifier(), + "login_names.login_names", + userPreferredLoginNameCol.identifier(), + HumanUserIDCol.identifier(), + HumanFirstNameCol.identifier(), + HumanLastNameCol.identifier(), + HumanNickNameCol.identifier(), + HumanDisplayNameCol.identifier(), + HumanPreferredLanguageCol.identifier(), + HumanGenderCol.identifier(), + HumanAvatarURLCol.identifier(), + HumanEmailCol.identifier(), + HumanIsEmailVerifiedCol.identifier(), + HumanPhoneCol.identifier(), + HumanIsPhoneVerifiedCol.identifier(), + MachineUserIDCol.identifier(), + MachineNameCol.identifier(), + MachineDescriptionCol.identifier(), + countColumn.identifier()). + From(userTable.identifier()). + LeftJoin(join(HumanUserIDCol, UserIDCol)). + LeftJoin(join(MachineUserIDCol, UserIDCol)). + LeftJoin("("+loginNamesQuery+") as login_names on "+userLoginNamesUserIDCol.identifier()+" = "+UserIDCol.identifier()). + LeftJoin("("+preferredLoginNameQuery+") as preferred_login_name on "+userPreferredLoginNameUserIDCol.identifier()+" = "+UserIDCol.identifier(), preferredLoginNameArgs...). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*Users, error) { + users := make([]*User, 0) + var count uint64 + for rows.Next() { + u := new(User) + loginNames := pq.StringArray{} + 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{} + + err := rows.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, + &email, + &isEmailVerified, + &phone, + &isPhoneVerified, + &machineID, + &name, + &description, + &count, + ) + if err != nil { + return nil, err + } + + u.LoginNames = loginNames + if preferredLoginName.Valid { + 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: email.String, + IsEmailVerified: isEmailVerified.Bool, + Phone: phone.String, + IsPhoneVerified: isPhoneVerified.Bool, + } + } else if machineID.Valid { + u.Machine = &Machine{ + Name: name.String, + Description: description.String, + } + } + + users = append(users, u) + } + + if err := rows.Close(); err != nil { + return nil, errors.ThrowInternal(err, "QUERY-frhbd", "Errors.Query.CloseRows") + } + + return &Users{ + Users: users, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 847db69518..c288cf0535 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -244,7 +244,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro HumanLastNameCol.identifier(), HumanEmailCol.identifier(), HumanDisplayNameCol.identifier(), - HumanAvaterURLCol.identifier(), + HumanAvatarURLCol.identifier(), UserGrantResourceOwner.identifier(), OrgColumnName.identifier(), @@ -347,7 +347,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e HumanLastNameCol.identifier(), HumanEmailCol.identifier(), HumanDisplayNameCol.identifier(), - HumanAvaterURLCol.identifier(), + HumanAvatarURLCol.identifier(), UserGrantResourceOwner.identifier(), OrgColumnName.identifier(), diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go new file mode 100644 index 0000000000..9fa546bb36 --- /dev/null +++ b/internal/query/user_metadata.go @@ -0,0 +1,210 @@ +package query + +import ( + "context" + "database/sql" + errs "errors" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query/projection" +) + +type UserMetadataList struct { + SearchResponse + Metadata []*UserMetadata +} + +type UserMetadata struct { + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + Sequence uint64 + Key string + Value []byte +} + +type UserMetadataSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +var ( + userMetadataTable = table{ + name: projection.UserMetadataProjectionTable, + } + UserMetadataUserIDCol = Column{ + name: projection.UserMetadataColumnUserID, + table: userMetadataTable, + } + UserMetadataCreationDateCol = Column{ + name: projection.UserMetadataColumnCreationDate, + table: userMetadataTable, + } + UserMetadataChangeDateCol = Column{ + name: projection.UserMetadataColumnChangeDate, + table: userMetadataTable, + } + UserMetadataResourceOwnerCol = Column{ + name: projection.UserMetadataColumnResourceOwner, + table: userMetadataTable, + } + UserMetadataSequenceCol = Column{ + name: projection.UserMetadataColumnSequence, + table: userMetadataTable, + } + UserMetadataKeyCol = Column{ + name: projection.UserMetadataColumnKey, + table: userMetadataTable, + } + UserMetadataValueCol = Column{ + name: projection.UserMetadataColumnValue, + table: userMetadataTable, + } +) + +func (q *Queries) GetUserMetadataByKey(ctx context.Context, userID, key string, queries ...SearchQuery) (*UserMetadata, error) { + query, scan := prepareUserMetadataQuery() + for _, q := range queries { + query = q.toQuery(query) + } + stmt, args, err := query.Where( + sq.Eq{ + UserMetadataUserIDCol.identifier(): userID, + UserMetadataKeyCol.identifier(): key, + }).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-aDGG2", "Errors.Query.SQLStatment") + } + + row := q.client.QueryRowContext(ctx, stmt, args...) + return scan(row) +} + +func (q *Queries) SearchUserMetadata(ctx context.Context, userID string, queries *UserMetadataSearchQueries) (*UserMetadataList, error) { + query, scan := prepareUserMetadataListQuery() + stmt, args, err := queries.toQuery(query).Where( + sq.Eq{ + UserMetadataUserIDCol.identifier(): userID, + }). + ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Egbgd", "Errors.Query.SQLStatment") + } + + rows, err := q.client.QueryContext(ctx, stmt, args...) + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Hr2wf", "Errors.Internal") + } + metadata, err := scan(rows) + if err != nil { + return nil, err + } + metadata.LatestSequence, err = q.latestSequence(ctx, userTable) + return metadata, err +} + +func (q *UserMetadataSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func (r *UserMetadataSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { + query, err := NewUserMetadataResourceOwnerSearchQuery(orgID) + if err != nil { + return err + } + r.Queries = append(r.Queries, query) + return nil +} + +func NewUserMetadataResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(UserMetadataResourceOwnerCol, value, TextEquals) +} + +func NewUserMetadataKeySearchQuery(value string, comparison TextComparison) (SearchQuery, error) { + return NewTextQuery(UserMetadataKeyCol, value, comparison) +} + +func prepareUserMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*UserMetadata, error)) { + return sq.Select( + UserMetadataCreationDateCol.identifier(), + UserMetadataChangeDateCol.identifier(), + UserMetadataResourceOwnerCol.identifier(), + UserMetadataSequenceCol.identifier(), + UserMetadataKeyCol.identifier(), + UserMetadataValueCol.identifier(), + ). + From(userMetadataTable.identifier()). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*UserMetadata, error) { + m := new(UserMetadata) + err := row.Scan( + &m.CreationDate, + &m.ChangeDate, + &m.ResourceOwner, + &m.Sequence, + &m.Key, + &m.Value, + ) + + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-Rgh32", "Errors.User.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-Hhjt2", "Errors.Internal") + } + return m, nil + } +} + +func prepareUserMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserMetadataList, error)) { + return sq.Select( + UserMetadataCreationDateCol.identifier(), + UserMetadataChangeDateCol.identifier(), + UserMetadataResourceOwnerCol.identifier(), + UserMetadataSequenceCol.identifier(), + UserMetadataKeyCol.identifier(), + UserMetadataValueCol.identifier(), + countColumn.identifier()). + From(userMetadataTable.identifier()). + LeftJoin(join(HumanUserIDCol, UserIDCol)). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*UserMetadataList, error) { + metadata := make([]*UserMetadata, 0) + var count uint64 + for rows.Next() { + m := new(UserMetadata) + err := rows.Scan( + &m.CreationDate, + &m.ChangeDate, + &m.ResourceOwner, + &m.Sequence, + &m.Key, + &m.Value, + &count, + ) + if err != nil { + return nil, err + } + + metadata = append(metadata, m) + } + + if err := rows.Close(); err != nil { + return nil, errors.ThrowInternal(err, "QUERY-sd3gh", "Errors.Query.CloseRows") + } + + return &UserMetadataList{ + Metadata: metadata, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} diff --git a/internal/query/user_metadata_test.go b/internal/query/user_metadata_test.go new file mode 100644 index 0000000000..4544755766 --- /dev/null +++ b/internal/query/user_metadata_test.go @@ -0,0 +1,248 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + + errs "github.com/caos/zitadel/internal/errors" +) + +var ( + userMetadataQuery = `SELECT zitadel.projections.user_metadata.creation_date,` + + ` zitadel.projections.user_metadata.change_date,` + + ` zitadel.projections.user_metadata.resource_owner,` + + ` zitadel.projections.user_metadata.sequence,` + + ` zitadel.projections.user_metadata.key,` + + ` zitadel.projections.user_metadata.value` + + ` FROM zitadel.projections.user_metadata` + userMetadataCols = []string{ + "creation_date", + "change_date", + "resource_owner", + "sequence", + "key", + "value", + } + userMetadataListQuery = `SELECT zitadel.projections.user_metadata.creation_date,` + + ` zitadel.projections.user_metadata.change_date,` + + ` zitadel.projections.user_metadata.resource_owner,` + + ` zitadel.projections.user_metadata.sequence,` + + ` zitadel.projections.user_metadata.key,` + + ` zitadel.projections.user_metadata.value,` + + ` COUNT(*) OVER ()` + + ` FROM zitadel.projections.user_metadata` + userMetadataListCols = []string{ + "creation_date", + "change_date", + "resource_owner", + "sequence", + "key", + "value", + "count", + } +) + +func Test_UserMetadataPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareUserMetadataQuery no result", + prepare: prepareUserMetadataQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userMetadataQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*UserMetadata)(nil), + }, + { + name: "prepareUserMetadataQuery found", + prepare: prepareUserMetadataQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userMetadataQuery), + userMetadataCols, + []driver.Value{ + testNow, + testNow, + "resource_owner", + uint64(20211108), + "key", + []byte("value"), + }, + ), + }, + object: &UserMetadata{ + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + Key: "key", + Value: []byte("value"), + }, + }, + { + name: "prepareUserMetadataQuery sql err", + prepare: prepareUserMetadataQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(userMetadataQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + { + name: "prepareUserMetadataListQuery no result", + prepare: prepareUserMetadataListQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(userMetadataListQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: &UserMetadataList{Metadata: []*UserMetadata{}}, + }, + { + name: "prepareUserMetadataListQuery one result", + prepare: prepareUserMetadataListQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(userMetadataListQuery), + userMetadataListCols, + [][]driver.Value{ + { + testNow, + testNow, + "resource_owner", + uint64(20211108), + "key", + []byte("value"), + }, + }, + ), + }, + object: &UserMetadataList{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Metadata: []*UserMetadata{ + { + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + Key: "key", + Value: []byte("value"), + }, + }, + }, + }, + { + name: "prepareUserMetadataListQuery multiple results", + prepare: prepareUserMetadataListQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(userMetadataListQuery), + userMetadataListCols, + [][]driver.Value{ + { + testNow, + testNow, + "resource_owner", + uint64(20211108), + "key", + []byte("value"), + }, + { + testNow, + testNow, + "resource_owner", + uint64(20211108), + "key2", + []byte("value2"), + }, + }, + ), + }, + object: &UserMetadataList{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Metadata: []*UserMetadata{ + { + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + Key: "key", + Value: []byte("value"), + }, + { + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + Key: "key2", + Value: []byte("value2"), + }, + }, + }, + }, + { + name: "prepareUserMetadataListQuery sql err", + prepare: prepareUserMetadataListQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(userMetadataListQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/query/user_test.go b/internal/query/user_test.go new file mode 100644 index 0000000000..176d0fd5c0 --- /dev/null +++ b/internal/query/user_test.go @@ -0,0 +1,1004 @@ +package query + +import ( + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/lib/pq" + "golang.org/x/text/language" + + "github.com/caos/zitadel/internal/domain" + errs "github.com/caos/zitadel/internal/errors" +) + +var ( + userQuery = `SELECT zitadel.projections.users.id,` + + ` zitadel.projections.users.creation_date,` + + ` zitadel.projections.users.change_date,` + + ` zitadel.projections.users.resource_owner,` + + ` zitadel.projections.users.sequence,` + + ` zitadel.projections.users.state,` + + ` zitadel.projections.users.type,` + + ` zitadel.projections.users.username,` + + ` login_names.login_names,` + + ` preferred_login_name.login_name,` + + ` zitadel.projections.users_humans.user_id,` + + ` zitadel.projections.users_humans.first_name,` + + ` zitadel.projections.users_humans.last_name,` + + ` zitadel.projections.users_humans.nick_name,` + + ` zitadel.projections.users_humans.display_name,` + + ` zitadel.projections.users_humans.preferred_language,` + + ` zitadel.projections.users_humans.gender,` + + ` zitadel.projections.users_humans.avatar_key,` + + ` zitadel.projections.users_humans.email,` + + ` zitadel.projections.users_humans.is_email_verified,` + + ` zitadel.projections.users_humans.phone,` + + ` zitadel.projections.users_humans.is_phone_verified,` + + ` zitadel.projections.users_machines.user_id,` + + ` zitadel.projections.users_machines.name,` + + ` zitadel.projections.users_machines.description` + + ` FROM zitadel.projections.users` + + ` LEFT JOIN zitadel.projections.users_humans ON zitadel.projections.users.id = zitadel.projections.users_humans.user_id` + + ` LEFT JOIN zitadel.projections.users_machines ON zitadel.projections.users.id = zitadel.projections.users_machines.user_id` + + ` LEFT JOIN` + + ` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name) as login_names` + + ` FROM zitadel.projections.login_names as login_names` + + ` GROUP BY login_names.user_id) as login_names` + + ` on login_names.user_id = zitadel.projections.users.id` + + ` LEFT JOIN` + + ` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM zitadel.projections.login_names as preferred_login_name WHERE preferred_login_name.is_primary = $1) as preferred_login_name` + + ` on preferred_login_name.user_id = zitadel.projections.users.id` + userCols = []string{ + "id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "state", + "type", + "username", + "login_names", + "login_name", + //human + "user_id", + "first_name", + "last_name", + "nick_name", + "display_name", + "preferred_language", + "gender", + "avatar_key", + "email", + "is_email_verified", + "phone", + "is_phone_verified", + //machine + "user_id", + "name", + "description", + } + profileQuery = `SELECT zitadel.projections.users.id,` + + ` zitadel.projections.users.creation_date,` + + ` zitadel.projections.users.change_date,` + + ` zitadel.projections.users.resource_owner,` + + ` zitadel.projections.users.sequence,` + + //` zitadel.projections.users.state,` + //TODO: + ` zitadel.projections.users_humans.user_id,` + + ` zitadel.projections.users_humans.first_name,` + + ` zitadel.projections.users_humans.last_name,` + + ` zitadel.projections.users_humans.nick_name,` + + ` zitadel.projections.users_humans.display_name,` + + ` zitadel.projections.users_humans.preferred_language,` + + ` zitadel.projections.users_humans.gender,` + + ` zitadel.projections.users_humans.avatar_key` + + ` FROM zitadel.projections.users` + + ` LEFT JOIN zitadel.projections.users_humans ON zitadel.projections.users.id = zitadel.projections.users_humans.user_id` + profileCols = []string{ + "id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + //"state", //TODO: + "user_id", + "first_name", + "last_name", + "nick_name", + "display_name", + "preferred_language", + "gender", + "avatar_key", + } + emailQuery = `SELECT zitadel.projections.users.id,` + + ` zitadel.projections.users.creation_date,` + + ` zitadel.projections.users.change_date,` + + ` zitadel.projections.users.resource_owner,` + + ` zitadel.projections.users.sequence,` + + //` zitadel.projections.users.state,` + //TODO: + ` zitadel.projections.users_humans.user_id,` + + ` zitadel.projections.users_humans.email,` + + ` zitadel.projections.users_humans.is_email_verified` + + ` FROM zitadel.projections.users` + + ` LEFT JOIN zitadel.projections.users_humans ON zitadel.projections.users.id = zitadel.projections.users_humans.user_id` + emailCols = []string{ + "id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + //"state", //TODO: + "user_id", + "email", + "is_email_verified", + } + phoneQuery = `SELECT zitadel.projections.users.id,` + + ` zitadel.projections.users.creation_date,` + + ` zitadel.projections.users.change_date,` + + ` zitadel.projections.users.resource_owner,` + + ` zitadel.projections.users.sequence,` + + //` zitadel.projections.users.state,` + //TODO: + ` zitadel.projections.users_humans.user_id,` + + ` zitadel.projections.users_humans.phone,` + + ` zitadel.projections.users_humans.is_phone_verified` + + ` FROM zitadel.projections.users` + + ` LEFT JOIN zitadel.projections.users_humans ON zitadel.projections.users.id = zitadel.projections.users_humans.user_id` + phoneCols = []string{ + "id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + //"state", //TODO: + "user_id", + "phone", + "is_phone_verified", + } + + userUniqueQuery = `SELECT zitadel.projections.users.id,` + + ` zitadel.projections.users.state,` + + ` zitadel.projections.users.username,` + + //` login_names.login_names,` + + //` preferred_login_name.login_name,` + + ` zitadel.projections.users_humans.user_id,` + + ` zitadel.projections.users_humans.email,` + + ` zitadel.projections.users_humans.is_email_verified` + + ` FROM zitadel.projections.users` + + ` LEFT JOIN zitadel.projections.users_humans ON zitadel.projections.users.id = zitadel.projections.users_humans.user_id` + //` LEFT JOIN` + + //` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name) as login_names` + + //` FROM zitadel.projections.login_names as login_names` + + //` GROUP BY login_names.user_id) as login_names` + + //` on login_names.user_id = zitadel.projections.users.id` + + //` LEFT JOIN` + + //` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM zitadel.projections.login_names as preferred_login_name WHERE preferred_login_name.is_primary = $1) as preferred_login_name` + + //` on preferred_login_name.user_id = zitadel.projections.users.id` + userUniqueCols = []string{ + "id", + "state", + "username", + //"login_names", + //"login_name", + //human + "user_id", + "email", + "is_email_verified", + } + usersQuery = `SELECT zitadel.projections.users.id,` + + ` zitadel.projections.users.creation_date,` + + ` zitadel.projections.users.change_date,` + + ` zitadel.projections.users.resource_owner,` + + ` zitadel.projections.users.sequence,` + + ` zitadel.projections.users.state,` + + ` zitadel.projections.users.type,` + + ` zitadel.projections.users.username,` + + ` login_names.login_names,` + + ` preferred_login_name.login_name,` + + ` zitadel.projections.users_humans.user_id,` + + ` zitadel.projections.users_humans.first_name,` + + ` zitadel.projections.users_humans.last_name,` + + ` zitadel.projections.users_humans.nick_name,` + + ` zitadel.projections.users_humans.display_name,` + + ` zitadel.projections.users_humans.preferred_language,` + + ` zitadel.projections.users_humans.gender,` + + ` zitadel.projections.users_humans.avatar_key,` + + ` zitadel.projections.users_humans.email,` + + ` zitadel.projections.users_humans.is_email_verified,` + + ` zitadel.projections.users_humans.phone,` + + ` zitadel.projections.users_humans.is_phone_verified,` + + ` zitadel.projections.users_machines.user_id,` + + ` zitadel.projections.users_machines.name,` + + ` zitadel.projections.users_machines.description,` + + ` COUNT(*) OVER ()` + + ` FROM zitadel.projections.users` + + ` LEFT JOIN zitadel.projections.users_humans ON zitadel.projections.users.id = zitadel.projections.users_humans.user_id` + + ` LEFT JOIN zitadel.projections.users_machines ON zitadel.projections.users.id = zitadel.projections.users_machines.user_id` + + ` LEFT JOIN` + + ` (SELECT login_names.user_id, ARRAY_AGG(login_names.login_name) as login_names` + + ` FROM zitadel.projections.login_names as login_names` + + ` GROUP BY login_names.user_id) as login_names` + + ` on login_names.user_id = zitadel.projections.users.id` + + ` LEFT JOIN` + + ` (SELECT preferred_login_name.user_id, preferred_login_name.login_name FROM zitadel.projections.login_names as preferred_login_name WHERE preferred_login_name.is_primary = $1) as preferred_login_name` + + ` on preferred_login_name.user_id = zitadel.projections.users.id` + usersCols = []string{ + "id", + "creation_date", + "change_date", + "resource_owner", + "sequence", + "state", + "type", + "username", + "login_names", + "login_name", + //human + "user_id", + "first_name", + "last_name", + "nick_name", + "display_name", + "preferred_language", + "gender", + "avatar_key", + "email", + "is_email_verified", + "phone", + "is_phone_verified", + //machine + "user_id", + "name", + "description", + "count", + } +) + +func Test_UserPrepares(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + prepare interface{} + want want + object interface{} + }{ + { + name: "prepareUserQuery no result", + prepare: prepareUserQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*User)(nil), + }, + { + name: "prepareUserQuery human found", + prepare: prepareUserQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userQuery), + userCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeHuman, + "username", + pq.StringArray{"login_name1", "login_name2"}, + "login_name1", + //human + "id", + "first_name", + "last_name", + "nick_name", + "display_name", + "de", + domain.GenderUnspecified, + "avatar_key", + "email", + true, + "phone", + true, + //machine + nil, + nil, + nil, + }, + ), + }, + object: &User{ + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + State: domain.UserStateActive, + Type: domain.UserTypeHuman, + Username: "username", + LoginNames: []string{"login_name1", "login_name2"}, + PreferredLoginName: "login_name1", + Human: &Human{ + FirstName: "first_name", + LastName: "last_name", + NickName: "nick_name", + DisplayName: "display_name", + AvatarKey: "avatar_key", + PreferredLanguage: language.German, + Gender: domain.GenderUnspecified, + Email: "email", + IsEmailVerified: true, + Phone: "phone", + IsPhoneVerified: true, + }, + }, + }, + { + name: "prepareUserQuery machine found", + prepare: prepareUserQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userQuery), + userCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeMachine, + "username", + pq.StringArray{"login_name1", "login_name2"}, + "login_name1", + //human + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + //machine + "id", + "name", + "description", + }, + ), + }, + object: &User{ + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + State: domain.UserStateActive, + Type: domain.UserTypeMachine, + Username: "username", + LoginNames: []string{"login_name1", "login_name2"}, + PreferredLoginName: "login_name1", + Machine: &Machine{ + Name: "name", + Description: "description", + }, + }, + }, + { + name: "prepareUserQuery sql err", + prepare: prepareUserQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(userQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + { + name: "prepareProfileQuery no result", + prepare: prepareProfileQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(profileQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*Profile)(nil), + }, + { + name: "prepareProfileQuery human found", + prepare: prepareProfileQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(profileQuery), + profileCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + "id", + "first_name", + "last_name", + "nick_name", + "display_name", + "de", + domain.GenderUnspecified, + "avatar_key", + }, + ), + }, + object: &Profile{ + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + FirstName: "first_name", + LastName: "last_name", + NickName: "nick_name", + DisplayName: "display_name", + AvatarKey: "avatar_key", + PreferredLanguage: language.German, + Gender: domain.GenderUnspecified, + }, + }, + { + name: "prepareProfileQuery not human found (error)", + prepare: prepareProfileQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(profileQuery), + profileCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + }, + ), + err: func(err error) (error, bool) { + if !errs.IsPreconditionFailed(err) { + return fmt.Errorf("err should be zitadel.PredconditionError got: %w", err), false + } + return nil, true + }, + }, + object: (*Profile)(nil), + }, + { + name: "prepareProfileQuery sql err", + prepare: prepareProfileQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(profileQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + { + name: "prepareEmailQuery no result", + prepare: prepareEmailQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(emailQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*Email)(nil), + }, + { + name: "prepareEmailQuery human found", + prepare: prepareEmailQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(emailQuery), + emailCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + //domain.UserStateActive, + "id", + "email", + true, + }, + ), + }, + object: &Email{ + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + //State: domain.UserStateActive, + Email: "email", + IsVerified: true, + }, + }, + { + name: "prepareEmailQuery not human found (error)", + prepare: prepareEmailQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(emailQuery), + emailCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + //domain.UserStateActive, + nil, + nil, + nil, + }, + ), + err: func(err error) (error, bool) { + if !errs.IsPreconditionFailed(err) { + return fmt.Errorf("err should be zitadel.PredconditionError got: %w", err), false + } + return nil, true + }, + }, + object: (*Email)(nil), + }, + { + name: "prepareEmailQuery sql err", + prepare: prepareEmailQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(emailQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + { + name: "preparePhoneQuery no result", + prepare: preparePhoneQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(phoneQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: (*Phone)(nil), + }, + { + name: "preparePhoneQuery human found", + prepare: preparePhoneQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(phoneQuery), + phoneCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + //domain.UserStateActive, + "id", + "phone", + true, + }, + ), + }, + object: &Phone{ + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + //State: domain.UserStateActive, + Phone: "phone", + IsVerified: true, + }, + }, + { + name: "preparePhoneQuery not human found (error)", + prepare: preparePhoneQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(phoneQuery), + phoneCols, + []driver.Value{ + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + //domain.UserStateActive, + nil, + nil, + nil, + }, + ), + err: func(err error) (error, bool) { + if !errs.IsPreconditionFailed(err) { + return fmt.Errorf("err should be zitadel.PredconditionError got: %w", err), false + } + return nil, true + }, + }, + object: (*Phone)(nil), + }, + { + name: "preparePhoneQuery sql err", + prepare: preparePhoneQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(phoneQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + { + name: "prepareUserUniqueQuery no result", + prepare: prepareUserUniqueQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userUniqueQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: true, + }, + { + name: "prepareUserUniqueQuery found", + prepare: prepareUserUniqueQuery, + want: want{ + sqlExpectations: mockQuery( + regexp.QuoteMeta(userUniqueQuery), + userUniqueCols, + []driver.Value{ + "id", + domain.UserStateActive, + "username", + "id", + "email", + true, + }, + ), + }, + object: false, + }, + { + name: "prepareUserUniqueQuery sql err", + prepare: prepareUserUniqueQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(userUniqueQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + { + name: "prepareUsersQuery no result", + prepare: prepareUsersQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(usersQuery), + nil, + nil, + ), + err: func(err error) (error, bool) { + if !errs.IsNotFound(err) { + return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false + } + return nil, true + }, + }, + object: &Users{Users: []*User{}}, + }, + { + name: "prepareUsersQuery one result", + prepare: prepareUsersQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(usersQuery), + usersCols, + [][]driver.Value{ + { + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeHuman, + "username", + pq.StringArray{"login_name1", "login_name2"}, + "login_name1", + //human + "id", + "first_name", + "last_name", + "nick_name", + "display_name", + "de", + domain.GenderUnspecified, + "avatar_key", + "email", + true, + "phone", + true, + //machine + nil, + nil, + nil, + }, + }, + ), + }, + object: &Users{ + SearchResponse: SearchResponse{ + Count: 1, + }, + Users: []*User{ + { + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + State: domain.UserStateActive, + Type: domain.UserTypeHuman, + Username: "username", + LoginNames: []string{"login_name1", "login_name2"}, + PreferredLoginName: "login_name1", + Human: &Human{ + FirstName: "first_name", + LastName: "last_name", + NickName: "nick_name", + DisplayName: "display_name", + AvatarKey: "avatar_key", + PreferredLanguage: language.German, + Gender: domain.GenderUnspecified, + Email: "email", + IsEmailVerified: true, + Phone: "phone", + IsPhoneVerified: true, + }, + }, + }, + }, + }, + { + name: "prepareUsersQuery multiple results", + prepare: prepareUsersQuery, + want: want{ + sqlExpectations: mockQueries( + regexp.QuoteMeta(usersQuery), + usersCols, + [][]driver.Value{ + { + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeHuman, + "username", + pq.StringArray{"login_name1", "login_name2"}, + "login_name1", + //human + "id", + "first_name", + "last_name", + "nick_name", + "display_name", + "de", + domain.GenderUnspecified, + "avatar_key", + "email", + true, + "phone", + true, + //machine + nil, + nil, + nil, + }, + { + "id", + testNow, + testNow, + "resource_owner", + uint64(20211108), + domain.UserStateActive, + domain.UserTypeMachine, + "username", + pq.StringArray{"login_name1", "login_name2"}, + "login_name1", + //human + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + //machine + "id", + "name", + "description", + }, + }, + ), + }, + object: &Users{ + SearchResponse: SearchResponse{ + Count: 2, + }, + Users: []*User{ + { + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + State: domain.UserStateActive, + Type: domain.UserTypeHuman, + Username: "username", + LoginNames: []string{"login_name1", "login_name2"}, + PreferredLoginName: "login_name1", + Human: &Human{ + FirstName: "first_name", + LastName: "last_name", + NickName: "nick_name", + DisplayName: "display_name", + AvatarKey: "avatar_key", + PreferredLanguage: language.German, + Gender: domain.GenderUnspecified, + Email: "email", + IsEmailVerified: true, + Phone: "phone", + IsPhoneVerified: true, + }, + }, + { + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + ResourceOwner: "resource_owner", + Sequence: 20211108, + State: domain.UserStateActive, + Type: domain.UserTypeMachine, + Username: "username", + LoginNames: []string{"login_name1", "login_name2"}, + PreferredLoginName: "login_name1", + Machine: &Machine{ + Name: "name", + Description: "description", + }, + }, + }, + }, + }, + { + name: "prepareUsersQuery sql err", + prepare: prepareUsersQuery, + want: want{ + sqlExpectations: mockQueryErr( + regexp.QuoteMeta(usersQuery), + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + object: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) + }) + } +} diff --git a/internal/ui/login/handler/init_password_handler.go b/internal/ui/login/handler/init_password_handler.go index e1a9a8fa4d..ab0f90445a 100644 --- a/internal/ui/login/handler/init_password_handler.go +++ b/internal/ui/login/handler/init_password_handler.go @@ -1,11 +1,12 @@ package handler import ( - "github.com/caos/zitadel/internal/domain" "net/http" http_mw "github.com/caos/zitadel/internal/api/http/middleware" + "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query" ) const ( @@ -86,7 +87,12 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe if authReq != nil { userOrg = authReq.UserOrgID } - user, err := l.authRepo.UserByLoginName(setContext(r.Context(), userOrg), authReq.LoginName) + 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(), userOrg), loginName) if err != nil { l.renderInitPassword(w, r, authReq, authReq.UserID, "", err) return diff --git a/internal/ui/login/handler/login.go b/internal/ui/login/handler/login.go index 8edeeaf003..af19eec708 100644 --- a/internal/ui/login/handler/login.go +++ b/internal/ui/login/handler/login.go @@ -26,7 +26,6 @@ import ( "github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/static" _ "github.com/caos/zitadel/internal/ui/login/statik" - usr_model "github.com/caos/zitadel/internal/user/model" ) type Login struct { @@ -163,20 +162,16 @@ func (l *Login) Listen(ctx context.Context) { } func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string) ([]string, error) { - users, err := l.authRepo.SearchUsers(ctx, &usr_model.UserSearchRequest{ - Queries: []*usr_model.UserSearchQuery{ - { - Key: usr_model.UserSearchKeyPreferredLoginName, - Method: domain.SearchMethodEndsWithIgnoreCase, - Value: "@" + domain.NewIAMDomainName(orgName, l.iamDomain), - }, - }, - }) + loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+domain.NewIAMDomainName(orgName, l.iamDomain), query.TextEndsWithIgnoreCase) if err != nil { return nil, err } - userIDs := make([]string, len(users.Result)) - for i, user := range users.Result { + users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}}) + 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 diff --git a/internal/ui/login/handler/password_complexity_policy_handler.go b/internal/ui/login/handler/password_complexity_policy_handler.go index abb96d8f32..f948a0471a 100644 --- a/internal/ui/login/handler/password_complexity_policy_handler.go +++ b/internal/ui/login/handler/password_complexity_policy_handler.go @@ -33,7 +33,7 @@ func (l *Login) getPasswordComplexityPolicy(r *http.Request, authReq *domain.Aut } func (l *Login) getPasswordComplexityPolicyByUserID(r *http.Request, authReq *domain.AuthRequest, userID string) (*iam_model.PasswordComplexityPolicyView, string, error) { - user, err := l.authRepo.UserByID(r.Context(), userID) + user, err := l.query.GetUserByID(r.Context(), userID) if err != nil { return nil, "", nil } diff --git a/internal/ui/login/handler/password_reset_handler.go b/internal/ui/login/handler/password_reset_handler.go index 5ca6327843..90bac81e75 100644 --- a/internal/ui/login/handler/password_reset_handler.go +++ b/internal/ui/login/handler/password_reset_handler.go @@ -2,6 +2,8 @@ package handler import ( "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/query" + "net/http" ) @@ -15,7 +17,12 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) { l.renderError(w, r, authReq, err) return } - user, err := l.authRepo.UserByLoginName(setContext(r.Context(), authReq.UserOrgID), authReq.LoginName) + 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), loginName) if err != nil { l.renderPasswordResetDone(w, r, authReq, err) return