From e769b163ef01d509b7d69aaabfe3a8b8f8a56679 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:26:30 +0100 Subject: [PATCH] perf: user grant owner removed (#6962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: change logic for usergrants projection with no selects * fix: change logic for usergrants projection with one select * fix: move resource owner select to single function * fix: move resource owner select to single function * fix: changes after merge * fix: changes after merge --------- Co-authored-by: Tim Möhlmann --- internal/api/grpc/admin/export.go | 2 +- internal/api/grpc/auth/permission.go | 2 +- internal/api/grpc/auth/user.go | 6 +- internal/api/grpc/management/project.go | 4 +- internal/api/grpc/management/project_grant.go | 4 +- internal/api/grpc/management/user.go | 2 +- internal/api/grpc/management/user_grant.go | 4 +- internal/api/grpc/user/v2/user.go | 2 +- internal/api/oidc/client.go | 2 +- internal/api/saml/storage.go | 2 +- .../repository/eventsourcing/repository.go | 2 +- internal/query/embed/userinfo_by_id.sql | 2 +- internal/query/projection/instance_member.go | 2 +- internal/query/projection/main_test.go | 73 ++++ internal/query/projection/org_member.go | 2 +- .../query/projection/project_grant_member.go | 6 +- .../projection/project_grant_member_test.go | 3 - internal/query/projection/project_member.go | 2 +- internal/query/projection/user_grant.go | 126 +++---- internal/query/projection/user_grant_test.go | 353 ++++++++++++++++-- internal/query/user_grant.go | 33 +- internal/query/user_grant_test.go | 64 ++-- 22 files changed, 488 insertions(+), 210 deletions(-) create mode 100644 internal/query/projection/main_test.go diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index 0bfe18528..0be1e8ed4 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -978,7 +978,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p return nil, err } - queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}, true, false) + queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}, true) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/permission.go b/internal/api/grpc/auth/permission.go index 519361dd3..6ef9877dc 100644 --- a/internal/api/grpc/auth/permission.go +++ b/internal/api/grpc/auth/permission.go @@ -34,7 +34,7 @@ func (s *Server) ListMyProjectPermissions(ctx context.Context, _ *auth_pb.ListMy if err != nil { return nil, err } - userGrant, err := s.query.UserGrant(ctx, true, false, userGrantOrgID, userGrantProjectID, userGrantUserID) + userGrant, err := s.query.UserGrant(ctx, true, userGrantOrgID, userGrantProjectID, userGrantUserID) if err != nil { return nil, err } diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 251b08c1b..f32af2f74 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -32,7 +32,7 @@ func (s *Server) RemoveMyUser(ctx context.Context, _ *auth_pb.RemoveMyUserReques return nil, err } queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID}} - grants, err := s.query.UserGrants(ctx, queries, true, false) + grants, err := s.query.UserGrants(ctx, queries, true) if err != nil { return nil, err } @@ -152,7 +152,7 @@ func (s *Server) ListMyUserGrants(ctx context.Context, req *auth_pb.ListMyUserGr if err != nil { return nil, err } - res, err := s.query.UserGrants(ctx, queries, false, false) + res, err := s.query.UserGrants(ctx, queries, false) if err != nil { return nil, err } @@ -181,7 +181,7 @@ func (s *Server) ListMyProjectOrgs(ctx context.Context, req *auth_pb.ListMyProje return nil, err } - grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantProjectID, userGrantUserID}}, false, false) + grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantProjectID, userGrantUserID}}, false) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project.go b/internal/api/grpc/management/project.go index e0eaf21b1..e7fad52c5 100644 --- a/internal/api/grpc/management/project.go +++ b/internal/api/grpc/management/project.go @@ -229,7 +229,7 @@ func (s *Server) RemoveProject(ctx context.Context, req *mgmt_pb.RemoveProjectRe } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery}, - }, true, false) + }, true) if err != nil { return nil, err } @@ -314,7 +314,7 @@ func (s *Server) RemoveProjectRole(ctx context.Context, req *mgmt_pb.RemoveProje } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, rolesQuery}, - }, false, false) + }, false) if err != nil { return nil, err diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index 101068092..cea8e929a 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -90,7 +90,7 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, grantQuery}, - }, true, false) + }, true) if err != nil { return nil, err } @@ -138,7 +138,7 @@ func (s *Server) RemoveProjectGrant(ctx context.Context, req *mgmt_pb.RemoveProj } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, grantQuery}, - }, false, true) + }, false) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 26d66d612..ef7ed0d73 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -369,7 +369,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]* } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true, true) + }, true) if err != nil { return nil, nil, err } diff --git a/internal/api/grpc/management/user_grant.go b/internal/api/grpc/management/user_grant.go index b13a01976..8589b49e3 100644 --- a/internal/api/grpc/management/user_grant.go +++ b/internal/api/grpc/management/user_grant.go @@ -19,7 +19,7 @@ func (s *Server) GetUserGrantByID(ctx context.Context, req *mgmt_pb.GetUserGrant if err != nil { return nil, err } - grant, err := s.query.UserGrant(ctx, true, false, idQuery, ownerQuery) + grant, err := s.query.UserGrant(ctx, true, idQuery, ownerQuery) if err != nil { return nil, err } @@ -33,7 +33,7 @@ func (s *Server) ListUserGrants(ctx context.Context, req *mgmt_pb.ListUserGrantR if err != nil { return nil, err } - res, err := s.query.UserGrants(ctx, queries, false, false) + res, err := s.query.UserGrants(ctx, queries, false) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 146020cdf..c7516bdc0 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -315,7 +315,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]* } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true, true) + }, true) if err != nil { return nil, nil, err } diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index ca8156a66..a4a4adb1b 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -752,7 +752,7 @@ func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID strin queries = append(queries, userIDQuery) grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: queries, - }, true, false) + }, true) if err != nil { return nil, nil, err } diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index 54378bb91..fb051dec5 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -328,7 +328,7 @@ func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) ( projectQuery, userIDQuery, }, - }, true, false) + }, true) } type customAttribute struct { diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index f3d11e74d..ff9f60bdd 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -117,7 +117,7 @@ func (q queryViewWrapper) UserGrantsByProjectAndUserID(ctx context.Context, proj return nil, err } queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID}} - grants, err := q.Queries.UserGrants(ctx, queries, true, false) + grants, err := q.Queries.UserGrants(ctx, queries, true) if err != nil { return nil, err } diff --git a/internal/query/embed/userinfo_by_id.sql b/internal/query/embed/userinfo_by_id.sql index ad959e43f..80fd42ddb 100644 --- a/internal/query/embed/userinfo_by_id.sql +++ b/internal/query/embed/userinfo_by_id.sql @@ -34,7 +34,7 @@ metadata as ( -- get all user grants, needed for the orgs query user_grants as ( select id, grant_id, state, creation_date, change_date, sequence, user_id, roles, resource_owner, project_id - from projections.user_grants3 + from projections.user_grants4 where user_id = $1 and instance_id = $2 and project_id = any($3) diff --git a/internal/query/projection/instance_member.go b/internal/query/projection/instance_member.go index ef71a4bf5..375ab92dd 100644 --- a/internal/query/projection/instance_member.go +++ b/internal/query/projection/instance_member.go @@ -105,7 +105,7 @@ func (p *instanceMemberProjection) reduceAdded(event eventstore.Event) (*handler return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-pGNCu", "reduce.wrong.event.type %s", instance.MemberAddedEventType) } ctx := setMemberContext(e.Aggregate()) - userOwner, err := getResourceOwnerOfUser(ctx, p.es, e.Aggregate().InstanceID, e.UserID) + userOwner, err := getUserResourceOwner(ctx, p.es, e.Aggregate().InstanceID, e.UserID) if err != nil { return nil, err } diff --git a/internal/query/projection/main_test.go b/internal/query/projection/main_test.go new file mode 100644 index 000000000..ac5251921 --- /dev/null +++ b/internal/query/projection/main_test.go @@ -0,0 +1,73 @@ +package projection + +import ( + "database/sql" + "testing" + "time" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/eventstore/repository/mock" + action_repo "github.com/zitadel/zitadel/internal/repository/action" + iam_repo "github.com/zitadel/zitadel/internal/repository/instance" + key_repo "github.com/zitadel/zitadel/internal/repository/keypair" + "github.com/zitadel/zitadel/internal/repository/limits" + "github.com/zitadel/zitadel/internal/repository/org" + proj_repo "github.com/zitadel/zitadel/internal/repository/project" + quota_repo "github.com/zitadel/zitadel/internal/repository/quota" + usr_repo "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/repository/usergrant" +) + +type expect func(mockRepository *mock.MockRepository) + +func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore { + m := mock.NewRepo(t) + for _, e := range expects { + e(m) + } + es := eventstore.NewEventstore( + &eventstore.Config{ + Querier: m.MockQuerier, + Pusher: m.MockPusher, + }, + ) + iam_repo.RegisterEventMappers(es) + org.RegisterEventMappers(es) + usr_repo.RegisterEventMappers(es) + proj_repo.RegisterEventMappers(es) + quota_repo.RegisterEventMappers(es) + limits.RegisterEventMappers(es) + usergrant.RegisterEventMappers(es) + key_repo.RegisterEventMappers(es) + action_repo.RegisterEventMappers(es) + return es +} + +func expectFilter(events ...eventstore.Event) expect { + return func(m *mock.MockRepository) { + m.ExpectFilterEvents(events...) + } +} + +func expectFilterError(err error) expect { + return func(m *mock.MockRepository) { + m.ExpectFilterEventsError(err) + } +} + +func eventFromEventPusher(event eventstore.Command) *repository.Event { + data, _ := eventstore.EventData(event) + return &repository.Event{ + ID: "", + Seq: 0, + CreationDate: time.Time{}, + Typ: event.Type(), + Data: data, + EditorUser: event.Creator(), + Version: event.Aggregate().Version, + AggregateID: event.Aggregate().ID, + AggregateType: event.Aggregate().Type, + ResourceOwner: sql.NullString{String: event.Aggregate().ResourceOwner, Valid: event.Aggregate().ResourceOwner != ""}, + } +} diff --git a/internal/query/projection/org_member.go b/internal/query/projection/org_member.go index 37c6d241a..770d624cb 100644 --- a/internal/query/projection/org_member.go +++ b/internal/query/projection/org_member.go @@ -104,7 +104,7 @@ func (p *orgMemberProjection) reduceAdded(event eventstore.Event) (*handler.Stat return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-uYq4r", "reduce.wrong.event.type %s", org.MemberAddedEventType) } ctx := setMemberContext(e.Aggregate()) - userOwner, err := getResourceOwnerOfUser(ctx, p.es, e.Aggregate().InstanceID, e.UserID) + userOwner, err := getUserResourceOwner(ctx, p.es, e.Aggregate().InstanceID, e.UserID) if err != nil { return nil, err } diff --git a/internal/query/projection/project_grant_member.go b/internal/query/projection/project_grant_member.go index b86c02531..82288ac14 100644 --- a/internal/query/projection/project_grant_member.go +++ b/internal/query/projection/project_grant_member.go @@ -125,11 +125,7 @@ func (p *projectGrantMemberProjection) reduceAdded(event eventstore.Event) (*han return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-0EBQf", "reduce.wrong.event.type %s", project.GrantMemberAddedType) } ctx := setMemberContext(e.Aggregate()) - userOwner, err := getResourceOwnerOfUser(ctx, p.es, e.Aggregate().InstanceID, e.UserID) - if err != nil { - return nil, err - } - grantedOrg, err := getGrantedOrgOfGrantedProject(ctx, p.es, e.Aggregate().InstanceID, e.Aggregate().ID, e.GrantID) + userOwner, _, grantedOrg, err := getResourceOwners(ctx, p.es, e.Aggregate().InstanceID, e.UserID, e.Aggregate().ID, e.GrantID) if err != nil { return nil, err } diff --git a/internal/query/projection/project_grant_member_test.go b/internal/query/projection/project_grant_member_test.go index 3b7839fe8..8e020debb 100644 --- a/internal/query/projection/project_grant_member_test.go +++ b/internal/query/projection/project_grant_member_test.go @@ -56,9 +56,6 @@ func TestProjectGrantMemberProjection_reduces(t *testing.T) { "email1", true, ), - }, - ).appendFilterResponse( - []eventstore.Event{ project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org2").Aggregate, "grant", "org3", []string{}, diff --git a/internal/query/projection/project_member.go b/internal/query/projection/project_member.go index bf1f82f85..822e2e8d7 100644 --- a/internal/query/projection/project_member.go +++ b/internal/query/projection/project_member.go @@ -117,7 +117,7 @@ func (p *projectMemberProjection) reduceAdded(event eventstore.Event) (*handler. return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-bgx5Q", "reduce.wrong.event.type %s", project.MemberAddedType) } ctx := setMemberContext(e.Aggregate()) - userOwner, err := getResourceOwnerOfUser(ctx, p.es, e.Aggregate().InstanceID, e.UserID) + userOwner, err := getUserResourceOwner(ctx, p.es, e.Aggregate().InstanceID, e.UserID) if err != nil { return nil, err } diff --git a/internal/query/projection/user_grant.go b/internal/query/projection/user_grant.go index 963cad395..49f4e4cb2 100644 --- a/internal/query/projection/user_grant.go +++ b/internal/query/projection/user_grant.go @@ -18,7 +18,7 @@ import ( ) const ( - UserGrantProjectionTable = "projections.user_grants3" + UserGrantProjectionTable = "projections.user_grants4" UserGrantID = "id" UserGrantCreationDate = "creation_date" @@ -29,15 +29,11 @@ const ( UserGrantInstanceID = "instance_id" UserGrantUserID = "user_id" UserGrantResourceOwnerUser = "resource_owner_user" - UserGrantUserOwnerRemoved = "user_owner_removed" UserGrantProjectID = "project_id" UserGrantResourceOwnerProject = "resource_owner_project" - UserGrantProjectOwnerRemoved = "project_owner_removed" UserGrantGrantID = "grant_id" UserGrantGrantedOrg = "granted_org" - UserGrantGrantedOrgRemoved = "granted_org_removed" UserGrantRoles = "roles" - UserGrantOwnerRemoved = "owner_removed" ) type userGrantProjection struct { @@ -64,23 +60,15 @@ func (*userGrantProjection) Init() *old_handler.Check { handler.NewColumn(UserGrantInstanceID, handler.ColumnTypeText), handler.NewColumn(UserGrantUserID, handler.ColumnTypeText), handler.NewColumn(UserGrantResourceOwnerUser, handler.ColumnTypeText), - handler.NewColumn(UserGrantUserOwnerRemoved, handler.ColumnTypeBool, handler.Default(false)), handler.NewColumn(UserGrantProjectID, handler.ColumnTypeText), handler.NewColumn(UserGrantResourceOwnerProject, handler.ColumnTypeText), - handler.NewColumn(UserGrantProjectOwnerRemoved, handler.ColumnTypeBool, handler.Default(false)), handler.NewColumn(UserGrantGrantID, handler.ColumnTypeText), handler.NewColumn(UserGrantGrantedOrg, handler.ColumnTypeText), - handler.NewColumn(UserGrantGrantedOrgRemoved, handler.ColumnTypeBool, handler.Default(false)), handler.NewColumn(UserGrantRoles, handler.ColumnTypeTextArray, handler.Nullable()), - handler.NewColumn(UserGrantOwnerRemoved, handler.ColumnTypeBool, handler.Default(false)), }, handler.NewPrimaryKey(UserGrantInstanceID, UserGrantID), handler.WithIndex(handler.NewIndex("user_id", []string{UserGrantUserID})), handler.WithIndex(handler.NewIndex("resource_owner", []string{UserGrantResourceOwner})), - handler.WithIndex(handler.NewIndex("owner_removed", []string{UserGrantOwnerRemoved})), - handler.WithIndex(handler.NewIndex("user_owner_removed", []string{UserGrantUserOwnerRemoved})), - handler.WithIndex(handler.NewIndex("project_owner_removed", []string{UserGrantProjectOwnerRemoved})), - handler.WithIndex(handler.NewIndex("granted_org_removed", []string{UserGrantGrantedOrgRemoved})), ), ) } @@ -182,25 +170,11 @@ func (p *userGrantProjection) reduceAdded(event eventstore.Event) (*handler.Stat } ctx := setUserGrantContext(e.Aggregate()) - userOwner, err := getResourceOwnerOfUser(ctx, p.es, e.Aggregate().InstanceID, e.UserID) + userOwner, projectOwner, grantOwner, err := getResourceOwners(ctx, p.es, e.Aggregate().InstanceID, e.UserID, e.ProjectID, e.ProjectGrantID) if err != nil { return nil, err } - projectOwner := "" - grantOwner := "" - if e.ProjectGrantID != "" { - grantOwner, err = getGrantedOrgOfGrantedProject(ctx, p.es, e.Aggregate().InstanceID, e.ProjectID, e.ProjectGrantID) - if err != nil { - return nil, err - } - } else { - projectOwner, err = getResourceOwnerOfProject(ctx, p.es, e.Aggregate().InstanceID, e.ProjectID) - if err != nil { - return nil, err - } - } - return handler.NewCreateStatement( e, []handler.Column{ @@ -425,74 +399,60 @@ func (p *userGrantProjection) reduceOwnerRemoved(event eventstore.Event) (*handl ), nil } -func getResourceOwnerOfUser(ctx context.Context, es handler.EventStore, instanceID, aggID string) (string, error) { - events, err := es.Filter( - ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AwaitOpenTransactions(). - InstanceID(instanceID). - AddQuery(). - AggregateTypes(user.AggregateType). - AggregateIDs(aggID). - EventTypes(user.HumanRegisteredType, user.HumanAddedType, user.MachineAddedEventType). - Builder(), - ) - if err != nil { - return "", err - } - if len(events) != 1 { - return "", zerrors.ThrowNotFound(nil, "PROJ-0I92sp", "Errors.User.NotFound") - } - return events[0].Aggregate().ResourceOwner, nil +func getUserResourceOwner(ctx context.Context, es handler.EventStore, instanceID, userID string) (string, error) { + userRO, _, _, err := getResourceOwners(ctx, es, instanceID, userID, "", "") + return userRO, err } -func getResourceOwnerOfProject(ctx context.Context, es handler.EventStore, instanceID, aggID string) (string, error) { - events, err := es.Filter( - ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AwaitOpenTransactions(). - InstanceID(instanceID). - AddQuery(). - AggregateTypes(project.AggregateType). - AggregateIDs(aggID). - EventTypes(project.ProjectAddedType). - Builder(), - ) - if err != nil { - return "", err - } - if len(events) != 1 { - return "", zerrors.ThrowNotFound(nil, "PROJ-0I91sp", "Errors.Project.NotFound") - } - return events[0].Aggregate().ResourceOwner, nil -} +func getResourceOwners(ctx context.Context, es handler.EventStore, instanceID, userID, projectID, grantID string) (userRO string, projectRO string, grantedOrg string, err error) { + eventCount := 1 + builder := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AwaitOpenTransactions(). + InstanceID(instanceID). + AddQuery(). + AggregateTypes(user.AggregateType). + AggregateIDs(userID). + EventTypes(user.HumanRegisteredType, user.HumanAddedType, user.MachineAddedEventType) -func getGrantedOrgOfGrantedProject(ctx context.Context, es handler.EventStore, instanceID, projectID, grantID string) (string, error) { - events, err := es.Filter( - ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AwaitOpenTransactions(). - InstanceID(instanceID). - AddQuery(). + // if it's a project grant then we only need the resourceowner for the projectgrant, else the project + if grantID != "" { + eventCount++ + builder = builder.Or(). AggregateTypes(project.AggregateType). AggregateIDs(projectID). EventTypes(project.GrantAddedType). EventData(map[string]interface{}{ "grantId": grantID, - }). - Builder(), + }) + } else if projectID != "" { + eventCount++ + builder = builder.Or(). + AggregateTypes(project.AggregateType). + AggregateIDs(projectID). + EventTypes(project.ProjectAddedType) + } + + events, err := es.Filter( + ctx, + builder.Builder(), ) if err != nil { - return "", err + return "", "", "", err } - if len(events) != 1 { - return "", zerrors.ThrowNotFound(nil, "PROJ-MoaSpw", "Errors.Grant.NotFound") + if len(events) != eventCount { + return "", "", "", zerrors.ThrowNotFound(nil, "PROJ-0I91sp", "Errors.NotFound") } - grantAddedEvent, ok := events[0].(*project.GrantAddedEvent) - if !ok { - return "", zerrors.ThrowNotFound(nil, "PROJ-P0s2o0", "Errors.Grant.NotFound") + for _, event := range events { + switch e := event.(type) { + case *project.GrantAddedEvent: + grantedOrg = e.GrantedOrgID + case *project.ProjectAddedEvent: + projectRO = e.Aggregate().ResourceOwner + case *user.HumanRegisteredEvent, *user.HumanAddedEvent, *user.MachineAddedEvent: + userRO = e.Aggregate().ResourceOwner + } } - return grantAddedEvent.GrantedOrgID, nil + return userRO, projectRO, grantedOrg, nil } func setUserGrantContext(aggregate *eventstore.Aggregate) context.Context { diff --git a/internal/query/projection/user_grant_test.go b/internal/query/projection/user_grant_test.go index 22644d6e8..1268aef97 100644 --- a/internal/query/projection/user_grant_test.go +++ b/internal/query/projection/user_grant_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/stretchr/testify/require" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/database" @@ -57,16 +58,15 @@ func TestUserGrantProjection_reduces(t *testing.T) { "email1", true, ), - }).appendFilterResponse([]eventstore.Event{ - project.NewProjectAddedEvent(context.Background(), - &project.NewAggregate("project-id", "org2").Aggregate, - "project", - false, - false, - false, - domain.PrivateLabelingSettingUnspecified, - ), - }), + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project-id", "org2").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + }), }).reduceAdded, want: wantReduce{ aggregateType: usergrant.AggregateType, @@ -74,7 +74,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_grants3 (id, resource_owner, instance_id, creation_date, change_date, sequence, user_id, resource_owner_user, project_id, resource_owner_project, grant_id, granted_org, roles, state) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", + expectedStmt: "INSERT INTO projections.user_grants4 (id, resource_owner, instance_id, creation_date, change_date, sequence, user_id, resource_owner_user, project_id, resource_owner_project, grant_id, granted_org, roles, state) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", expectedArgs: []interface{}{ "agg-id", "ro-id", @@ -126,14 +126,13 @@ func TestUserGrantProjection_reduces(t *testing.T) { "email1", true, ), - }).appendFilterResponse([]eventstore.Event{ - project.NewGrantAddedEvent(context.Background(), - &project.NewAggregate("project-id", "org2").Aggregate, - "grant-id", - "org3", - []string{}, - ), - }), + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project-id", "org2").Aggregate, + "grant-id", + "org3", + []string{}, + ), + }), }).reduceAdded, want: wantReduce{ aggregateType: usergrant.AggregateType, @@ -141,7 +140,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.user_grants3 (id, resource_owner, instance_id, creation_date, change_date, sequence, user_id, resource_owner_user, project_id, resource_owner_project, grant_id, granted_org, roles, state) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", + expectedStmt: "INSERT INTO projections.user_grants4 (id, resource_owner, instance_id, creation_date, change_date, sequence, user_id, resource_owner_user, project_id, resource_owner_project, grant_id, granted_org, roles, state) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)", expectedArgs: []interface{}{ "agg-id", "ro-id", @@ -182,7 +181,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_grants3 SET (change_date, roles, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.user_grants4 SET (change_date, roles, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, database.TextArray[string]{"role"}, @@ -214,7 +213,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_grants3 SET (change_date, roles, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.user_grants4 SET (change_date, roles, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, database.TextArray[string]{"role"}, @@ -244,7 +243,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ anyArg{}, "instance-id", @@ -271,7 +270,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -297,7 +296,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ anyArg{}, "instance-id", @@ -324,7 +323,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_grants3 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.user_grants4 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserGrantStateInactive, @@ -354,7 +353,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_grants3 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", + expectedStmt: "UPDATE projections.user_grants4 SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedArgs: []interface{}{ anyArg{}, domain.UserGrantStateActive, @@ -384,7 +383,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (user_id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (user_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ anyArg{}, "instance-id", @@ -411,7 +410,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (project_id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (project_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ anyArg{}, "instance-id", @@ -438,7 +437,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (grant_id = $1) AND (instance_id = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (grant_id = $1) AND (instance_id = $2)", expectedArgs: []interface{}{ "grantID", "instance-id", @@ -465,7 +464,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_grants3 SET roles = array_remove(roles, $1) WHERE (project_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.user_grants4 SET roles = array_remove(roles, $1) WHERE (project_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ "key", "agg-id", @@ -493,7 +492,7 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "UPDATE projections.user_grants3 SET (roles) = (SELECT ARRAY( SELECT UNNEST(roles) INTERSECT SELECT UNNEST ($1::TEXT[]))) WHERE (grant_id = $2) AND (instance_id = $3)", + expectedStmt: "UPDATE projections.user_grants4 SET (roles) = (SELECT ARRAY( SELECT UNNEST(roles) INTERSECT SELECT UNNEST ($1::TEXT[]))) WHERE (grant_id = $2) AND (instance_id = $3)", expectedArgs: []interface{}{ database.TextArray[string]{"key"}, "grantID", @@ -521,28 +520,28 @@ func TestUserGrantProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (instance_id = $1) AND (resource_owner = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (instance_id = $1) AND (resource_owner = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", }, }, { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (instance_id = $1) AND (resource_owner_user = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (instance_id = $1) AND (resource_owner_user = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", }, }, { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (instance_id = $1) AND (resource_owner_project = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (instance_id = $1) AND (resource_owner_project = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", }, }, { - expectedStmt: "DELETE FROM projections.user_grants3 WHERE (instance_id = $1) AND (granted_org = $2)", + expectedStmt: "DELETE FROM projections.user_grants4 WHERE (instance_id = $1) AND (granted_org = $2)", expectedArgs: []interface{}{ "instance-id", "agg-id", @@ -567,3 +566,285 @@ func TestUserGrantProjection_reduces(t *testing.T) { }) } } + +func Test_getResourceOwners(t *testing.T) { + type args struct { + instanceID string + userID string + projectID string + grantID string + } + type fields struct { + eventstore *eventstore.Eventstore + } + type want struct { + userRO string + projectRO string + grantedOrg string + wantErr bool + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "user RO, filter error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilterError(zerrors.ThrowNotFound(nil, "error", "error")), + ), + }, + args: args{ + instanceID: "instance", + userID: "user", + }, + want: want{ + wantErr: true, + }, + }, + { + name: "user RO", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user", "org").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + ), + }, + args: args{ + instanceID: "instance", + userID: "user", + }, + want: want{ + userRO: "org", + wantErr: false, + }, + }, + { + name: "user RO, no user", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + instanceID: "instance", + userID: "user", + }, + want: want{ + wantErr: true, + }, + }, + { + name: "user and project RO", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user", "org").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project", "org").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + ), + }, + args: args{ + instanceID: "instance", + userID: "user", + projectID: "project", + }, + want: want{ + userRO: "org", + projectRO: "org", + wantErr: false, + }, + }, + { + name: "user and project RO, no project", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user", "org").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + ), + }, + args: args{ + instanceID: "instance", + userID: "user", + projectID: "project", + }, + want: want{ + wantErr: true, + }, + }, + { + name: "user and grant RO", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user", "org").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project", "org").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + ), + }, + args: args{ + instanceID: "instance", + userID: "user", + projectID: "project", + grantID: "projectgrant1", + }, + want: want{ + userRO: "org", + grantedOrg: "grantedorg1", + wantErr: false, + }, + }, + { + name: "user and grant RO, no grant", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user", "org").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + ), + }, + args: args{ + instanceID: "instance", + userID: "user", + projectID: "project", + grantID: "projectgrant1", + }, + want: want{ + wantErr: true, + }, + }, + { + name: "user and grant RO, no user", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project", "org").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + ), + }, + args: args{ + instanceID: "instance", + userID: "user", + projectID: "project", + grantID: "projectgrant1", + }, + want: want{ + wantErr: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + userRO, projectRO, grantedOrg, err := getResourceOwners(context.Background(), tt.fields.eventstore, tt.args.instanceID, tt.args.userID, tt.args.projectID, tt.args.grantID) + if tt.want.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want.userRO, userRO) + require.Equal(t, tt.want.projectRO, projectRO) + require.Equal(t, tt.want.grantedOrg, grantedOrg) + } + }) + } +} diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index 9e3cd3790..d33fb6ba4 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -209,32 +209,9 @@ var ( name: projection.UserGrantState, table: userGrantTable, } - UserGrantOwnerRemoved = Column{ - name: projection.UserGrantOwnerRemoved, - table: userGrantTable, - } - UserGrantUserOwnerRemoved = Column{ - name: projection.UserGrantUserOwnerRemoved, - table: userGrantTable, - } - UserGrantProjectOwnerRemoved = Column{ - name: projection.UserGrantProjectOwnerRemoved, - table: userGrantTable, - } - UserGrantGrantGrantedOrgRemoved = Column{ - name: projection.UserGrantGrantedOrgRemoved, - table: userGrantTable, - } ) -func addUserGrantWithoutOwnerRemoved(eq map[string]interface{}) { - eq[UserGrantOwnerRemoved.identifier()] = false - eq[UserGrantUserOwnerRemoved.identifier()] = false - eq[UserGrantProjectOwnerRemoved.identifier()] = false - eq[UserGrantGrantGrantedOrgRemoved.identifier()] = false -} - -func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, withOwnerRemoved bool, queries ...SearchQuery) (grant *UserGrant, err error) { +func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, queries ...SearchQuery) (grant *UserGrant, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -250,9 +227,6 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, withOwn query = q.toQuery(query) } eq := sq.Eq{UserGrantInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} - if !withOwnerRemoved { - addUserGrantWithoutOwnerRemoved(eq) - } stmt, args, err := query.Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Fa1KW", "Errors.Query.SQLStatement") @@ -265,7 +239,7 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, withOwn return grant, err } -func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk, withOwnerRemoved bool) (grants *UserGrants, err error) { +func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool) (grants *UserGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -278,9 +252,6 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh query, scan := prepareUserGrantsQuery(ctx, q.client) eq := sq.Eq{UserGrantInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} - if !withOwnerRemoved { - addUserGrantWithoutOwnerRemoved(eq) - } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-wXnQR", "Errors.Query.SQLStatement") diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 67e7fb9b8..fc09ca319 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -15,14 +15,14 @@ import ( var ( userGrantStmt = regexp.QuoteMeta( - "SELECT projections.user_grants3.id" + - ", projections.user_grants3.creation_date" + - ", projections.user_grants3.change_date" + - ", projections.user_grants3.sequence" + - ", projections.user_grants3.grant_id" + - ", projections.user_grants3.roles" + - ", projections.user_grants3.state" + - ", projections.user_grants3.user_id" + + "SELECT projections.user_grants4.id" + + ", projections.user_grants4.creation_date" + + ", projections.user_grants4.change_date" + + ", projections.user_grants4.sequence" + + ", projections.user_grants4.grant_id" + + ", projections.user_grants4.roles" + + ", projections.user_grants4.state" + + ", projections.user_grants4.user_id" + ", projections.users10.username" + ", projections.users10.type" + ", projections.users10.resource_owner" + @@ -32,17 +32,17 @@ var ( ", projections.users10_humans.display_name" + ", projections.users10_humans.avatar_key" + ", projections.login_names3.login_name" + - ", projections.user_grants3.resource_owner" + + ", projections.user_grants4.resource_owner" + ", projections.orgs1.name" + ", projections.orgs1.primary_domain" + - ", projections.user_grants3.project_id" + + ", projections.user_grants4.project_id" + ", projections.projects4.name" + - " FROM projections.user_grants3" + - " LEFT JOIN projections.users10 ON projections.user_grants3.user_id = projections.users10.id AND projections.user_grants3.instance_id = projections.users10.instance_id" + - " LEFT JOIN projections.users10_humans ON projections.user_grants3.user_id = projections.users10_humans.user_id AND projections.user_grants3.instance_id = projections.users10_humans.instance_id" + - " LEFT JOIN projections.orgs1 ON projections.user_grants3.resource_owner = projections.orgs1.id AND projections.user_grants3.instance_id = projections.orgs1.instance_id" + - " LEFT JOIN projections.projects4 ON projections.user_grants3.project_id = projections.projects4.id AND projections.user_grants3.instance_id = projections.projects4.instance_id" + - " LEFT JOIN projections.login_names3 ON projections.user_grants3.user_id = projections.login_names3.user_id AND projections.user_grants3.instance_id = projections.login_names3.instance_id" + + " FROM projections.user_grants4" + + " LEFT JOIN projections.users10 ON projections.user_grants4.user_id = projections.users10.id AND projections.user_grants4.instance_id = projections.users10.instance_id" + + " LEFT JOIN projections.users10_humans ON projections.user_grants4.user_id = projections.users10_humans.user_id AND projections.user_grants4.instance_id = projections.users10_humans.instance_id" + + " LEFT JOIN projections.orgs1 ON projections.user_grants4.resource_owner = projections.orgs1.id AND projections.user_grants4.instance_id = projections.orgs1.instance_id" + + " LEFT JOIN projections.projects4 ON projections.user_grants4.project_id = projections.projects4.id AND projections.user_grants4.instance_id = projections.projects4.instance_id" + + " LEFT JOIN projections.login_names3 ON projections.user_grants4.user_id = projections.login_names3.user_id AND projections.user_grants4.instance_id = projections.login_names3.instance_id" + ` AS OF SYSTEM TIME '-1 ms' ` + " WHERE projections.login_names3.is_primary = $1") userGrantCols = []string{ @@ -70,14 +70,14 @@ var ( "name", //project name } userGrantsStmt = regexp.QuoteMeta( - "SELECT projections.user_grants3.id" + - ", projections.user_grants3.creation_date" + - ", projections.user_grants3.change_date" + - ", projections.user_grants3.sequence" + - ", projections.user_grants3.grant_id" + - ", projections.user_grants3.roles" + - ", projections.user_grants3.state" + - ", projections.user_grants3.user_id" + + "SELECT projections.user_grants4.id" + + ", projections.user_grants4.creation_date" + + ", projections.user_grants4.change_date" + + ", projections.user_grants4.sequence" + + ", projections.user_grants4.grant_id" + + ", projections.user_grants4.roles" + + ", projections.user_grants4.state" + + ", projections.user_grants4.user_id" + ", projections.users10.username" + ", projections.users10.type" + ", projections.users10.resource_owner" + @@ -87,18 +87,18 @@ var ( ", projections.users10_humans.display_name" + ", projections.users10_humans.avatar_key" + ", projections.login_names3.login_name" + - ", projections.user_grants3.resource_owner" + + ", projections.user_grants4.resource_owner" + ", projections.orgs1.name" + ", projections.orgs1.primary_domain" + - ", projections.user_grants3.project_id" + + ", projections.user_grants4.project_id" + ", projections.projects4.name" + ", COUNT(*) OVER ()" + - " FROM projections.user_grants3" + - " LEFT JOIN projections.users10 ON projections.user_grants3.user_id = projections.users10.id AND projections.user_grants3.instance_id = projections.users10.instance_id" + - " LEFT JOIN projections.users10_humans ON projections.user_grants3.user_id = projections.users10_humans.user_id AND projections.user_grants3.instance_id = projections.users10_humans.instance_id" + - " LEFT JOIN projections.orgs1 ON projections.user_grants3.resource_owner = projections.orgs1.id AND projections.user_grants3.instance_id = projections.orgs1.instance_id" + - " LEFT JOIN projections.projects4 ON projections.user_grants3.project_id = projections.projects4.id AND projections.user_grants3.instance_id = projections.projects4.instance_id" + - " LEFT JOIN projections.login_names3 ON projections.user_grants3.user_id = projections.login_names3.user_id AND projections.user_grants3.instance_id = projections.login_names3.instance_id" + + " FROM projections.user_grants4" + + " LEFT JOIN projections.users10 ON projections.user_grants4.user_id = projections.users10.id AND projections.user_grants4.instance_id = projections.users10.instance_id" + + " LEFT JOIN projections.users10_humans ON projections.user_grants4.user_id = projections.users10_humans.user_id AND projections.user_grants4.instance_id = projections.users10_humans.instance_id" + + " LEFT JOIN projections.orgs1 ON projections.user_grants4.resource_owner = projections.orgs1.id AND projections.user_grants4.instance_id = projections.orgs1.instance_id" + + " LEFT JOIN projections.projects4 ON projections.user_grants4.project_id = projections.projects4.id AND projections.user_grants4.instance_id = projections.projects4.instance_id" + + " LEFT JOIN projections.login_names3 ON projections.user_grants4.user_id = projections.login_names3.user_id AND projections.user_grants4.instance_id = projections.login_names3.instance_id" + ` AS OF SYSTEM TIME '-1 ms' ` + " WHERE projections.login_names3.is_primary = $1") userGrantsCols = append(